mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-21 16:23:34 +08:00
feat(v3): add music automation workflow
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
from typing import Any, List, Annotated, Literal, Optional
|
||||
from typing import Any, List, Annotated, Literal, Optional, Union
|
||||
|
||||
from fastapi import APIRouter, Depends, Body
|
||||
|
||||
from app import schemas
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.music import MusicChain
|
||||
from app.core.context import MediaInfo, Context, SubtitleInfo, TorrentInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.core.music import MusicInfo
|
||||
from app.core.security import verify_token
|
||||
from app.db.models.user import User
|
||||
from app.db.site_oper import SiteOper
|
||||
@@ -17,7 +19,7 @@ from app.schemas.types import SystemConfigKey
|
||||
from app.utils.security import SecurityUtils
|
||||
|
||||
router = APIRouter()
|
||||
MediaSource = Literal["themoviedb", "douban", "bangumi", "anilist"]
|
||||
MediaSource = Literal["themoviedb", "douban", "bangumi", "anilist", "musicbrainz"]
|
||||
|
||||
|
||||
def _prepare_subtitle_download(subtitle: SubtitleInfo) -> tuple[bool, str]:
|
||||
@@ -57,7 +59,7 @@ def current(
|
||||
|
||||
@router.post("/", summary="添加下载(含媒体信息)", response_model=schemas.Response)
|
||||
def download(
|
||||
media_in: schemas.MediaInfo,
|
||||
media_in: Union[schemas.MusicInfo, schemas.MediaInfo],
|
||||
torrent_in: schemas.TorrentInfo,
|
||||
downloader: Annotated[str | None, Body()] = None,
|
||||
save_path: Annotated[str | None, Body()] = None,
|
||||
@@ -66,11 +68,14 @@ def download(
|
||||
"""
|
||||
添加下载任务(含媒体信息)
|
||||
"""
|
||||
# 元数据
|
||||
metainfo = MetaInfo(title=torrent_in.title, subtitle=torrent_in.description)
|
||||
# 媒体信息
|
||||
mediainfo = MediaInfo()
|
||||
mediainfo.from_dict(media_in.model_dump())
|
||||
if isinstance(media_in, schemas.MusicInfo):
|
||||
mediainfo = MusicInfo.from_dict(media_in.model_dump())
|
||||
metainfo = MusicChain.to_meta(mediainfo)
|
||||
metainfo.org_string = torrent_in.title
|
||||
else:
|
||||
metainfo = MetaInfo(title=torrent_in.title, subtitle=torrent_in.description)
|
||||
mediainfo = MediaInfo()
|
||||
mediainfo.from_dict(media_in.model_dump())
|
||||
# 种子信息
|
||||
torrentinfo = TorrentInfo()
|
||||
torrentinfo.from_dict(torrent_in.model_dump())
|
||||
|
||||
70
app/api/endpoints/music.py
Normal file
70
app/api/endpoints/music.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from app import schemas
|
||||
from app.chain.music import MusicChain
|
||||
from app.core.music import MusicInfo
|
||||
from app.core.security import verify_token
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
CountParam = Annotated[int, Query(ge=1, le=100)]
|
||||
|
||||
|
||||
def _serialize_music(info: MusicInfo) -> schemas.MusicInfo:
|
||||
"""将内部音乐信息转换为 REST 响应模型。"""
|
||||
return schemas.MusicInfo(**info.to_dict())
|
||||
|
||||
|
||||
@router.get(
|
||||
"/search",
|
||||
summary="搜索音乐元数据",
|
||||
response_model=list[schemas.MusicInfo],
|
||||
)
|
||||
async def search_music(
|
||||
query: str = Query(min_length=1),
|
||||
count: CountParam = 20,
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
) -> list[schemas.MusicInfo]:
|
||||
"""按歌曲、专辑或艺术家关键词搜索标准音乐候选。"""
|
||||
results = await MusicChain().async_search(query=query, limit=count)
|
||||
return [_serialize_music(info) for info in results]
|
||||
|
||||
|
||||
@router.post(
|
||||
"/recognize",
|
||||
summary="识别音乐元数据详情",
|
||||
response_model=schemas.MusicInfo,
|
||||
)
|
||||
async def recognize_music(
|
||||
request: schemas.MusicRecognizeRequest,
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
) -> schemas.MusicInfo:
|
||||
"""根据音乐元数据来源和媒体 ID 获取标准详情。"""
|
||||
info = await MusicChain().async_recognize(
|
||||
source=request.source,
|
||||
media_id=request.media_id,
|
||||
)
|
||||
if not info:
|
||||
raise HTTPException(status_code=404, detail="未识别到音乐信息")
|
||||
return _serialize_music(info)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/explore",
|
||||
summary="探索热门音乐",
|
||||
response_model=list[schemas.MusicInfo],
|
||||
)
|
||||
async def explore_music(
|
||||
page: Annotated[int, Query(ge=1)] = 1,
|
||||
count: CountParam = 30,
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
) -> list[schemas.MusicInfo]:
|
||||
"""按月度全站收听榜单分页返回可搜索和订阅的音乐候选。"""
|
||||
results = await MusicChain().async_chart(
|
||||
range_name="this_month",
|
||||
page=page,
|
||||
count=count,
|
||||
)
|
||||
return [_serialize_music(info) for info in results]
|
||||
@@ -60,6 +60,20 @@ async def bangumi_calendar(
|
||||
return await RecommendChain().async_bangumi_calendar(page=page, count=count)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/music_weekly",
|
||||
summary="ListenBrainz 本周热门音乐",
|
||||
response_model=List[schemas.MusicInfo],
|
||||
)
|
||||
async def music_weekly(
|
||||
page: Optional[int] = 1,
|
||||
count: Optional[int] = 30,
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
) -> Any:
|
||||
"""浏览本周全站热门音乐。"""
|
||||
return await RecommendChain().async_music_weekly(page=page, count=count)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/douban_showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo]
|
||||
)
|
||||
|
||||
@@ -475,6 +475,7 @@ async def search_by_id(
|
||||
async def search_by_title_stream(
|
||||
request: Request,
|
||||
keyword: Optional[str] = None,
|
||||
mtype: Optional[str] = None,
|
||||
page: Optional[int] = 0,
|
||||
sites: Optional[str] = None,
|
||||
_: schemas.TokenPayload = Depends(verify_resource_token),
|
||||
@@ -484,7 +485,11 @@ async def search_by_title_stream(
|
||||
"""
|
||||
|
||||
event_source = SearchChain().async_search_by_title_stream(
|
||||
title=keyword, page=page, sites=_parse_site_list(sites), cache_local=True
|
||||
title=keyword,
|
||||
page=page,
|
||||
sites=_parse_site_list(sites),
|
||||
cache_local=True,
|
||||
mtype=_parse_media_type(mtype),
|
||||
)
|
||||
return StreamingResponse(
|
||||
_stream_search_events(request, event_source),
|
||||
@@ -496,6 +501,7 @@ async def search_by_title_stream(
|
||||
@router.get("/title", summary="模糊搜索资源", response_model=schemas.Response)
|
||||
async def search_by_title(
|
||||
keyword: Optional[str] = None,
|
||||
mtype: Optional[str] = None,
|
||||
page: Optional[int] = 0,
|
||||
sites: Optional[str] = None,
|
||||
_: schemas.TokenPayload = Depends(verify_token),
|
||||
@@ -504,7 +510,11 @@ async def search_by_title(
|
||||
根据名称模糊搜索站点资源,支持分页,关键词为空是返回首页资源
|
||||
"""
|
||||
torrents = await SearchChain().async_search_by_title(
|
||||
title=keyword, page=page, sites=_parse_site_list(sites), cache_local=True
|
||||
title=keyword,
|
||||
page=page,
|
||||
sites=_parse_site_list(sites),
|
||||
cache_local=True,
|
||||
mtype=_parse_media_type(mtype),
|
||||
)
|
||||
if not torrents:
|
||||
return schemas.Response(success=False, message="未搜索到任何资源")
|
||||
|
||||
@@ -30,7 +30,7 @@ from app.db.user_oper import (
|
||||
from app.helper.sites import SitesHelper # noqa
|
||||
from app.log import logger
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import SystemConfigKey, EventType
|
||||
from app.schemas.types import SystemConfigKey, EventType, MediaType
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
router = APIRouter()
|
||||
@@ -394,6 +394,7 @@ async def site_category(
|
||||
async def site_resource(
|
||||
site_id: int,
|
||||
keyword: Optional[str] = None,
|
||||
mtype: Optional[str] = None,
|
||||
cat: Optional[str] = None,
|
||||
page: Optional[int] = 0,
|
||||
db: AsyncSession = Depends(get_async_db),
|
||||
@@ -409,7 +410,11 @@ async def site_resource(
|
||||
detail=f"站点 {site_id} 不存在",
|
||||
)
|
||||
torrents = await TorrentsChain().async_browse(
|
||||
domain=site.domain, keyword=keyword, cat=cat, page=page
|
||||
domain=site.domain,
|
||||
keyword=keyword,
|
||||
cat=cat,
|
||||
page=page,
|
||||
mtype=MediaType.from_agent(mtype) or MediaType(mtype) if mtype else None,
|
||||
)
|
||||
if not torrents:
|
||||
return []
|
||||
|
||||
@@ -173,10 +173,13 @@ async def create_subscribe(
|
||||
mtype = None
|
||||
# 非 TMDB 来源的标题可能自带季标记,入库前统一拆分。
|
||||
if (
|
||||
subscribe_in.doubanid
|
||||
or subscribe_in.bangumiid
|
||||
or subscribe_in.anilistid
|
||||
or normalize_media_source(subscribe_in.media_source) not in (None, "themoviedb")
|
||||
mtype != MediaType.MUSIC
|
||||
and (
|
||||
subscribe_in.doubanid
|
||||
or subscribe_in.bangumiid
|
||||
or subscribe_in.anilistid
|
||||
or normalize_media_source(subscribe_in.media_source) not in (None, "themoviedb")
|
||||
)
|
||||
):
|
||||
meta = MetaInfo(subscribe_in.name)
|
||||
subscribe_in.name = meta.name
|
||||
|
||||
Reference in New Issue
Block a user