diff --git a/app/chain/download.py b/app/chain/download.py index 288b3f290..103e26843 100644 --- a/app/chain/download.py +++ b/app/chain/download.py @@ -951,6 +951,7 @@ class DownloadChain(ChainBase): seasons=_meta.season, episodes=download_episodes or _meta.episode, image=_media.get_backdrop_image(), + poster=_media.get_poster_image(), downloader=_downloader, download_hash=_hash, torrent_name=_torrent.title, @@ -1777,7 +1778,9 @@ class DownloadChain(ChainBase): "title": history.title, "season": history.seasons, "episode": history.episodes, - "image": history.image, + "image": history.poster, + "poster": history.poster, + "backdrop": history.image, } torrent.site_name = history.torrent_site # 下载用户 diff --git a/app/db/models/downloadhistory.py b/app/db/models/downloadhistory.py index c39b6b847..5e12922dc 100644 --- a/app/db/models/downloadhistory.py +++ b/app/db/models/downloadhistory.py @@ -39,8 +39,10 @@ class DownloadHistory(Base): seasons = Column(String) # Exx episodes = Column(String) - # 海报 + # 背景图 image = Column(String) + # 海报 + poster = Column(String) # 下载器 downloader = Column(String) # 下载任务Hash diff --git a/app/schemas/history.py b/app/schemas/history.py index 48fccca18..04f14ea5b 100644 --- a/app/schemas/history.py +++ b/app/schemas/history.py @@ -4,6 +4,10 @@ from pydantic import BaseModel, ConfigDict, Field class DownloadHistory(BaseModel): + """ + 下载历史记录 + """ + # ID id: int # 保存路程 @@ -34,8 +38,10 @@ class DownloadHistory(BaseModel): seasons: Optional[str] = None # 集Exx episodes: Optional[str] = None - # 海报 + # 背景图 image: Optional[str] = None + # 海报 + poster: Optional[str] = None # 下载器Hash download_hash: Optional[str] = None # 种子名称 diff --git a/database/versions/a8c4e2f6b1d9_2_2_15.py b/database/versions/a8c4e2f6b1d9_2_2_15.py new file mode 100644 index 000000000..34147e102 --- /dev/null +++ b/database/versions/a8c4e2f6b1d9_2_2_15.py @@ -0,0 +1,42 @@ +"""2.2.15 +为下载历史增加海报字段 + +Revision ID: a8c4e2f6b1d9 +Revises: f7b2d5c9a301 +Create Date: 2026-08-03 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = "a8c4e2f6b1d9" +down_revision = "f7b2d5c9a301" +branch_labels = None +depends_on = None + + +def _has_column(table_name: str, column_name: str) -> bool: + """检查数据表是否已存在指定字段。""" + inspector = sa.inspect(op.get_bind()) + if table_name not in inspector.get_table_names(): + return False + return any( + column["name"] == column_name + for column in inspector.get_columns(table_name) + ) + + +def upgrade() -> None: + """为下载历史增加可空海报字段。""" + if not _has_column("downloadhistory", "poster"): + op.add_column( + "downloadhistory", + sa.Column("poster", sa.String(), nullable=True), + ) + + +def downgrade() -> None: + """移除下载历史海报字段。""" + if _has_column("downloadhistory", "poster"): + op.drop_column("downloadhistory", "poster") diff --git a/docs/mcp-api.md b/docs/mcp-api.md index 104459706..c1e2fdf9c 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -170,7 +170,7 @@ AniList 榜单、探索、详情、人物和推荐接口优先通过 `anilist-ch | 方法 | 路径 | 说明 | | :--- | :--- | :--- | -| GET | `/api/v1/download/` | 查询正在下载的任务,参数:`name`;关联下载历史时返回媒体类型及来源站点 `site_name` | +| GET | `/api/v1/download/` | 查询正在下载的任务,参数:`name`;关联下载历史时返回媒体类型、来源站点 `site_name`,以及 `media.poster` 海报和 `media.backdrop` 背景图;兼容字段 `media.image` 与 `media.poster` 相同 | | POST | `/api/v1/download/` | 添加含媒体信息的下载任务,请求体包含媒体信息和种子信息 | | POST | `/api/v1/download/add` | 添加不含媒体信息的下载任务,请求体包含 `torrent_in`,可选 `media_source` + `media_id`;继续兼容四种专用 ID,并支持 `downloader`、`save_path` | | POST | `/api/v1/download/subtitle` | 下载字幕到识别出的媒体下载目录,请求体包含 `subtitle_in`,可选 `media_source` + `media_id`;继续兼容四种专用 ID,并支持 `save_path` | @@ -180,6 +180,13 @@ AniList 榜单、探索、详情、人物和推荐接口优先通过 `anilist-ch | GET | `/api/v1/download/paths` | 查询可用于下载接口 `save_path` 参数的下载路径 | | DELETE | `/api/v1/download/{hashString}` | 删除下载任务,参数:`name` | +#### 历史 + +| 方法 | 路径 | 说明 | +| :--- | :--- | :--- | +| GET | `/api/v1/history/download` | 按下载时间倒序查询下载历史,参数:`page`、`count`;`poster` 为海报,兼容字段 `image` 为背景图 | +| DELETE | `/api/v1/history/download` | 删除下载历史,请求体为下载历史记录 | + #### 系统 | 方法 | 路径 | 说明 | diff --git a/skills/moviepilot-api/SKILL.md b/skills/moviepilot-api/SKILL.md index 286e78b24..a7dd72f14 100644 --- a/skills/moviepilot-api/SKILL.md +++ b/skills/moviepilot-api/SKILL.md @@ -1,6 +1,6 @@ --- name: moviepilot-api -version: 6 +version: 7 description: >- Use this skill when you need to call MoviePilot REST API endpoints directly with the bundled Python client. Covers MoviePilot HTTP endpoints across media @@ -251,7 +251,7 @@ Streaming search sends `{"type":"heartbeat"}` every 15 seconds without business | Method | Path | Description | |--------|------|-------------| -| GET | `/api/v1/history/download` | Download history, newest first. Params: `page`, `count` | +| GET | `/api/v1/history/download` | Download history, newest first. Params: `page`, `count`. `poster` is the poster image; legacy `image` is the backdrop image. | | DELETE | `/api/v1/history/download` | Delete download history. Body: DownloadHistory JSON | | GET | `/api/v1/history/transfer` | Transfer history. Params: `title`, `page`, `count`, `status` | | DELETE | `/api/v1/history/transfer` | Delete transfer history. Params: `deletesrc`, `deletedest`. Body: TransferHistory | diff --git a/tests/test_builtin_skill_boundaries.py b/tests/test_builtin_skill_boundaries.py index 3a68fc134..8adab2cd2 100644 --- a/tests/test_builtin_skill_boundaries.py +++ b/tests/test_builtin_skill_boundaries.py @@ -23,7 +23,7 @@ def test_modified_builtin_skills_have_incremented_versions() -> None: """本次修改过的内置技能必须递增版本,确保用户端同步更新。""" expected_versions = { "database-operation": "3", - "moviepilot-api": "6", + "moviepilot-api": "7", "moviepilot-cli": "6", "moviepilot-update": "3", "transfer-failed-retry": "2", diff --git a/tests/test_download_chain.py b/tests/test_download_chain.py index b13f2f618..817c5863d 100644 --- a/tests/test_download_chain.py +++ b/tests/test_download_chain.py @@ -249,9 +249,11 @@ def test_download_single_persists_custom_words_snapshot(monkeypatch): """捕获写入下载历史的字段,验证识别词快照确实落库。""" def add(self, **kwargs): + """捕获下载历史字段。""" captured.update(kwargs) def add_files(self, _files): + """忽略与当前断言无关的下载文件记录。""" pass _FakeThreadHelper.submitted = [] @@ -278,6 +280,8 @@ def test_download_single_persists_custom_words_snapshot(monkeypatch): year="2024", tmdb_id=1, genre_ids=[18], + poster_path="https://images.example.com/original/poster.jpg", + backdrop_path="https://images.example.com/original/backdrop.jpg", ), torrent_info=TorrentInfo( title="Demo Show 2024", @@ -298,6 +302,8 @@ def test_download_single_persists_custom_words_snapshot(monkeypatch): assert result == "hash123" assert captured["custom_words"] == custom_words + assert captured["poster"] == "https://images.example.com/w500/poster.jpg" + assert captured["image"] == "https://images.example.com/w500/backdrop.jpg" def test_save_subtitle_response_creates_missing_temp_directory(monkeypatch, tmp_path): @@ -1045,7 +1051,8 @@ def test_downloading_includes_media_type_and_source_site(monkeypatch): torrent = DownloaderTorrent(hash="download-hash", title="Demo.Release") history = SimpleNamespace( episodes="E02", - image="https://images.example.com/demo.jpg", + image="https://images.example.com/backdrop.jpg", + poster="https://images.example.com/poster.jpg", seasons="S01", title="示例剧集", tmdbid=1001, @@ -1066,6 +1073,9 @@ def test_downloading_includes_media_type_and_source_site(monkeypatch): assert result == [torrent] assert torrent.media["type"] == "电视剧" + assert torrent.media["image"] == "https://images.example.com/poster.jpg" + assert torrent.media["poster"] == "https://images.example.com/poster.jpg" + assert torrent.media["backdrop"] == "https://images.example.com/backdrop.jpg" assert torrent.site_name == "示例站点" assert torrent.userid == "user-1" assert torrent.username == "tester" diff --git a/tests/test_downloadhistory_poster_migration.py b/tests/test_downloadhistory_poster_migration.py new file mode 100644 index 000000000..6a217241a --- /dev/null +++ b/tests/test_downloadhistory_poster_migration.py @@ -0,0 +1,50 @@ +import importlib + +import sqlalchemy as sa +from alembic.migration import MigrationContext +from alembic.operations import Operations + + +def test_downloadhistory_poster_migration_adds_and_removes_column(monkeypatch) -> None: + """迁移应为下载历史增加可空海报字段,并支持回滚。""" + migration = importlib.import_module( + "database.versions.a8c4e2f6b1d9_2_2_15" + ) + engine = sa.create_engine("sqlite://") + metadata = sa.MetaData() + download_history = sa.Table( + "downloadhistory", + metadata, + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("image", sa.String()), + ) + + with engine.begin() as connection: + metadata.create_all(connection) + connection.execute(download_history.insert(), { + "id": 1, + "image": "https://images.example.com/backdrop.jpg", + }) + context = MigrationContext.configure(connection) + monkeypatch.setattr(migration, "op", Operations(context)) + + migration.upgrade() + migration.upgrade() + + migrated = sa.Table( + "downloadhistory", + sa.MetaData(), + autoload_with=connection, + ) + row = connection.execute(sa.select(migrated)).mappings().one() + assert "poster" in migrated.c + assert row["image"] == "https://images.example.com/backdrop.jpg" + assert row["poster"] is None + + migration.downgrade() + rolled_back = sa.Table( + "downloadhistory", + sa.MetaData(), + autoload_with=connection, + ) + assert "poster" not in rolled_back.c