mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
feat(download): persist poster and backdrop images (#6232)
This commit is contained in:
@@ -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
|
||||||
# 下载用户
|
# 下载用户
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
# 种子名称
|
# 种子名称
|
||||||
|
|||||||
@@ -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
@@ -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` | 删除下载历史,请求体为下载历史记录 |
|
||||||
|
|
||||||
#### 系统
|
#### 系统
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user