feat: expand metadata sources and media server sync (#6129)

This commit is contained in:
jxxghp
2026-07-21 11:34:18 +08:00
parent 1d708870c9
commit e3f80af74f
43 changed files with 2581 additions and 152 deletions

View File

@@ -1,4 +1,4 @@
from typing import Any, List, Annotated, Optional
from typing import Any, List, Annotated, Literal, Optional
from fastapi import APIRouter, Depends, Body
@@ -17,6 +17,7 @@ from app.schemas.types import SystemConfigKey
from app.utils.security import SecurityUtils
router = APIRouter()
MediaSource = Literal["themoviedb", "douban", "bangumi", "anilist"]
def _prepare_subtitle_download(subtitle: SubtitleInfo) -> tuple[bool, str]:
@@ -97,6 +98,8 @@ def add(
torrent_in: schemas.TorrentInfo,
tmdbid: Annotated[int | None, Body()] = None,
doubanid: Annotated[str | None, Body()] = None,
media_source: Annotated[MediaSource | None, Body()] = None,
media_id: Annotated[str | None, Body()] = None,
downloader: Annotated[str | None, Body()] = None,
# 保存路径, 支持<storage>:<path>, 如rclone:/MP, smb:/server/share/Movies等
save_path: Annotated[str | None, Body()] = None,
@@ -108,15 +111,18 @@ def add(
# 元数据
metainfo = MetaInfo(title=torrent_in.title, subtitle=torrent_in.description)
# 媒体信息
if tmdbid or doubanid:
if tmdbid or doubanid or media_id:
mediainfo = MediaChain().recognize_media(
meta=metainfo,
source=media_source,
mediaid=media_id,
tmdbid=tmdbid,
doubanid=doubanid,
)
else:
mediainfo = MediaChain().recognize_by_meta(
metainfo,
source=media_source,
obtain_images=False,
)
if not mediainfo:
@@ -146,6 +152,8 @@ def download_subtitle(
subtitle_in: schemas.SubtitleInfo,
tmdbid: Annotated[int | None, Body()] = None,
doubanid: Annotated[str | None, Body()] = None,
media_source: Annotated[MediaSource | None, Body()] = None,
media_id: Annotated[str | None, Body()] = None,
save_path: Annotated[str | None, Body()] = None,
current_user: User = Depends(get_current_active_user),
) -> Any:
@@ -160,6 +168,8 @@ def download_subtitle(
success, message, saved_files = DownloadChain().download_subtitle(
subtitle=subtitle_info,
media_source=media_source,
media_id=media_id,
tmdbid=tmdbid,
doubanid=doubanid,
save_path=save_path,

View File

@@ -1,5 +1,5 @@
from pathlib import Path
from typing import List, Any, Union, Annotated, Optional
from typing import Annotated, Any, List, Literal, Optional, Union
from fastapi import APIRouter, Depends
@@ -18,6 +18,7 @@ from app.schemas.category import CategoryConfig
from app.schemas.types import ChainEventType
router = APIRouter()
MediaSource = Literal["themoviedb", "douban", "bangumi", "anilist"]
@router.get(
@@ -27,17 +28,22 @@ async def recognize(
title: str,
subtitle: Optional[str] = None,
custom_words: Optional[str] = None,
source: Optional[MediaSource] = None,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
根据标题、副标题识别媒体信息
:param custom_words: 临时识别词(每行一条规则),传入时仅在本次识别中生效,不会保存到系统配置
:param source: 请求级识别数据源
"""
# 识别媒体信息,传入临时识别词时优先于系统配置的识别词生效
metainfo = MetaInfo(
title, subtitle, custom_words=custom_words.split("\n") if custom_words else None
)
mediainfo = await MediaChain().async_recognize_by_meta(metainfo)
mediainfo = await MediaChain().async_recognize_by_meta(
metainfo,
source=source,
)
if mediainfo:
return Context(meta_info=metainfo, media_info=mediainfo).to_dict()
return schemas.Context()
@@ -53,25 +59,28 @@ async def recognize2(
title: str,
subtitle: Optional[str] = None,
custom_words: Optional[str] = None,
source: Optional[MediaSource] = None,
) -> Any:
"""
根据标题、副标题识别媒体信息 API_TOKEN认证?token=xxx
"""
# 识别媒体信息
return await recognize(title, subtitle, custom_words)
return await recognize(title, subtitle, custom_words, source)
@router.get(
"/recognize_file", summary="识别媒体信息(文件)", response_model=schemas.Context
)
async def recognize_file(
path: str, _: schemas.TokenPayload = Depends(verify_token)
path: str,
source: Optional[MediaSource] = None,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
根据文件路径识别媒体信息
"""
# 识别媒体信息
context = await MediaChain().async_recognize_by_path(path)
context = await MediaChain().async_recognize_by_path(path, source=source)
if context:
return context.to_dict()
return schemas.Context()
@@ -83,13 +92,15 @@ async def recognize_file(
response_model=schemas.Context,
)
async def recognize_file2(
path: str, _: Annotated[str, Depends(verify_apitoken)]
path: str,
_: Annotated[str, Depends(verify_apitoken)],
source: Optional[MediaSource] = None,
) -> Any:
"""
根据文件路径识别媒体信息 API_TOKEN认证?token=xxx
"""
# 识别媒体信息
return await recognize_file(path)
return await recognize_file(path, source)
@router.get("/search", summary="搜索媒体/人物信息", response_model=List[dict])
@@ -98,6 +109,7 @@ async def search(
type: Optional[str] = "media",
page: int = 1,
count: int = 8,
source: Optional[MediaSource] = None,
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
@@ -114,7 +126,7 @@ async def search(
media_chain = MediaChain()
if type == "media":
_, medias = await media_chain.async_search(title=title)
_, medias = await media_chain.async_search(title=title, source=source)
result = [media.to_dict() for media in medias] if medias else []
elif type == "collection":
collections = await media_chain.async_search_collections(name=title)
@@ -294,7 +306,7 @@ async def detail(
_: schemas.TokenPayload = Depends(verify_token),
) -> Any:
"""
根据媒体ID查询themoviedb或豆瓣媒体信息type_name: 电影/电视剧
根据带来源前缀的媒体ID查询媒体信息type_name: 电影/电视剧
"""
mtype = MediaType(type_name)
mediainfo = None
@@ -311,6 +323,10 @@ async def detail(
mediainfo = await mediachain.async_recognize_media(
bangumiid=int(mediaid[8:]), mtype=mtype
)
elif mediaid.startswith("anilist:"):
mediainfo = await mediachain.async_recognize_media(
anilistid=int(mediaid[8:]), mtype=mtype
)
else:
# 广播事件解析媒体信息
event_data = MediaRecognizeConvertEventData(

View File

@@ -292,6 +292,14 @@ def manual_transfer(
transer_item.doubanid = (
str(history.doubanid) if history.doubanid else transer_item.doubanid
)
transer_item.media_source = (
getattr(history, "media_source", None)
or transer_item.media_source
)
transer_item.media_id = (
getattr(history, "media_id", None)
or transer_item.media_id
)
transer_item.season = (
int(str(history.seasons).replace("S", ""))
if history.seasons
@@ -409,6 +417,8 @@ def manual_transfer(
target_path=target_path,
tmdbid=transer_item.tmdbid,
doubanid=transer_item.doubanid,
media_source=transer_item.media_source,
media_id=transer_item.media_id,
mtype=mtype,
season=transer_item.season,
episode_group=transer_item.episode_group,
@@ -491,6 +501,8 @@ def manual_transfer(
target_path=target_path,
tmdbid=transer_item.tmdbid,
doubanid=transer_item.doubanid,
media_source=transer_item.media_source,
media_id=transer_item.media_id,
mtype=mtype,
season=transer_item.season,
episode_group=transer_item.episode_group,