From 55b15ea8fea46a9b86c1115dcc9af08a5c6275dd Mon Sep 17 00:00:00 2001 From: Estrella Pan <33726646+EstrellaXD@users.noreply.github.com> Date: Sun, 25 Jan 2026 09:13:49 +0100 Subject: [PATCH] feat: add bangumi archive and episode offset features (#958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add bangumi archive and episode offset features Archive Feature: - Add archived field to Bangumi model with database migration (v4) - Add archive/unarchive API endpoints (PATCH /bangumi/archive/{id}) - Add auto-archive for ended series via TMDB metadata refresh - Add collapsible archived section in UI with visual styling - Add archive/unarchive button in edit rule popup Episode Offset Feature: - Extract series_status and season_episode_counts from TMDB API - Add suggest-offset API endpoint with auto-detection logic - Apply offset in renamer gen_path() for episode numbering - Add offset field with "Auto Detect" button in rule editor - Look up offset from database when renaming files The offset auto-detection calculates the sum of episodes from all previous seasons (e.g., if S01 has 13 episodes, S02E18 → S02E05 with offset=-13). Co-Authored-By: Claude Opus 4.5 * docs: add changelog for bangumi archive and episode offset features Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- CHANGELOG.md | 28 ++ backend/src/module/api/bangumi.py | 54 ++++ backend/src/module/database/bangumi.py | 46 ++- backend/src/module/database/combine.py | 11 +- backend/src/module/manager/renamer.py | 44 ++- backend/src/module/manager/torrent.py | 115 +++++++ backend/src/module/models/bangumi.py | 2 + .../src/module/parser/analyser/tmdb_parser.py | 36 ++- webui/src/api/bangumi.ts | 45 ++- webui/src/components/ab-edit-rule.vue | 24 ++ webui/src/components/ab-rule.vue | 87 ++++- webui/src/i18n/en.json | 7 +- webui/src/i18n/zh-CN.json | 5 + webui/src/pages/index.vue | 6 +- webui/src/pages/index/bangumi.vue | 298 +++++++++++++++++- webui/src/store/bangumi.ts | 20 ++ webui/types/bangumi.ts | 7 + 17 files changed, 787 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 001527ab..85371717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +# [3.2.0-beta.6] - 2026-01-25 + +## Backend + +### Features + +- 新增番剧归档功能:支持手动归档/取消归档,已完结番剧自动归档 +- 新增剧集偏移自动检测:根据 TMDB 季度集数自动计算偏移量(如 S02E18 → S02E05) +- TMDB 解析器新增 `series_status` 和 `season_episode_counts` 字段提取 +- 新增数据库迁移 v4:为 `bangumi` 表添加 `archived` 字段 +- 新增 API 端点: + - `PATCH /bangumi/archive/{id}` - 归档番剧 + - `PATCH /bangumi/unarchive/{id}` - 取消归档 + - `GET /bangumi/refresh/metadata` - 刷新元数据并自动归档已完结番剧 + - `GET /bangumi/suggest-offset/{id}` - 获取建议的剧集偏移量 +- 重命名模块支持从数据库查询偏移量并应用到文件名 + +## Frontend + +### Features + +- 番剧列表页新增可折叠的「已归档」分区 +- 规则编辑弹窗新增归档/取消归档按钮 +- 规则编辑器新增剧集偏移字段和「自动检测」按钮 +- 新增 i18n 翻译(中文/英文) + +--- + # [3.2.0-beta.5] - 2026-01-24 ## Backend diff --git a/backend/src/module/api/bangumi.py b/backend/src/module/api/bangumi.py index 8646f49c..b9a69c1d 100644 --- a/backend/src/module/api/bangumi.py +++ b/backend/src/module/api/bangumi.py @@ -1,5 +1,6 @@ from fastapi import APIRouter, Depends from fastapi.responses import JSONResponse +from pydantic import BaseModel from module.manager import TorrentManager from module.models import APIResponse, Bangumi, BangumiUpdate @@ -7,6 +8,11 @@ from module.security.api import UNAUTHORIZED, get_current_user from .response import u_response + +class OffsetSuggestion(BaseModel): + suggested_offset: int + reason: str + router = APIRouter(prefix="/bangumi", tags=["bangumi"]) @@ -148,3 +154,51 @@ async def reset_all(): status_code=200, content={"msg_en": "Reset all rules successfully.", "msg_zh": "重置所有规则成功。"}, ) + + +@router.patch( + path="/archive/{bangumi_id}", + response_model=APIResponse, + dependencies=[Depends(get_current_user)], +) +async def archive_rule(bangumi_id: int): + """Archive a bangumi.""" + with TorrentManager() as manager: + resp = manager.archive_rule(bangumi_id) + return u_response(resp) + + +@router.patch( + path="/unarchive/{bangumi_id}", + response_model=APIResponse, + dependencies=[Depends(get_current_user)], +) +async def unarchive_rule(bangumi_id: int): + """Unarchive a bangumi.""" + with TorrentManager() as manager: + resp = manager.unarchive_rule(bangumi_id) + return u_response(resp) + + +@router.get( + path="/refresh/metadata", + response_model=APIResponse, + dependencies=[Depends(get_current_user)], +) +async def refresh_metadata(): + """Refresh TMDB metadata and auto-archive ended series.""" + with TorrentManager() as manager: + resp = await manager.refresh_metadata() + return u_response(resp) + + +@router.get( + path="/suggest-offset/{bangumi_id}", + response_model=OffsetSuggestion, + dependencies=[Depends(get_current_user)], +) +async def suggest_offset(bangumi_id: int): + """Suggest offset based on TMDB episode counts.""" + with TorrentManager() as manager: + resp = await manager.suggest_offset(bangumi_id) + return resp diff --git a/backend/src/module/database/bangumi.py b/backend/src/module/database/bangumi.py index d1c1e9f5..3277d206 100644 --- a/backend/src/module/database/bangumi.py +++ b/backend/src/module/database/bangumi.py @@ -172,11 +172,17 @@ class BangumiDatabase: return unmatched def match_torrent(self, torrent_name: str) -> Optional[Bangumi]: - statement = select(Bangumi).where( - and_( - func.instr(torrent_name, Bangumi.title_raw) > 0, - Bangumi.deleted == false(), + statement = ( + select(Bangumi) + .where( + and_( + func.instr(torrent_name, Bangumi.title_raw) > 0, + Bangumi.deleted == false(), + ) ) + # Prefer longer title_raw matches (more specific) + .order_by(func.length(Bangumi.title_raw).desc()) + .limit(1) ) result = self.session.execute(statement) return result.scalar_one_or_none() @@ -213,3 +219,35 @@ class BangumiDatabase: statement = select(Bangumi).where(func.instr(rss_link, Bangumi.rss_link) > 0) result = self.session.execute(statement) return list(result.scalars().all()) + + def archive_one(self, _id: int) -> bool: + """Set archived=True for the given bangumi.""" + bangumi = self.session.get(Bangumi, _id) + if not bangumi: + logger.warning(f"[Database] Cannot archive bangumi id: {_id}, not found.") + return False + bangumi.archived = True + self.session.add(bangumi) + self.session.commit() + _invalidate_bangumi_cache() + logger.debug(f"[Database] Archived bangumi id: {_id}.") + return True + + def unarchive_one(self, _id: int) -> bool: + """Set archived=False for the given bangumi.""" + bangumi = self.session.get(Bangumi, _id) + if not bangumi: + logger.warning(f"[Database] Cannot unarchive bangumi id: {_id}, not found.") + return False + bangumi.archived = False + self.session.add(bangumi) + self.session.commit() + _invalidate_bangumi_cache() + logger.debug(f"[Database] Unarchived bangumi id: {_id}.") + return True + + def match_by_save_path(self, save_path: str) -> Optional[Bangumi]: + """Find bangumi by save_path to get offset.""" + statement = select(Bangumi).where(Bangumi.save_path == save_path) + result = self.session.execute(statement) + return result.scalar_one_or_none() diff --git a/backend/src/module/database/combine.py b/backend/src/module/database/combine.py index 72c73c83..e9e4632a 100644 --- a/backend/src/module/database/combine.py +++ b/backend/src/module/database/combine.py @@ -15,7 +15,7 @@ from .user import UserDatabase logger = logging.getLogger(__name__) # Increment this when adding new migrations to MIGRATIONS list. -CURRENT_SCHEMA_VERSION = 3 +CURRENT_SCHEMA_VERSION = 4 # Each migration is a tuple of (version, description, list of SQL statements). # Migrations are applied in order. A migration at index i brings the schema @@ -57,6 +57,11 @@ MIGRATIONS = [ "CREATE UNIQUE INDEX IF NOT EXISTS ix_passkey_credential_id ON passkey(credential_id)", ], ), + ( + 4, + "add archived column to bangumi", + ["ALTER TABLE bangumi ADD COLUMN archived BOOLEAN DEFAULT 0"], + ), ] @@ -125,6 +130,10 @@ class Database(Session): needs_run = False if version == 3 and "passkey" in tables: needs_run = False + if "bangumi" in tables and version == 4: + columns = [col["name"] for col in inspector.get_columns("bangumi")] + if "archived" in columns: + needs_run = False if needs_run: with self.engine.connect() as conn: for stmt in statements: diff --git a/backend/src/module/manager/renamer.py b/backend/src/module/manager/renamer.py index 98d6a6ab..13e575c8 100644 --- a/backend/src/module/manager/renamer.py +++ b/backend/src/module/manager/renamer.py @@ -3,6 +3,7 @@ import logging import re from module.conf import settings +from module.database import Database from module.downloader import DownloadClient from module.models import EpisodeFile, Notification, SubtitleFile from module.parser import TitleParser @@ -26,12 +27,18 @@ class Renamer(DownloadClient): @staticmethod def gen_path( - file_info: EpisodeFile | SubtitleFile, bangumi_name: str, method: str + file_info: EpisodeFile | SubtitleFile, + bangumi_name: str, + method: str, + offset: int = 0, ) -> str: season = f"0{file_info.season}" if file_info.season < 10 else file_info.season - episode = ( - f"0{file_info.episode}" if file_info.episode < 10 else file_info.episode - ) + # Apply offset (offset is stored as the value to ADD) + adjusted_episode = int(file_info.episode) + offset + if adjusted_episode < 1: + adjusted_episode = int(file_info.episode) # Safety: don't go below 1 + logger.warning(f"[Renamer] Offset {offset} would result in negative episode, ignoring") + episode = f"0{adjusted_episode}" if adjusted_episode < 10 else adjusted_episode if method == "none" or method == "subtitle_none": return file_info.media_path elif method == "pn": @@ -57,6 +64,7 @@ class Renamer(DownloadClient): method: str, season: int, _hash: str, + offset: int = 0, **kwargs, ): ep = self._parser.torrent_parser( @@ -65,16 +73,20 @@ class Renamer(DownloadClient): season=season, ) if ep: - new_path = self.gen_path(ep, bangumi_name, method=method) + new_path = self.gen_path(ep, bangumi_name, method=method, offset=offset) if media_path != new_path: if new_path not in self.check_pool.keys(): if await self.rename_torrent_file( _hash=_hash, old_path=media_path, new_path=new_path ): + # Return adjusted episode number for notification + adjusted_episode = int(ep.episode) + offset + if adjusted_episode < 1: + adjusted_episode = int(ep.episode) return Notification( official_title=bangumi_name, season=ep.season, - episode=ep.episode, + episode=adjusted_episode, ) else: logger.warning(f"[Renamer] {media_path} parse failed") @@ -89,6 +101,7 @@ class Renamer(DownloadClient): season: int, method: str, _hash: str, + offset: int = 0, **kwargs, ): for media_path in media_list: @@ -98,7 +111,7 @@ class Renamer(DownloadClient): season=season, ) if ep: - new_path = self.gen_path(ep, bangumi_name, method=method) + new_path = self.gen_path(ep, bangumi_name, method=method, offset=offset) if media_path != new_path: renamed = await self.rename_torrent_file( _hash=_hash, old_path=media_path, new_path=new_path @@ -118,6 +131,7 @@ class Renamer(DownloadClient): season: int, method: str, _hash, + offset: int = 0, **kwargs, ): method = "subtitle_" + method @@ -129,7 +143,7 @@ class Renamer(DownloadClient): file_type="subtitle", ) if sub: - new_path = self.gen_path(sub, bangumi_name, method=method) + new_path = self.gen_path(sub, bangumi_name, method=method, offset=offset) if subtitle_path != new_path: renamed = await self.rename_torrent_file( _hash=_hash, old_path=subtitle_path, new_path=new_path @@ -137,6 +151,17 @@ class Renamer(DownloadClient): if not renamed: logger.warning(f"[Renamer] {subtitle_path} rename failed") + def _lookup_offset_by_path(self, save_path: str) -> int: + """Look up the offset for a bangumi by its save_path.""" + try: + with Database() as db: + bangumi = db.bangumi.match_by_save_path(save_path) + if bangumi: + return bangumi.offset + except Exception as e: + logger.debug(f"[Renamer] Could not lookup offset for {save_path}: {e}") + return 0 + async def rename(self) -> list[Notification]: # Get torrent info logger.debug("[Renamer] Start rename process.") @@ -153,12 +178,15 @@ class Renamer(DownloadClient): save_path = info["save_path"] media_list, subtitle_list = self.check_files(files) bangumi_name, season = self._path_to_bangumi(save_path) + # Look up offset from database + offset = self._lookup_offset_by_path(save_path) kwargs = { "torrent_name": torrent_name, "bangumi_name": bangumi_name, "method": rename_method, "season": season, "_hash": torrent_hash, + "offset": offset, } # Rename single media file if len(media_list) == 1: diff --git a/backend/src/module/manager/torrent.py b/backend/src/module/manager/torrent.py index c8edae3f..f2e4b9c4 100644 --- a/backend/src/module/manager/torrent.py +++ b/backend/src/module/manager/torrent.py @@ -1,10 +1,12 @@ import logging +from module.conf import settings from module.database import Database from module.downloader import DownloadClient from module.models import Bangumi, BangumiUpdate, ResponseModel from module.parser import TitleParser from module.parser.analyser.bgm_calendar import fetch_bgm_calendar, match_weekday +from module.parser.analyser.tmdb_parser import tmdb_parser logger = logging.getLogger(__name__) @@ -206,3 +208,116 @@ class TorrentManager(Database): ) else: return data + + def archive_rule(self, _id: int): + """Archive a bangumi.""" + data = self.bangumi.search_id(_id) + if not data: + return ResponseModel( + status_code=406, + status=False, + msg_en=f"Can't find id {_id}", + msg_zh=f"无法找到 id {_id}", + ) + if self.bangumi.archive_one(_id): + logger.info(f"[Manager] Archived {data.official_title}") + return ResponseModel( + status_code=200, + status=True, + msg_en=f"Archived {data.official_title}", + msg_zh=f"已归档 {data.official_title}", + ) + return ResponseModel( + status_code=500, + status=False, + msg_en=f"Failed to archive {data.official_title}", + msg_zh=f"归档 {data.official_title} 失败", + ) + + def unarchive_rule(self, _id: int): + """Unarchive a bangumi.""" + data = self.bangumi.search_id(_id) + if not data: + return ResponseModel( + status_code=406, + status=False, + msg_en=f"Can't find id {_id}", + msg_zh=f"无法找到 id {_id}", + ) + if self.bangumi.unarchive_one(_id): + logger.info(f"[Manager] Unarchived {data.official_title}") + return ResponseModel( + status_code=200, + status=True, + msg_en=f"Unarchived {data.official_title}", + msg_zh=f"已取消归档 {data.official_title}", + ) + return ResponseModel( + status_code=500, + status=False, + msg_en=f"Failed to unarchive {data.official_title}", + msg_zh=f"取消归档 {data.official_title} 失败", + ) + + async def refresh_metadata(self): + """Refresh TMDB metadata and auto-archive ended series.""" + bangumis = self.bangumi.search_all() + language = settings.rss_parser.language + archived_count = 0 + poster_count = 0 + + for bangumi in bangumis: + if bangumi.deleted: + continue + tmdb_info = await tmdb_parser(bangumi.official_title, language) + if tmdb_info: + # Update poster if missing + if not bangumi.poster_link and tmdb_info.poster_link: + bangumi.poster_link = tmdb_info.poster_link + poster_count += 1 + # Auto-archive ended series + if tmdb_info.series_status == "Ended" and not bangumi.archived: + bangumi.archived = True + archived_count += 1 + logger.info(f"[Manager] Auto-archived ended series: {bangumi.official_title}") + + if archived_count > 0 or poster_count > 0: + self.bangumi.update_all(bangumis) + + logger.info(f"[Manager] Metadata refresh: archived {archived_count}, updated posters {poster_count}") + return ResponseModel( + status_code=200, + status=True, + msg_en=f"Metadata refreshed. Archived {archived_count} ended series, updated {poster_count} posters.", + msg_zh=f"已刷新元数据。归档了 {archived_count} 部已完结番剧,更新了 {poster_count} 个海报。", + ) + + async def suggest_offset(self, bangumi_id: int) -> dict: + """Suggest offset based on TMDB episode counts.""" + data = self.bangumi.search_id(bangumi_id) + if not data: + return {"suggested_offset": 0, "reason": f"Bangumi id {bangumi_id} not found"} + + language = settings.rss_parser.language + tmdb_info = await tmdb_parser(data.official_title, language) + + if not tmdb_info or not tmdb_info.season_episode_counts: + return {"suggested_offset": 0, "reason": "Unable to fetch TMDB episode data"} + + season = data.season + if season <= 1: + return {"suggested_offset": 0, "reason": "Season 1 does not need offset"} + + offset = tmdb_info.get_offset_for_season(season) + if offset == 0: + return {"suggested_offset": 0, "reason": "No previous seasons found"} + + # Build reason with episode counts + prev_seasons = [ + f"S{s}: {tmdb_info.season_episode_counts.get(s, 0)} eps" + for s in range(1, season) + if s in tmdb_info.season_episode_counts + ] + reason = f"Previous seasons: {', '.join(prev_seasons)}" + + return {"suggested_offset": offset, "reason": reason} diff --git a/backend/src/module/models/bangumi.py b/backend/src/module/models/bangumi.py index b03380d7..c19b509b 100644 --- a/backend/src/module/models/bangumi.py +++ b/backend/src/module/models/bangumi.py @@ -27,6 +27,7 @@ class Bangumi(SQLModel, table=True): rule_name: Optional[str] = Field(alias="rule_name", title="番剧规则名") save_path: Optional[str] = Field(alias="save_path", title="番剧保存路径") deleted: bool = Field(False, alias="deleted", title="是否已删除", index=True) + archived: bool = Field(default=False, alias="archived", title="是否已归档", index=True) air_weekday: Optional[int] = Field(default=None, alias="air_weekday", title="放送星期") @@ -51,6 +52,7 @@ class BangumiUpdate(SQLModel): rule_name: Optional[str] = Field(alias="rule_name", title="番剧规则名") save_path: Optional[str] = Field(alias="save_path", title="番剧保存路径") deleted: bool = Field(False, alias="deleted", title="是否已删除") + archived: bool = Field(default=False, alias="archived", title="是否已归档") air_weekday: Optional[int] = Field(default=None, alias="air_weekday", title="放送星期") diff --git a/backend/src/module/parser/analyser/tmdb_parser.py b/backend/src/module/parser/analyser/tmdb_parser.py index 2a2b4217..431ca18a 100644 --- a/backend/src/module/parser/analyser/tmdb_parser.py +++ b/backend/src/module/parser/analyser/tmdb_parser.py @@ -24,6 +24,18 @@ class TMDBInfo: last_season: int year: str poster_link: str = None + series_status: str = None # "Ended", "Returning Series", etc. + season_episode_counts: dict[int, int] = None # {1: 13, 2: 12, ...} + + def get_offset_for_season(self, season: int) -> int: + """Calculate offset for a season (negative sum of all previous seasons' episodes). + + Used when RSS episode numbers are absolute (e.g., S02E18 should be S02E05). + Returns the offset to subtract from the parsed episode number. + """ + if not self.season_episode_counts or season <= 1: + return 0 + return -sum(self.season_episode_counts.get(s, 0) for s in range(1, season)) LANGUAGE = {"zh": "zh-CN", "jp": "ja-JP", "en": "en-US"} @@ -97,6 +109,14 @@ async def tmdb_parser(title, language, test: bool = False) -> TMDBInfo | None: for s in info_content.get("seasons") ] last_season, poster_path = get_season(season) + # Extract series status (e.g., "Ended", "Returning Series") + series_status = info_content.get("status") + # Extract episode counts per season (exclude specials at season 0) + season_episode_counts = { + s.get("season_number"): s.get("episode_count", 0) + for s in info_content.get("seasons", []) + if s.get("season_number", 0) > 0 + } if poster_path is None: poster_path = info_content.get("poster_path") original_title = info_content.get("original_name") @@ -111,13 +131,15 @@ async def tmdb_parser(title, language, test: bool = False) -> TMDBInfo | None: else: poster_link = None result = TMDBInfo( - id, - official_title, - original_title, - season, - last_season, - str(year_number), - poster_link, + id=id, + title=official_title, + original_title=original_title, + season=season, + last_season=last_season, + year=str(year_number), + poster_link=poster_link, + series_status=series_status, + season_episode_counts=season_episode_counts, ) _tmdb_cache[cache_key] = result return result diff --git a/webui/src/api/bangumi.ts b/webui/src/api/bangumi.ts index c7c057a2..121f2680 100644 --- a/webui/src/api/bangumi.ts +++ b/webui/src/api/bangumi.ts @@ -1,5 +1,5 @@ import { omit } from 'radash'; -import type { BangumiAPI, BangumiRule } from '#/bangumi'; +import type { BangumiAPI, BangumiRule, OffsetSuggestion } from '#/bangumi'; import type { ApiSuccess } from '#/api'; export const apiBangumi = { @@ -146,4 +146,47 @@ export const apiBangumi = { ); return data; }, + + /** + * 归档指定 bangumi + * @param bangumiId - 需要归档的 bangumi 的 id + */ + async archiveRule(bangumiId: number) { + const { data } = await axios.patch( + `api/v1/bangumi/archive/${bangumiId}` + ); + return data; + }, + + /** + * 取消归档指定 bangumi + * @param bangumiId - 需要取消归档的 bangumi 的 id + */ + async unarchiveRule(bangumiId: number) { + const { data } = await axios.patch( + `api/v1/bangumi/unarchive/${bangumiId}` + ); + return data; + }, + + /** + * 刷新 TMDB 元数据并自动归档已完结番剧 + */ + async refreshMetadata() { + const { data } = await axios.get( + 'api/v1/bangumi/refresh/metadata' + ); + return data; + }, + + /** + * 获取自动检测的剧集偏移量建议 + * @param bangumiId - bangumi 的 id + */ + async suggestOffset(bangumiId: number) { + const { data } = await axios.get( + `api/v1/bangumi/suggest-offset/${bangumiId}` + ); + return data; + }, }; diff --git a/webui/src/components/ab-edit-rule.vue b/webui/src/components/ab-edit-rule.vue index 03b0283a..c236b8fd 100644 --- a/webui/src/components/ab-edit-rule.vue +++ b/webui/src/components/ab-edit-rule.vue @@ -4,6 +4,8 @@ import type { BangumiRule } from '#/bangumi'; const emit = defineEmits<{ (e: 'apply', rule: BangumiRule): void; (e: 'enable', id: number): void; + (e: 'archive', id: number): void; + (e: 'unarchive', id: number): void; ( e: 'deleteFile', type: 'disable' | 'delete', @@ -57,6 +59,14 @@ function emitEnable() { emit('enable', rule.value.id); } +function emitArchive() { + emit('archive', rule.value.id); +} + +function emitUnarchive() { + emit('unarchive', rule.value.id); +} + const popupTitle = computed(() => { if (rule.value.deleted) { return t('homepage.rule.enable_rule'); @@ -99,6 +109,20 @@ const boxSize = computed(() => {
+ + {{ $t('homepage.rule.unarchive') }} + + + {{ $t('homepage.rule.archive') }} + ('rule', { required: true, }); +const offsetLoading = ref(false); +const offsetReason = ref(''); + +async function autoDetectOffset() { + if (!rule.value.id) return; + offsetLoading.value = true; + offsetReason.value = ''; + try { + const result = await apiBangumi.suggestOffset(rule.value.id); + rule.value.offset = result.suggested_offset; + offsetReason.value = result.reason; + } catch (e) { + console.error('Failed to detect offset:', e); + } finally { + offsetLoading.value = false; + } +} + const items: SettingItem[] = [ { configKey: 'official_title', @@ -36,15 +54,6 @@ const items: SettingItem[] = [ }, bottomLine: true, }, - // { - // configKey: 'offset', - // label: () => t('homepage.rule.offset'), - // type: 'input', - // css: 'w-72', - // prop: { - // type: 'number', - // }, - // }, { configKey: 'filter', label: () => t('homepage.rule.exclude'), @@ -62,5 +71,65 @@ const items: SettingItem[] = [ v-bind="i" v-model:data="rule[i.configKey]" > + + +
+
{{ $t('homepage.rule.offset') }}
+
+ + + {{ $t('homepage.rule.auto_detect') }} + +
+
{{ offsetReason }}
+
+ + diff --git a/webui/src/i18n/en.json b/webui/src/i18n/en.json index 74fcd1a9..39a1f5e8 100644 --- a/webui/src/i18n/en.json +++ b/webui/src/i18n/en.json @@ -131,7 +131,12 @@ "exclude": "Exclude", "no_btn": "No", "official_title": "Official Title", - "offset": "Offset", + "offset": "Episode Offset", + "auto_detect": "Auto Detect", + "archive": "Archive", + "unarchive": "Unarchive", + "archived": "Archived", + "archived_section": "Archived ({count})", "season": "Season", "year": "Year", "yes_btn": "Yes" diff --git a/webui/src/i18n/zh-CN.json b/webui/src/i18n/zh-CN.json index db6dd941..8eeea9ea 100644 --- a/webui/src/i18n/zh-CN.json +++ b/webui/src/i18n/zh-CN.json @@ -132,6 +132,11 @@ "no_btn": "否", "official_title": "官方名称", "offset": "剧集偏移", + "auto_detect": "自动检测", + "archive": "归档", + "unarchive": "取消归档", + "archived": "已归档", + "archived_section": "已归档 ({count})", "season": "季度", "year": "年份", "yes_btn": "是" diff --git a/webui/src/pages/index.vue b/webui/src/pages/index.vue index 4f731abf..c4534c90 100644 --- a/webui/src/pages/index.vue +++ b/webui/src/pages/index.vue @@ -5,7 +5,7 @@ definePage({ }); const { editRule } = storeToRefs(useBangumiStore()); -const { updateRule, enableRule, ruleManage } = useBangumiStore(); +const { updateRule, enableRule, archiveRule, unarchiveRule, ruleManage } = useBangumiStore();