feat(download): persist poster and backdrop images (#6232)

This commit is contained in:
jxxghp
2026-08-03 21:38:26 +08:00
committed by GitHub
parent 36dd381260
commit a700ba9379
9 changed files with 128 additions and 8 deletions
+4 -1
View File
@@ -951,6 +951,7 @@ class DownloadChain(ChainBase):
seasons=_meta.season, seasons=_meta.season,
episodes=download_episodes or _meta.episode, episodes=download_episodes or _meta.episode,
image=_media.get_backdrop_image(), image=_media.get_backdrop_image(),
poster=_media.get_poster_image(),
downloader=_downloader, downloader=_downloader,
download_hash=_hash, download_hash=_hash,
torrent_name=_torrent.title, torrent_name=_torrent.title,
@@ -1777,7 +1778,9 @@ class DownloadChain(ChainBase):
"title": history.title, "title": history.title,
"season": history.seasons, "season": history.seasons,
"episode": history.episodes, "episode": history.episodes,
"image": history.image, "image": history.poster,
"poster": history.poster,
"backdrop": history.image,
} }
torrent.site_name = history.torrent_site torrent.site_name = history.torrent_site
# 下载用户 # 下载用户
+3 -1
View File
@@ -39,8 +39,10 @@ class DownloadHistory(Base):
seasons = Column(String) seasons = Column(String)
# Exx # Exx
episodes = Column(String) episodes = Column(String)
# 海报 # 背景图
image = Column(String) image = Column(String)
# 海报
poster = Column(String)
# 下载器 # 下载器
downloader = Column(String) downloader = Column(String)
# 下载任务Hash # 下载任务Hash
+7 -1
View File
@@ -4,6 +4,10 @@ from pydantic import BaseModel, ConfigDict, Field
class DownloadHistory(BaseModel): class DownloadHistory(BaseModel):
"""
下载历史记录
"""
# ID # ID
id: int id: int
# 保存路程 # 保存路程
@@ -34,8 +38,10 @@ class DownloadHistory(BaseModel):
seasons: Optional[str] = None seasons: Optional[str] = None
# 集Exx # 集Exx
episodes: Optional[str] = None episodes: Optional[str] = None
# 海报 # 背景图
image: Optional[str] = None image: Optional[str] = None
# 海报
poster: Optional[str] = None
# 下载器Hash # 下载器Hash
download_hash: Optional[str] = None download_hash: Optional[str] = None
# 种子名称 # 种子名称
+42
View File
@@ -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")
+8 -1
View File
@@ -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/` | 添加含媒体信息的下载任务,请求体包含媒体信息和种子信息 |
| POST | `/api/v1/download/add` | 添加不含媒体信息的下载任务,请求体包含 `torrent_in`,可选 `media_source` + `media_id`;继续兼容四种专用 ID,并支持 `downloader``save_path` | | 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` | | 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` 参数的下载路径 | | GET | `/api/v1/download/paths` | 查询可用于下载接口 `save_path` 参数的下载路径 |
| DELETE | `/api/v1/download/{hashString}` | 删除下载任务,参数:`name` | | DELETE | `/api/v1/download/{hashString}` | 删除下载任务,参数:`name` |
#### 历史
| 方法 | 路径 | 说明 |
| :--- | :--- | :--- |
| GET | `/api/v1/history/download` | 按下载时间倒序查询下载历史,参数:`page``count``poster` 为海报,兼容字段 `image` 为背景图 |
| DELETE | `/api/v1/history/download` | 删除下载历史,请求体为下载历史记录 |
#### 系统 #### 系统
| 方法 | 路径 | 说明 | | 方法 | 路径 | 说明 |
+2 -2
View File
@@ -1,6 +1,6 @@
--- ---
name: moviepilot-api name: moviepilot-api
version: 6 version: 7
description: >- description: >-
Use this skill when you need to call MoviePilot REST API endpoints directly Use this skill when you need to call MoviePilot REST API endpoints directly
with the bundled Python client. Covers MoviePilot HTTP endpoints across media 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 | | 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 | | DELETE | `/api/v1/history/download` | Delete download history. Body: DownloadHistory JSON |
| GET | `/api/v1/history/transfer` | Transfer history. Params: `title`, `page`, `count`, `status` | | 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 | | DELETE | `/api/v1/history/transfer` | Delete transfer history. Params: `deletesrc`, `deletedest`. Body: TransferHistory |
+1 -1
View File
@@ -23,7 +23,7 @@ def test_modified_builtin_skills_have_incremented_versions() -> None:
"""本次修改过的内置技能必须递增版本,确保用户端同步更新。""" """本次修改过的内置技能必须递增版本,确保用户端同步更新。"""
expected_versions = { expected_versions = {
"database-operation": "3", "database-operation": "3",
"moviepilot-api": "6", "moviepilot-api": "7",
"moviepilot-cli": "6", "moviepilot-cli": "6",
"moviepilot-update": "3", "moviepilot-update": "3",
"transfer-failed-retry": "2", "transfer-failed-retry": "2",
+11 -1
View File
@@ -249,9 +249,11 @@ def test_download_single_persists_custom_words_snapshot(monkeypatch):
"""捕获写入下载历史的字段,验证识别词快照确实落库。""" """捕获写入下载历史的字段,验证识别词快照确实落库。"""
def add(self, **kwargs): def add(self, **kwargs):
"""捕获下载历史字段。"""
captured.update(kwargs) captured.update(kwargs)
def add_files(self, _files): def add_files(self, _files):
"""忽略与当前断言无关的下载文件记录。"""
pass pass
_FakeThreadHelper.submitted = [] _FakeThreadHelper.submitted = []
@@ -278,6 +280,8 @@ def test_download_single_persists_custom_words_snapshot(monkeypatch):
year="2024", year="2024",
tmdb_id=1, tmdb_id=1,
genre_ids=[18], genre_ids=[18],
poster_path="https://images.example.com/original/poster.jpg",
backdrop_path="https://images.example.com/original/backdrop.jpg",
), ),
torrent_info=TorrentInfo( torrent_info=TorrentInfo(
title="Demo Show 2024", title="Demo Show 2024",
@@ -298,6 +302,8 @@ def test_download_single_persists_custom_words_snapshot(monkeypatch):
assert result == "hash123" assert result == "hash123"
assert captured["custom_words"] == custom_words 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): 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") torrent = DownloaderTorrent(hash="download-hash", title="Demo.Release")
history = SimpleNamespace( history = SimpleNamespace(
episodes="E02", 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", seasons="S01",
title="示例剧集", title="示例剧集",
tmdbid=1001, tmdbid=1001,
@@ -1066,6 +1073,9 @@ def test_downloading_includes_media_type_and_source_site(monkeypatch):
assert result == [torrent] assert result == [torrent]
assert torrent.media["type"] == "电视剧" 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.site_name == "示例站点"
assert torrent.userid == "user-1" assert torrent.userid == "user-1"
assert torrent.username == "tester" assert torrent.username == "tester"
@@ -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