mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 00:46:57 +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,
|
||||
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
|
||||
# 下载用户
|
||||
|
||||
@@ -39,8 +39,10 @@ class DownloadHistory(Base):
|
||||
seasons = Column(String)
|
||||
# Exx
|
||||
episodes = Column(String)
|
||||
# 海报
|
||||
# 背景图
|
||||
image = Column(String)
|
||||
# 海报
|
||||
poster = Column(String)
|
||||
# 下载器
|
||||
downloader = Column(String)
|
||||
# 下载任务Hash
|
||||
|
||||
@@ -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
|
||||
# 种子名称
|
||||
|
||||
@@ -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/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` | 删除下载历史,请求体为下载历史记录 |
|
||||
|
||||
#### 系统
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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