This commit is contained in:
jxxghp 2023-06-17 17:34:18 +08:00
parent f85e960fa9
commit acdec220f7
42 changed files with 423 additions and 253 deletions

View File

@ -1,6 +1,6 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.endpoints import login, user, site, message, webhook, subscribe, media, douban from app.api.endpoints import login, user, site, message, webhook, subscribe, media, douban, search
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(login.router, tags=["login"]) api_router.include_router(login.router, tags=["login"])
@ -10,4 +10,5 @@ api_router.include_router(message.router, prefix="/message", tags=["message"])
api_router.include_router(webhook.router, prefix="/webhook", tags=["webhook"]) api_router.include_router(webhook.router, prefix="/webhook", tags=["webhook"])
api_router.include_router(subscribe.router, prefix="/subscribe", tags=["subscribe"]) api_router.include_router(subscribe.router, prefix="/subscribe", tags=["subscribe"])
api_router.include_router(media.router, prefix="/media", tags=["media"]) api_router.include_router(media.router, prefix="/media", tags=["media"])
api_router.include_router(search.router, prefix="/search", tags=["search"])
api_router.include_router(douban.router, prefix="/douban", tags=["douban"]) api_router.include_router(douban.router, prefix="/douban", tags=["douban"])

View File

@ -1,7 +1,9 @@
from typing import Any
from fastapi import APIRouter, Depends, BackgroundTasks from fastapi import APIRouter, Depends, BackgroundTasks
from app import schemas from app import schemas
from app.chain.douban_sync import DoubanSyncChain from app.chain.douban import DoubanChain
from app.db.models.user import User from app.db.models.user import User
from app.db.userauth import get_current_active_superuser from app.db.userauth import get_current_active_superuser
@ -12,15 +14,15 @@ def start_douban_chain():
""" """
启动链式任务 启动链式任务
""" """
DoubanSyncChain().process() DoubanChain().sync()
@router.get("/sync", response_model=schemas.Response) @router.get("/sync", response_model=schemas.Response)
async def sync_douban( async def sync_douban(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
_: User = Depends(get_current_active_superuser)): _: User = Depends(get_current_active_superuser)) -> Any:
""" """
查询所有订阅 同步豆瓣想看
""" """
background_tasks.add_task(start_douban_chain) background_tasks.add_task(start_douban_chain)
return {"success": True} return {"success": True}

View File

@ -1,20 +1,59 @@
from typing import List, Any
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from app import schemas from app import schemas
from app.chain.identify import IdentifyChain from app.chain.media import MediaChain
from app.core.context import MediaInfo
from app.db.models.user import User from app.db.models.user import User
from app.db.userauth import get_current_active_user from app.db.userauth import get_current_active_user
from app.schemas.types import MediaType
router = APIRouter() router = APIRouter()
@router.post("/recognize", response_model=schemas.Context) @router.get("/recognize", response_model=schemas.Context)
async def recognize(title: str, async def recognize(title: str,
subtitle: str = None, subtitle: str = None,
_: User = Depends(get_current_active_user)): _: User = Depends(get_current_active_user)) -> Any:
""" """
识别媒体信息 识别媒体信息
""" """
# 识别媒体信息 # 识别媒体信息
context = IdentifyChain().process(title=title, subtitle=subtitle) context = MediaChain().recognize_by_title(title=title, subtitle=subtitle)
return context.to_dict() return context.to_dict()
@router.get("/tmdb", response_model=schemas.MediaInfo)
async def tmdb_info(tmdbid: int, type_name: str) -> Any:
"""
根据TMDBID查询媒体信息
"""
mtype = MediaType.MOVIE if type_name == MediaType.MOVIE.value else MediaType.TV
media = MediaChain().recognize_media(tmdbid=tmdbid, mtype=mtype)
if media:
return media.to_dict()
else:
return schemas.MediaInfo()
@router.get("/douban", response_model=schemas.MediaInfo)
async def douban_info(doubanid: str) -> Any:
"""
根据豆瓣ID查询豆瓣媒体信息
"""
doubaninfo = MediaChain().douban_info(doubanid=doubanid)
if doubaninfo:
return MediaInfo(douban_info=doubaninfo).to_dict()
else:
return schemas.MediaInfo()
@router.get("/search", response_model=List[schemas.MediaInfo])
async def search_by_title(title: str,
_: User = Depends(get_current_active_user)) -> Any:
"""
搜索媒体信息
"""
_, medias = MediaChain().search(title=title)
return [media.to_dict() for media in medias]

View File

@ -5,7 +5,7 @@ from fastapi import Request
from starlette.responses import PlainTextResponse from starlette.responses import PlainTextResponse
from app import schemas from app import schemas
from app.chain.user_message import UserMessageChain from app.chain.message import MessageChain
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt
@ -17,7 +17,7 @@ def start_message_chain(body: Any, form: Any, args: Any):
""" """
启动链式任务 启动链式任务
""" """
UserMessageChain().process(body=body, form=form, args=args) MessageChain().process(body=body, form=form, args=args)
@router.post("/", response_model=schemas.Response) @router.post("/", response_model=schemas.Response)
@ -33,7 +33,8 @@ async def user_message(background_tasks: BackgroundTasks, request: Request):
@router.get("/") @router.get("/")
async def wechat_verify(echostr: str, msg_signature: str, timestamp: Union[str, int], nonce: str): async def wechat_verify(echostr: str, msg_signature: str,
timestamp: Union[str, int], nonce: str) -> Any:
""" """
用户消息响应 用户消息响应
""" """

View File

@ -0,0 +1,24 @@
from typing import List, Any
from fastapi import APIRouter, Depends
from app import schemas
from app.chain.search import SearchChain
from app.db.models.user import User
from app.db.userauth import get_current_active_user
from app.schemas.types import MediaType
router = APIRouter()
@router.get("/tmdbid", response_model=List[schemas.Context])
async def search_by_tmdbid(tmdbid: int,
mtype: str = None,
_: User = Depends(get_current_active_user)) -> Any:
"""
根据TMDBID搜索资源
"""
if mtype:
mtype = MediaType.TV if mtype == MediaType.TV.value else MediaType.MOVIE
torrents = SearchChain().search_by_tmdbid(tmdbid=tmdbid, mtype=mtype)
return [torrent.to_dict() for torrent in torrents]

View File

@ -43,7 +43,7 @@ async def update_site(
@router.get("/cookiecloud", response_model=schemas.Response) @router.get("/cookiecloud", response_model=schemas.Response)
async def cookie_cloud_sync(_: User = Depends(get_current_active_user)) -> dict: async def cookie_cloud_sync(_: User = Depends(get_current_active_user)) -> Any:
""" """
运行CookieCloud同步站点信息 运行CookieCloud同步站点信息
""" """

View File

@ -20,14 +20,14 @@ def start_subscribe_chain(title: str, year: str,
""" """
启动订阅链式任务 启动订阅链式任务
""" """
SubscribeChain().process(title=title, year=year, SubscribeChain().add(title=title, year=year,
mtype=mtype, tmdbid=tmdbid, season=season, username=username) mtype=mtype, tmdbid=tmdbid, season=season, username=username)
@router.get("/", response_model=List[schemas.Subscribe]) @router.get("/", response_model=List[schemas.Subscribe])
async def read_subscribes( async def read_subscribes(
db: Session = Depends(get_db), db: Session = Depends(get_db),
_: User = Depends(get_current_active_superuser)): _: User = Depends(get_current_active_superuser)) -> Any:
""" """
查询所有订阅 查询所有订阅
""" """
@ -43,7 +43,7 @@ async def create_subscribe(
""" """
新增订阅 新增订阅
""" """
result = SubscribeChain().process(**subscribe_in.dict()) result = SubscribeChain().add(**subscribe_in.dict())
return {"success": result} return {"success": result}
@ -83,7 +83,7 @@ async def delete_subscribe(
@router.post("/seerr", response_model=schemas.Response) @router.post("/seerr", response_model=schemas.Response)
async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks, async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
authorization: str = Header(None)): authorization: str = Header(None)) -> Any:
""" """
Jellyseerr/Overseerr订阅 Jellyseerr/Overseerr订阅
""" """
@ -136,7 +136,7 @@ async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
@router.get("/refresh", response_model=schemas.Response) @router.get("/refresh", response_model=schemas.Response)
async def refresh_subscribes( async def refresh_subscribes(
_: User = Depends(get_current_active_superuser)): _: User = Depends(get_current_active_superuser)) -> Any:
""" """
刷新所有订阅 刷新所有订阅
""" """
@ -146,7 +146,7 @@ async def refresh_subscribes(
@router.get("/search", response_model=schemas.Response) @router.get("/search", response_model=schemas.Response)
async def search_subscribes( async def search_subscribes(
_: User = Depends(get_current_active_superuser)): _: User = Depends(get_current_active_superuser)) -> Any:
""" """
搜索所有订阅 搜索所有订阅
""" """

View File

@ -3,7 +3,7 @@ from typing import Any
from fastapi import APIRouter, BackgroundTasks, Request from fastapi import APIRouter, BackgroundTasks, Request
from app import schemas from app import schemas
from app.chain.webhook_message import WebhookMessageChain from app.chain.webhook import WebhookChain
from app.core.config import settings from app.core.config import settings
router = APIRouter() router = APIRouter()
@ -13,11 +13,12 @@ def start_webhook_chain(body: Any, form: Any, args: Any):
""" """
启动链式任务 启动链式任务
""" """
WebhookMessageChain().process(body=body, form=form, args=args) WebhookChain().message(body=body, form=form, args=args)
@router.post("/", response_model=schemas.Response) @router.post("/", response_model=schemas.Response)
async def webhook_message(background_tasks: BackgroundTasks, token: str, request: Request): async def webhook_message(background_tasks: BackgroundTasks,
token: str, request: Request) -> Any:
""" """
Webhook响应 Webhook响应
""" """

View File

@ -4,7 +4,7 @@ from fastapi import APIRouter, HTTPException, Depends
from requests import Session from requests import Session
from app import schemas from app import schemas
from app.chain.identify import IdentifyChain from app.chain.media import MediaChain
from app.chain.subscribe import SubscribeChain from app.chain.subscribe import SubscribeChain
from app.core.config import settings from app.core.config import settings
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
@ -232,11 +232,11 @@ async def arr_movie_lookup(apikey: str, term: str, db: Session = Depends(get_db)
) )
tmdbid = term.replace("tmdb:", "") tmdbid = term.replace("tmdb:", "")
# 查询媒体信息 # 查询媒体信息
mediainfo = IdentifyChain().recognize_media(mtype=MediaType.MOVIE, tmdbid=int(tmdbid)) mediainfo = MediaChain().recognize_media(mtype=MediaType.MOVIE, tmdbid=int(tmdbid))
if not mediainfo: if not mediainfo:
return [RadarrMovie()] return [RadarrMovie()]
# 查询是否已存在 # 查询是否已存在
exists = IdentifyChain().media_exists(mediainfo=mediainfo) exists = MediaChain().media_exists(mediainfo=mediainfo)
if not exists: if not exists:
# 文件不存在 # 文件不存在
hasfile = False hasfile = False
@ -311,11 +311,11 @@ async def arr_add_movie(apikey: str, movie: RadarrMovie) -> Any:
status_code=403, status_code=403,
detail="认证失败!", detail="认证失败!",
) )
sid = SubscribeChain().process(title=movie.title, sid = SubscribeChain().add(title=movie.title,
year=movie.year, year=movie.year,
mtype=MediaType.MOVIE, mtype=MediaType.MOVIE,
tmdbid=movie.tmdbId, tmdbid=movie.tmdbId,
userid="Seerr") userid="Seerr")
if sid: if sid:
return { return {
"id": sid "id": sid
@ -501,29 +501,29 @@ async def arr_series_lookup(apikey: str, term: str, db: Session = Depends(get_db
) )
# 查询TMDB媒体信息 # 查询TMDB媒体信息
if not term.startswith("tvdb:"): if not term.startswith("tvdb:"):
mediainfo = IdentifyChain().recognize_media(meta=MetaInfo(term), mediainfo = MediaChain().recognize_media(meta=MetaInfo(term),
mtype=MediaType.MOVIE) mtype=MediaType.MOVIE)
if not mediainfo: if not mediainfo:
return [SonarrSeries()] return [SonarrSeries()]
tvdbid = mediainfo.tvdb_id tvdbid = mediainfo.tvdb_id
tmdbid = mediainfo.tmdb_id tmdbid = mediainfo.tmdb_id
else: else:
tvdbid = int(term.replace("tvdb:", "")) tvdbid = int(term.replace("tvdb:", ""))
mediainfo = IdentifyChain().recognize_media(mtype=MediaType.MOVIE, mediainfo = MediaChain().recognize_media(mtype=MediaType.MOVIE,
tmdbid=tvdbid) tmdbid=tvdbid)
if not mediainfo: if not mediainfo:
return [SonarrSeries()] return [SonarrSeries()]
tmdbid = mediainfo.tmdb_id tmdbid = mediainfo.tmdb_id
# 查询TVDB季信息 # 查询TVDB季信息
seas: List[int] = [] seas: List[int] = []
if tvdbid: if tvdbid:
tvdbinfo = IdentifyChain().tvdb_info(tvdbid=tvdbid) tvdbinfo = MediaChain().tvdb_info(tvdbid=tvdbid)
if tvdbinfo: if tvdbinfo:
sea_num = tvdbinfo.get('season') sea_num = tvdbinfo.get('season')
if sea_num: if sea_num:
seas = list(range(1, int(sea_num) + 1)) seas = list(range(1, int(sea_num) + 1))
# 查询是否存在 # 查询是否存在
exists = IdentifyChain().media_exists(mediainfo) exists = MediaChain().media_exists(mediainfo)
if exists: if exists:
hasfile = True hasfile = True
else: else:
@ -629,12 +629,12 @@ async def arr_add_series(apikey: str, tv: schemas.SonarrSeries) -> Any:
for season in tv.seasons: for season in tv.seasons:
if not season.get("monitored"): if not season.get("monitored"):
continue continue
sid = SubscribeChain().process(title=tv.title, sid = SubscribeChain().add(title=tv.title,
year=tv.year, year=tv.year,
season=season.get("seasonNumber"), season=season.get("seasonNumber"),
tmdbid=tv.tmdbId, tmdbid=tv.tmdbId,
mtype=MediaType.TV, mtype=MediaType.TV,
userid="Seerr") userid="Seerr")
if sid: if sid:
return { return {

View File

@ -1,19 +1,18 @@
import traceback import traceback
from abc import abstractmethod
from pathlib import Path from pathlib import Path
from typing import Optional, Any, Tuple, List, Set, Union, Dict from typing import Optional, Any, Tuple, List, Set, Union, Dict
from ruamel.yaml import CommentedMap from ruamel.yaml import CommentedMap
from app.core.context import Context from app.core.context import Context
from app.core.event import EventManager
from app.core.module import ModuleManager
from app.core.context import MediaInfo, TorrentInfo from app.core.context import MediaInfo, TorrentInfo
from app.core.event import EventManager
from app.core.meta import MetaBase from app.core.meta import MetaBase
from app.core.module import ModuleManager
from app.log import logger from app.log import logger
from app.schemas.context import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent
from app.utils.singleton import AbstractSingleton, Singleton
from app.schemas.types import TorrentStatus, MediaType from app.schemas.types import TorrentStatus, MediaType
from app.utils.singleton import AbstractSingleton, Singleton
class ChainBase(AbstractSingleton, metaclass=Singleton): class ChainBase(AbstractSingleton, metaclass=Singleton):
@ -28,14 +27,7 @@ class ChainBase(AbstractSingleton, metaclass=Singleton):
self.modulemanager = ModuleManager() self.modulemanager = ModuleManager()
self.eventmanager = EventManager() self.eventmanager = EventManager()
@abstractmethod def __run_module(self, method: str, *args, **kwargs) -> Any:
def process(self, *args, **kwargs) -> Optional[Context]:
"""
处理链返回上下文
"""
pass
def run_module(self, method: str, *args, **kwargs) -> Any:
""" """
运行包含该方法的所有模块然后返回结果 运行包含该方法的所有模块然后返回结果
""" """
@ -69,84 +61,84 @@ class ChainBase(AbstractSingleton, metaclass=Singleton):
def prepare_recognize(self, title: str, def prepare_recognize(self, title: str,
subtitle: str = None) -> Tuple[str, str]: subtitle: str = None) -> Tuple[str, str]:
return self.run_module("prepare_recognize", title=title, subtitle=subtitle) return self.__run_module("prepare_recognize", title=title, subtitle=subtitle)
def recognize_media(self, meta: MetaBase = None, def recognize_media(self, meta: MetaBase = None,
mtype: MediaType = None, mtype: MediaType = None,
tmdbid: int = None) -> Optional[MediaInfo]: tmdbid: int = None) -> Optional[MediaInfo]:
return self.run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid) return self.__run_module("recognize_media", meta=meta, mtype=mtype, tmdbid=tmdbid)
def obtain_image(self, mediainfo: MediaInfo) -> Optional[MediaInfo]: def obtain_image(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
return self.run_module("obtain_image", mediainfo=mediainfo) return self.__run_module("obtain_image", mediainfo=mediainfo)
def douban_info(self, doubanid: str) -> Optional[dict]: def douban_info(self, doubanid: str) -> Optional[dict]:
return self.run_module("douban_info", doubanid=doubanid) return self.__run_module("douban_info", doubanid=doubanid)
def tvdb_info(self, tvdbid: int) -> Optional[dict]: def tvdb_info(self, tvdbid: int) -> Optional[dict]:
return self.run_module("tvdb_info", tvdbid=tvdbid) return self.__run_module("tvdb_info", tvdbid=tvdbid)
def message_parser(self, body: Any, form: Any, args: Any) -> Optional[dict]: def message_parser(self, body: Any, form: Any, args: Any) -> Optional[dict]:
return self.run_module("message_parser", body=body, form=form, args=args) return self.__run_module("message_parser", body=body, form=form, args=args)
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[dict]: def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[dict]:
return self.run_module("webhook_parser", body=body, form=form, args=args) return self.__run_module("webhook_parser", body=body, form=form, args=args)
def search_medias(self, meta: MetaBase) -> Optional[List[MediaInfo]]: def search_medias(self, meta: MetaBase) -> Optional[List[MediaInfo]]:
return self.run_module("search_medias", meta=meta) return self.__run_module("search_medias", meta=meta)
def search_torrents(self, mediainfo: Optional[MediaInfo], sites: List[CommentedMap], def search_torrents(self, mediainfo: Optional[MediaInfo], sites: List[CommentedMap],
keyword: str = None) -> Optional[List[TorrentInfo]]: keyword: str = None) -> Optional[List[TorrentInfo]]:
return self.run_module("search_torrents", mediainfo=mediainfo, sites=sites, keyword=keyword) return self.__run_module("search_torrents", mediainfo=mediainfo, sites=sites, keyword=keyword)
def refresh_torrents(self, sites: List[CommentedMap]) -> Optional[List[TorrentInfo]]: def refresh_torrents(self, sites: List[CommentedMap]) -> Optional[List[TorrentInfo]]:
return self.run_module("refresh_torrents", sites=sites) return self.__run_module("refresh_torrents", sites=sites)
def filter_torrents(self, torrent_list: List[TorrentInfo], def filter_torrents(self, torrent_list: List[TorrentInfo],
season_episodes: Dict[int, list] = None) -> List[TorrentInfo]: season_episodes: Dict[int, list] = None) -> List[TorrentInfo]:
return self.run_module("filter_torrents", torrent_list=torrent_list, season_episodes=season_episodes) return self.__run_module("filter_torrents", torrent_list=torrent_list, season_episodes=season_episodes)
def download(self, torrent_path: Path, cookie: str, def download(self, torrent_path: Path, cookie: str,
episodes: Set[int] = None) -> Optional[Tuple[Optional[str], str]]: episodes: Set[int] = None) -> Optional[Tuple[Optional[str], str]]:
return self.run_module("download", torrent_path=torrent_path, cookie=cookie, episodes=episodes) return self.__run_module("download", torrent_path=torrent_path, cookie=cookie, episodes=episodes)
def download_added(self, context: Context, torrent_path: Path) -> None: def download_added(self, context: Context, torrent_path: Path) -> None:
return self.run_module("download_added", context=context, torrent_path=torrent_path) return self.__run_module("download_added", context=context, torrent_path=torrent_path)
def list_torrents(self, status: TorrentStatus = None, def list_torrents(self, status: TorrentStatus = None,
hashs: Union[list, str] = None) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]: hashs: Union[list, str] = None) -> Optional[List[Union[TransferTorrent, DownloadingTorrent]]]:
return self.run_module("list_torrents", status=status, hashs=hashs) return self.__run_module("list_torrents", status=status, hashs=hashs)
def transfer(self, path: Path, mediainfo: MediaInfo) -> Optional[TransferInfo]: def transfer(self, path: Path, mediainfo: MediaInfo) -> Optional[TransferInfo]:
return self.run_module("transfer", path=path, mediainfo=mediainfo) return self.__run_module("transfer", path=path, mediainfo=mediainfo)
def transfer_completed(self, hashs: Union[str, list], transinfo: TransferInfo) -> None: def transfer_completed(self, hashs: Union[str, list], transinfo: TransferInfo) -> None:
return self.run_module("transfer_completed", hashs=hashs, transinfo=transinfo) return self.__run_module("transfer_completed", hashs=hashs, transinfo=transinfo)
def remove_torrents(self, hashs: Union[str, list]) -> bool: def remove_torrents(self, hashs: Union[str, list]) -> bool:
return self.run_module("remove_torrents", hashs=hashs) return self.__run_module("remove_torrents", hashs=hashs)
def media_exists(self, mediainfo: MediaInfo) -> Optional[ExistMediaInfo]: def media_exists(self, mediainfo: MediaInfo) -> Optional[ExistMediaInfo]:
return self.run_module("media_exists", mediainfo=mediainfo) return self.__run_module("media_exists", mediainfo=mediainfo)
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]: def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]:
return self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path) return self.__run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path)
def post_message(self, title: str, text: str = None, def post_message(self, title: str, text: str = None,
image: str = None, userid: Union[str, int] = None) -> Optional[bool]: image: str = None, userid: Union[str, int] = None) -> Optional[bool]:
return self.run_module("post_message", title=title, text=text, image=image, userid=userid) return self.__run_module("post_message", title=title, text=text, image=image, userid=userid)
def post_medias_message(self, title: str, items: List[MediaInfo], def post_medias_message(self, title: str, items: List[MediaInfo],
userid: Union[str, int] = None) -> Optional[bool]: userid: Union[str, int] = None) -> Optional[bool]:
return self.run_module("post_medias_message", title=title, items=items, userid=userid) return self.__run_module("post_medias_message", title=title, items=items, userid=userid)
def post_torrents_message(self, title: str, items: List[Context], def post_torrents_message(self, title: str, items: List[Context],
mediainfo: MediaInfo, mediainfo: MediaInfo,
userid: Union[str, int] = None) -> Optional[bool]: userid: Union[str, int] = None) -> Optional[bool]:
return self.run_module("post_torrents_message", title=title, mediainfo=mediainfo, return self.__run_module("post_torrents_message", title=title, mediainfo=mediainfo,
items=items, userid=userid) items=items, userid=userid)
def scrape_metadata(self, path: Path, mediainfo: MediaInfo) -> None: def scrape_metadata(self, path: Path, mediainfo: MediaInfo) -> None:
return self.run_module("scrape_metadata", path=path, mediainfo=mediainfo) return self.__run_module("scrape_metadata", path=path, mediainfo=mediainfo)
def register_commands(self, commands: dict) -> None: def register_commands(self, commands: dict) -> None:
return self.run_module("register_commands", commands=commands) return self.__run_module("register_commands", commands=commands)

View File

@ -12,7 +12,7 @@ from app.helper.rss import RssHelper
from app.log import logger from app.log import logger
class DoubanSyncChain(ChainBase): class DoubanChain(ChainBase):
""" """
同步豆瓣想看数据 同步豆瓣想看数据
""" """
@ -28,7 +28,7 @@ class DoubanSyncChain(ChainBase):
self.searchchain = SearchChain() self.searchchain = SearchChain()
self.subscribechain = SubscribeChain() self.subscribechain = SubscribeChain()
def process(self): def sync(self):
""" """
通过用户RSS同步豆瓣想看数据 通过用户RSS同步豆瓣想看数据
""" """
@ -80,8 +80,7 @@ class DoubanSyncChain(ChainBase):
continue continue
logger.info(f'{mediainfo.title_year} 媒体库中不存在,开始搜索 ...') logger.info(f'{mediainfo.title_year} 媒体库中不存在,开始搜索 ...')
# 搜索 # 搜索
contexts = self.searchchain.process(meta=meta, contexts = self.searchchain.process(mediainfo=mediainfo,
mediainfo=mediainfo,
no_exists=no_exists) no_exists=no_exists)
if not contexts: if not contexts:
logger.warn(f'{mediainfo.title_year} 未搜索到资源') logger.warn(f'{mediainfo.title_year} 未搜索到资源')
@ -95,12 +94,12 @@ class DoubanSyncChain(ChainBase):
# 未完成下载 # 未完成下载
logger.info(f'{mediainfo.title_year} 未下载未完整,添加订阅 ...') logger.info(f'{mediainfo.title_year} 未下载未完整,添加订阅 ...')
# 添加订阅 # 添加订阅
self.subscribechain.process(title=mediainfo.title, self.subscribechain.add(title=mediainfo.title,
year=mediainfo.year, year=mediainfo.year,
mtype=mediainfo.type, mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id, tmdbid=mediainfo.tmdb_id,
season=meta.begin_season, season=meta.begin_season,
username="豆瓣想看") username="豆瓣想看")
logger.info(f"用户 {user_id} 豆瓣想看同步完成") logger.info(f"用户 {user_id} 豆瓣想看同步完成")
# 保存缓存 # 保存缓存

View File

@ -8,7 +8,7 @@ from app.core.meta import MetaBase
from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.downloadhistory_oper import DownloadHistoryOper
from app.helper.torrent import TorrentHelper from app.helper.torrent import TorrentHelper
from app.log import logger from app.log import logger
from app.schemas.context import ExistMediaInfo, NotExistMediaInfo from app.schemas import ExistMediaInfo, NotExistMediaInfo
from app.schemas.types import MediaType, TorrentStatus, EventType from app.schemas.types import MediaType, TorrentStatus, EventType
from app.utils.string import StringUtils from app.utils.string import StringUtils
@ -20,9 +20,6 @@ class DownloadChain(ChainBase):
self.torrent = TorrentHelper() self.torrent = TorrentHelper()
self.downloadhis = DownloadHistoryOper() self.downloadhis = DownloadHistoryOper()
def process(self, *args, **kwargs) -> Optional[Context]:
pass
def post_download_message(self, meta: MetaBase, mediainfo: MediaInfo, torrent: TorrentInfo, userid: str = None): def post_download_message(self, meta: MetaBase, mediainfo: MediaInfo, torrent: TorrentInfo, userid: str = None):
""" """
发送添加下载的消息 发送添加下载的消息

View File

@ -1,34 +0,0 @@
from typing import Optional
from app.chain import ChainBase
from app.core.metainfo import MetaInfo
from app.core.context import Context, MediaInfo
from app.log import logger
class IdentifyChain(ChainBase):
"""
识别处理链
"""
def process(self, title: str, subtitle: str = None) -> Optional[Context]:
"""
识别媒体信息
"""
logger.info(f'开始识别媒体信息,标题:{title},副标题:{subtitle} ...')
# 识别前预处理
result: Optional[tuple] = self.prepare_recognize(title=title, subtitle=subtitle)
if result:
title, subtitle = result
# 识别元数据
metainfo = MetaInfo(title, subtitle)
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta=metainfo)
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
# 更新媒体图片
self.obtain_image(mediainfo=mediainfo)
# 返回上下文
return Context(meta=metainfo, mediainfo=mediainfo, title=title, subtitle=subtitle)

68
app/chain/media.py Normal file
View File

@ -0,0 +1,68 @@
from typing import Optional, List, Tuple
from app.chain import ChainBase
from app.core.context import Context, MediaInfo
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
from app.log import logger
from app.utils.string import StringUtils
class MediaChain(ChainBase):
"""
识别处理链
"""
def recognize_by_title(self, title: str, subtitle: str = None) -> Optional[Context]:
"""
识别媒体信息
"""
logger.info(f'开始识别媒体信息,标题:{title},副标题:{subtitle} ...')
# 识别前预处理
result: Optional[tuple] = self.prepare_recognize(title=title, subtitle=subtitle)
if result:
title, subtitle = result
# 识别元数据
metainfo = MetaInfo(title, subtitle)
# 识别媒体信息
mediainfo: MediaInfo = self.recognize_media(meta=metainfo)
if not mediainfo:
logger.warn(f'{title} 未识别到媒体信息')
return Context(meta=metainfo)
logger.info(f'{title} 识别到媒体信息:{mediainfo.type.value} {mediainfo.title_year}')
# 更新媒体图片
self.obtain_image(mediainfo=mediainfo)
# 返回上下文
return Context(meta=metainfo, mediainfo=mediainfo, title=title, subtitle=subtitle)
def search(self, title: str) -> Tuple[MetaBase, List[MediaInfo]]:
"""
搜索媒体信息
:param title: 搜索内容
:return: 识别元数据媒体信息列表
"""
# 提取要素
mtype, key_word, season_num, episode_num, year, content = StringUtils.get_keyword(title)
# 识别
meta = MetaInfo(content)
if not meta.name:
logger.warn(f'{title} 未识别到元数据!')
return meta, []
# 合并信息
if mtype:
meta.type = mtype
if season_num:
meta.begin_season = season_num
if episode_num:
meta.begin_episode = episode_num
if year:
meta.year = year
# 开始搜索
logger.info(f"开始搜索媒体信息:{meta.name}")
medias: Optional[List[MediaInfo]] = self.search_medias(meta=meta)
if not medias:
logger.warn(f"{meta.name} 没有找到对应的媒体信息!")
return meta, []
logger.info(f"{content} 搜索到 {len(medias)} 条相关媒体信息")
# 识别的元数据,媒体信息列表
return meta, medias

View File

@ -1,16 +1,16 @@
from typing import Any from typing import Any
from app.chain.download import * from app.chain.download import *
from app.chain.media import MediaChain
from app.chain.search import SearchChain from app.chain.search import SearchChain
from app.chain.subscribe import SubscribeChain from app.chain.subscribe import SubscribeChain
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.core.event import EventManager from app.core.event import EventManager
from app.core.metainfo import MetaInfo
from app.log import logger from app.log import logger
from app.schemas.types import EventType from app.schemas.types import EventType
class UserMessageChain(ChainBase): class MessageChain(ChainBase):
""" """
外来消息处理链 外来消息处理链
""" """
@ -30,6 +30,7 @@ class UserMessageChain(ChainBase):
self.downloadchain = DownloadChain() self.downloadchain = DownloadChain()
self.subscribechain = SubscribeChain() self.subscribechain = SubscribeChain()
self.searchchain = SearchChain() self.searchchain = SearchChain()
self.medtachain = MediaChain()
self.torrent = TorrentHelper() self.torrent = TorrentHelper()
self.eventmanager = EventManager() self.eventmanager = EventManager()
@ -93,16 +94,16 @@ class UserMessageChain(ChainBase):
# 发送缺失的媒体信息 # 发送缺失的媒体信息
if no_exists: if no_exists:
# 发送消息 # 发送消息
messages = [f"{no_exist.get('season')} 季缺失 {len(no_exist.get('episodes')) or no_exist.get('total_episodes')}" messages = [
for no_exist in no_exists.get(mediainfo.tmdb_id)] f"{no_exist.get('season')} 季缺失 {len(no_exist.get('episodes')) or no_exist.get('total_episodes')}"
for no_exist in no_exists.get(mediainfo.tmdb_id)]
self.post_message(title=f"{mediainfo.title_year}\n" + "\n".join(messages)) self.post_message(title=f"{mediainfo.title_year}\n" + "\n".join(messages))
# 搜索种子,过滤掉不需要的剧集,以便选择 # 搜索种子,过滤掉不需要的剧集,以便选择
logger.info(f"{mediainfo.title_year} 媒体库中不存在,开始搜索 ...") logger.info(f"{mediainfo.title_year} 媒体库中不存在,开始搜索 ...")
self.post_message( self.post_message(
title=f"开始搜索 {mediainfo.type.value} {mediainfo.title_year} ...", userid=userid) title=f"开始搜索 {mediainfo.type.value} {mediainfo.title_year} ...", userid=userid)
# 开始搜索 # 开始搜索
contexts = self.searchchain.process(meta=self._current_meta, contexts = self.searchchain.process(mediainfo=mediainfo,
mediainfo=mediainfo,
no_exists=no_exists) no_exists=no_exists)
if not contexts: if not contexts:
# 没有数据 # 没有数据
@ -129,13 +130,13 @@ class UserMessageChain(ChainBase):
elif cache_type == "Subscribe": elif cache_type == "Subscribe":
# 订阅媒体 # 订阅媒体
mediainfo: MediaInfo = cache_list[int(text) - 1] mediainfo: MediaInfo = cache_list[int(text) - 1]
self.subscribechain.process(title=mediainfo.title, self.subscribechain.add(title=mediainfo.title,
year=mediainfo.year, year=mediainfo.year,
mtype=mediainfo.type, mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id, tmdbid=mediainfo.tmdb_id,
season=self._current_meta.begin_season, season=self._current_meta.begin_season,
userid=userid, userid=userid,
username=username) username=username)
elif cache_type == "Torrent": elif cache_type == "Torrent":
if int(text) == 0: if int(text) == 0:
# 自动选择下载 # 自动选择下载
@ -158,13 +159,13 @@ class UserMessageChain(ChainBase):
# 未完成下载 # 未完成下载
logger.info(f'{self._current_media.title_year} 未下载未完整,添加订阅 ...') logger.info(f'{self._current_media.title_year} 未下载未完整,添加订阅 ...')
# 添加订阅 # 添加订阅
self.subscribechain.process(title=self._current_media.title, self.subscribechain.add(title=self._current_media.title,
year=self._current_media.year, year=self._current_media.year,
mtype=self._current_media.type, mtype=self._current_media.type,
tmdbid=self._current_media.tmdb_id, tmdbid=self._current_media.tmdb_id,
season=self._current_meta.begin_season, season=self._current_meta.begin_season,
userid=userid, userid=userid,
username=username) username=username)
else: else:
# 下载种子 # 下载种子
context: Context = cache_list[int(text) - 1] context: Context = cache_list[int(text) - 1]
@ -245,31 +246,19 @@ class UserMessageChain(ChainBase):
# 搜索 # 搜索
content = re.sub(r"(搜索|下载)[:\s]*", "", text) content = re.sub(r"(搜索|下载)[:\s]*", "", text)
action = "Search" action = "Search"
# 提取要素 # 搜索
mtype, key_word, season_num, episode_num, year, title = StringUtils.get_keyword(content) meta, medias = self.medtachain.search(content)
# 识别 # 识别
meta = MetaInfo(title)
if not meta.name: if not meta.name:
self.post_message(title="无法识别输入内容!", userid=userid) self.post_message(title="无法识别输入内容!", userid=userid)
return return
# 合并信息
if mtype:
meta.type = mtype
if season_num:
meta.begin_season = season_num
if episode_num:
meta.begin_episode = episode_num
if year:
meta.year = year
# 记录当前状态
self._current_meta = meta
# 开始搜索 # 开始搜索
logger.info(f"开始搜索:{meta.name}")
medias: Optional[List[MediaInfo]] = self.search_medias(meta=meta)
if not medias: if not medias:
self.post_message(title=f"{meta.name} 没有找到对应的媒体信息!", userid=userid) self.post_message(title=f"{meta.name} 没有找到对应的媒体信息!", userid=userid)
return return
logger.info(f"搜索到 {len(medias)} 条相关媒体信息") logger.info(f"搜索到 {len(medias)} 条相关媒体信息")
# 记录当前状态
self._current_meta = meta
self._user_cache[userid] = { self._user_cache[userid] = {
'type': action, 'type': action,
'items': medias 'items': medias

View File

@ -3,13 +3,12 @@ from typing import Optional, List, Dict
from app.chain import ChainBase from app.chain import ChainBase
from app.core.config import settings from app.core.config import settings
from app.core.context import Context, MediaInfo, TorrentInfo from app.core.context import Context, MediaInfo, TorrentInfo
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.helper.sites import SitesHelper from app.helper.sites import SitesHelper
from app.log import logger from app.log import logger
from app.schemas.context import NotExistMediaInfo from app.schemas import NotExistMediaInfo
from app.utils.string import StringUtils
from app.schemas.types import MediaType from app.schemas.types import MediaType
from app.utils.string import StringUtils
class SearchChain(ChainBase): class SearchChain(ChainBase):
@ -21,12 +20,23 @@ class SearchChain(ChainBase):
super().__init__() super().__init__()
self.siteshelper = SitesHelper() self.siteshelper = SitesHelper()
def process(self, meta: MetaBase, mediainfo: MediaInfo, def search_by_tmdbid(self, tmdbid: int, mtype: str = None) -> Optional[List[Context]]:
"""
根据TMDB ID搜索资源不过滤本地存在的内容
:param tmdbid: TMDB ID
:param mtype: 媒体电影 or 电视剧
"""
mediainfo = self.recognize_media(tmdbid=tmdbid, mtype=mtype)
if not mediainfo:
logger.error(f'{tmdbid} 媒体信息识别失败!')
return None
return self.process(mediainfo=mediainfo)
def process(self, mediainfo: MediaInfo,
keyword: str = None, keyword: str = None,
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None) -> Optional[List[Context]]: no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None) -> Optional[List[Context]]:
""" """
根据媒体信息执行搜索 根据媒体信息搜索种子资源
:param meta: 元数据
:param mediainfo: 媒体信息 :param mediainfo: 媒体信息
:param keyword: 搜索关键词 :param keyword: 搜索关键词
:param no_exists: 缺失的媒体信息 :param no_exists: 缺失的媒体信息
@ -124,6 +134,6 @@ class SearchChain(ChainBase):
_match_torrents = torrents _match_torrents = torrents
logger.info(f"匹配完成,共匹配到 {len(_match_torrents)} 个资源") logger.info(f"匹配完成,共匹配到 {len(_match_torrents)} 个资源")
# 组装上下文返回 # 组装上下文返回
return [Context(meta=MetaInfo(torrent.title), return [Context(meta=MetaInfo(title=torrent.title, subtitle=torrent.description),
mediainfo=mediainfo, mediainfo=mediainfo,
torrentinfo=torrent) for torrent in _match_torrents] torrentinfo=torrent) for torrent in _match_torrents]

View File

@ -7,7 +7,7 @@ from app.helper.cookie import CookieHelper
from app.log import logger from app.log import logger
class SiteMessageChain(ChainBase): class SiteChain(ChainBase):
""" """
站点远程管理处理链 站点远程管理处理链
""" """
@ -20,7 +20,7 @@ class SiteMessageChain(ChainBase):
self._siteoper = SiteOper() self._siteoper = SiteOper()
self._cookiehelper = CookieHelper() self._cookiehelper = CookieHelper()
def process(self, userid: Union[str, int] = None): def list(self, userid: Union[str, int] = None):
""" """
查询所有站点发送消息 查询所有站点发送消息
""" """
@ -63,7 +63,7 @@ class SiteMessageChain(ChainBase):
"is_active": False "is_active": False
}) })
# 重新发送消息 # 重新发送消息
self.process() self.list()
def enable(self, arg_str, userid: Union[str, int] = None): def enable(self, arg_str, userid: Union[str, int] = None):
""" """
@ -84,7 +84,7 @@ class SiteMessageChain(ChainBase):
"is_active": True "is_active": True
}) })
# 重新发送消息 # 重新发送消息
self.process() self.list()
def get_cookie(self, arg_str: str, userid: Union[str, int] = None): def get_cookie(self, arg_str: str, userid: Union[str, int] = None):
""" """

View File

@ -9,7 +9,7 @@ from app.core.config import settings
from app.db.subscribe_oper import SubscribeOper from app.db.subscribe_oper import SubscribeOper
from app.helper.sites import SitesHelper from app.helper.sites import SitesHelper
from app.log import logger from app.log import logger
from app.schemas.context import NotExistMediaInfo from app.schemas import NotExistMediaInfo
from app.utils.string import StringUtils from app.utils.string import StringUtils
from app.schemas.types import MediaType from app.schemas.types import MediaType
@ -29,13 +29,13 @@ class SubscribeChain(ChainBase):
self.subscribehelper = SubscribeOper() self.subscribehelper = SubscribeOper()
self.siteshelper = SitesHelper() self.siteshelper = SitesHelper()
def process(self, title: str, year: str, def add(self, title: str, year: str,
mtype: MediaType = None, mtype: MediaType = None,
tmdbid: int = None, tmdbid: int = None,
season: int = None, season: int = None,
userid: str = None, userid: str = None,
username: str = None, username: str = None,
**kwargs) -> Optional[int]: **kwargs) -> Optional[int]:
""" """
识别媒体信息并添加订阅 识别媒体信息并添加订阅
""" """
@ -153,8 +153,7 @@ class SubscribeChain(ChainBase):
) )
# 搜索 # 搜索
contexts = self.searchchain.process(meta=meta, contexts = self.searchchain.process(mediainfo=mediainfo,
mediainfo=mediainfo,
keyword=subscribe.keyword, keyword=subscribe.keyword,
no_exists=no_exists) no_exists=no_exists)
if not contexts: if not contexts:

View File

@ -9,7 +9,7 @@ from app.core.metainfo import MetaInfo
from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.downloadhistory_oper import DownloadHistoryOper
from app.db.models.downloadhistory import DownloadHistory from app.db.models.downloadhistory import DownloadHistory
from app.log import logger from app.log import logger
from app.schemas.context import TransferInfo, TransferTorrent from app.schemas import TransferInfo, TransferTorrent
from app.schemas.types import TorrentStatus, EventType, MediaType from app.schemas.types import TorrentStatus, EventType, MediaType
from app.utils.string import StringUtils from app.utils.string import StringUtils

View File

@ -6,12 +6,12 @@ from app.utils.http import WebUtils
from app.schemas.types import EventType from app.schemas.types import EventType
class WebhookMessageChain(ChainBase): class WebhookChain(ChainBase):
""" """
响应Webhook事件 响应Webhook事件
""" """
def process(self, body: Any, form: Any, args: Any) -> None: def message(self, body: Any, form: Any, args: Any) -> None:
""" """
处理Webhook报文并发送消息 处理Webhook报文并发送消息
""" """

View File

@ -4,9 +4,9 @@ from typing import Any, Union
from app.chain import ChainBase from app.chain import ChainBase
from app.chain.cookiecloud import CookieCloudChain from app.chain.cookiecloud import CookieCloudChain
from app.chain.douban_sync import DoubanSyncChain from app.chain.douban import DoubanChain
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.chain.site_message import SiteMessageChain from app.chain.site import SiteChain
from app.chain.subscribe import SubscribeChain from app.chain.subscribe import SubscribeChain
from app.chain.transfer import TransferChain from app.chain.transfer import TransferChain
from app.core.event import eventmanager, EventManager from app.core.event import eventmanager, EventManager
@ -50,27 +50,27 @@ class Command(metaclass=Singleton):
"data": {} "data": {}
}, },
"/sites": { "/sites": {
"func": SiteMessageChain().process, "func": SiteChain().list,
"description": "查询站点", "description": "查询站点",
"data": {} "data": {}
}, },
"/site_cookie": { "/site_cookie": {
"func": SiteMessageChain().get_cookie, "func": SiteChain().get_cookie,
"description": "更新站点Cookie", "description": "更新站点Cookie",
"data": {} "data": {}
}, },
"/site_enable": { "/site_enable": {
"func": SiteMessageChain().enable, "func": SiteChain().enable,
"description": "启用站点", "description": "启用站点",
"data": {} "data": {}
}, },
"/site_disable": { "/site_disable": {
"func": SiteMessageChain().disable, "func": SiteChain().disable,
"description": "禁用站点", "description": "禁用站点",
"data": {} "data": {}
}, },
"/douban_sync": { "/douban_sync": {
"func": DoubanSyncChain().process, "func": DoubanChain().sync,
"description": "同步豆瓣想看", "description": "同步豆瓣想看",
"data": {} "data": {}
}, },

View File

@ -1,4 +1,5 @@
from typing import Optional, Any, List import re
from typing import Optional, Any, List, Dict
from app.core.config import settings from app.core.config import settings
from app.core.meta import MetaBase from app.core.meta import MetaBase
@ -121,7 +122,7 @@ class MediaInfo:
# 所有别名和译名 # 所有别名和译名
names: Optional[list] = [] names: Optional[list] = []
# 各季的剧集清单信息 # 各季的剧集清单信息
seasons: Optional[dict] = {} seasons: Optional[Dict[int, list]] = {}
# 各季的年份 # 各季的年份
season_years: Optional[dict] = {} season_years: Optional[dict] = {}
# 二级分类 # 二级分类
@ -302,44 +303,61 @@ class MediaInfo:
self.douban_info = info self.douban_info = info
# 豆瓣ID # 豆瓣ID
self.douban_id = str(info.get("id")) self.douban_id = str(info.get("id"))
# 类型
if not self.type:
self.type = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV
# 标题
if not self.title:
self.title = MetaInfo(info.get("title")).name
# 原语种标题
if not self.original_title:
self.original_title = info.get("original_title")
# 年份
if not self.year:
self.year = info.get("year")[:4] if info.get("year") else None
# 评分 # 评分
if not self.vote_average: if not self.vote_average:
rating = info.get('rating') rating = info.get("rating")
if rating: if rating:
vote_average = float(rating.get("value")) vote_average = float(rating.get("value"))
else: else:
vote_average = 0 vote_average = 0
self.vote_average = vote_average self.vote_average = vote_average
# 标题 # 发行日期
if not self.title: if not self.release_date:
self.title = info.get('title') if info.get("release_date"):
# 年份 self.release_date = info.get("release_date")
if not self.year: elif info.get("pubdate") and isinstance(info.get("pubdate"), list):
self.year = info.get('year')[:4] if info.get('year') else None release_date = info.get("pubdate")[0]
# 原语种标题 if release_date:
if not self.original_title: match = re.search(r'\d{4}-\d{2}-\d{2}', release_date)
self.original_title = info.get("original_title") if match:
# 类型 self.release_date = match.group()
if not self.type: # 海报
self.type = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV
if not self.poster_path: if not self.poster_path:
if self.type == MediaType.MOVIE: self.poster_path = info.get("pic", {}).get("large")
# 海报 if not self.poster_path and info.get("cover_url"):
poster_path = info.get('cover', {}).get("url") self.poster_path = info.get("cover_url")
if not poster_path: if self.poster_path:
poster_path = info.get('cover_url') self.poster_path = self.poster_path.replace("m_ratio_poster", "l_ratio_poster")
if not poster_path:
poster_path = info.get('pic', {}).get("large")
else:
# 海报
poster_path = info.get('pic', {}).get("normal")
self.poster_path = poster_path
# 简介 # 简介
if not self.overview: if not self.overview:
overview = info.get("card_subtitle") or "" self.overview = info.get("intro") or info.get("card_subtitle") or ""
if not self.year and overview: # 导演和演员
if overview.split("/")[0].strip().isdigit(): if not self.directors:
self.year = overview.split("/")[0].strip() self.directors = info.get("directors") or []
if not self.actors:
self.actors = info.get("actors") or []
# 别名
if not self.names:
self.names = info.get("aka") or []
# 剧集
if self.type == MediaType.TV and not self.seasons:
meta = MetaInfo(info.get("title"))
if meta.begin_season:
episodes_count = info.get("episodes_count")
if episodes_count:
self.seasons[meta.begin_season] = list(range(1, episodes_count + 1))
@property @property
def title_year(self): def title_year(self):
@ -420,6 +438,20 @@ class MediaInfo:
return [] return []
return self.seasons.get(sea) or [] return self.seasons.get(sea) or []
def to_dict(self):
"""
返回字典
"""
attributes = [
attr for attr in dir(self)
if not callable(getattr(self, attr)) and not attr.startswith("_")
]
return {
attr: getattr(self, attr).value
if isinstance(getattr(self, attr), MediaType)
else getattr(self, attr) for attr in attributes
}
class Context: class Context:
""" """
@ -505,5 +537,6 @@ class Context:
return { return {
"meta_info": object_to_dict(self.meta_info), "meta_info": object_to_dict(self.meta_info),
"media_info": object_to_dict(self.media_info) "media_info": object_to_dict(self.media_info),
"torrent_info": object_to_dict(self.torrent_info)
} }

View File

@ -6,7 +6,7 @@ from ruamel.yaml import CommentedMap
from app.core.context import MediaInfo, TorrentInfo, Context from app.core.context import MediaInfo, TorrentInfo, Context
from app.core.meta import MetaBase from app.core.meta import MetaBase
from app.schemas.context import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent
from app.schemas.types import TorrentStatus, MediaType from app.schemas.types import TorrentStatus, MediaType

View File

@ -5,7 +5,7 @@ from app.core.context import MediaInfo
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.emby.emby import Emby from app.modules.emby.emby import Emby
from app.schemas.context import ExistMediaInfo, RefreshMediaItem from app.schemas import ExistMediaInfo, RefreshMediaItem
from app.schemas.types import MediaType from app.schemas.types import MediaType

View File

@ -5,7 +5,7 @@ from typing import List, Optional, Union, Dict
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
from app.schemas.context import RefreshMediaItem from app.schemas import RefreshMediaItem
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from app.utils.singleton import Singleton from app.utils.singleton import Singleton
from app.utils.string import StringUtils from app.utils.string import StringUtils

View File

@ -11,7 +11,7 @@ from app.core.config import settings
from app.core.meta import MetaBase from app.core.meta import MetaBase
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.schemas.context import TransferInfo from app.schemas import TransferInfo
from app.utils.system import SystemUtils from app.utils.system import SystemUtils
from app.schemas.types import MediaType from app.schemas.types import MediaType

View File

@ -6,7 +6,7 @@ from app.core.context import MediaInfo
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.jellyfin.jellyfin import Jellyfin from app.modules.jellyfin.jellyfin import Jellyfin
from app.schemas.context import ExistMediaInfo from app.schemas import ExistMediaInfo
from app.schemas.types import MediaType from app.schemas.types import MediaType

View File

@ -5,7 +5,7 @@ from app.core.context import MediaInfo
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.plex.plex import Plex from app.modules.plex.plex import Plex
from app.schemas.context import ExistMediaInfo, RefreshMediaItem from app.schemas import ExistMediaInfo, RefreshMediaItem
from app.schemas.types import MediaType from app.schemas.types import MediaType

View File

@ -8,7 +8,7 @@ from plexapi.server import PlexServer
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
from app.schemas.context import RefreshMediaItem from app.schemas import RefreshMediaItem
from app.utils.singleton import Singleton from app.utils.singleton import Singleton

View File

@ -6,7 +6,7 @@ from app.core.metainfo import MetaInfo
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.qbittorrent.qbittorrent import Qbittorrent from app.modules.qbittorrent.qbittorrent import Qbittorrent
from app.schemas.context import TransferInfo, TransferTorrent, DownloadingTorrent from app.schemas import TransferInfo, TransferTorrent, DownloadingTorrent
from app.utils.string import StringUtils from app.utils.string import StringUtils
from app.schemas.types import TorrentStatus from app.schemas.types import TorrentStatus

View File

@ -539,13 +539,13 @@ class TmdbHelper:
if tmdb_info: if tmdb_info:
tmdb_info['media_type'] = MediaType.TV tmdb_info['media_type'] = MediaType.TV
else: else:
tmdb_info = self.__get_movie_detail(tmdbid) tmdb_info = self.__get_tv_detail(tmdbid)
if tmdb_info: if tmdb_info:
tmdb_info['media_type'] = MediaType.MOVIE tmdb_info['media_type'] = MediaType.TV
else: else:
tmdb_info = self.__get_tv_detail(tmdbid) tmdb_info = self.__get_movie_detail(tmdbid)
if tmdb_info: if tmdb_info:
tmdb_info['media_type'] = MediaType.TV tmdb_info['media_type'] = MediaType.MOVIE
if tmdb_info: if tmdb_info:
# 转换genreid # 转换genreid

View File

@ -6,7 +6,7 @@ from app.core.metainfo import MetaInfo
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.transmission.transmission import Transmission from app.modules.transmission.transmission import Transmission
from app.schemas.context import TransferInfo, TransferTorrent, DownloadingTorrent from app.schemas import TransferInfo, TransferTorrent, DownloadingTorrent
from app.schemas.types import TorrentStatus from app.schemas.types import TorrentStatus

View File

@ -2,7 +2,6 @@ from typing import Tuple
from ruamel.yaml import CommentedMap from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils from app.utils.string import StringUtils

View File

@ -2,7 +2,6 @@ from typing import Tuple
from ruamel.yaml import CommentedMap from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils from app.utils.string import StringUtils

View File

@ -2,7 +2,6 @@ from typing import Tuple
from ruamel.yaml import CommentedMap from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils from app.utils.string import StringUtils

View File

@ -3,7 +3,6 @@ from typing import Tuple
from ruamel.yaml import CommentedMap from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils from app.utils.string import StringUtils

View File

@ -3,7 +3,6 @@ from typing import Tuple
from ruamel.yaml import CommentedMap from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler from app.plugins.autosignin.sites import _ISiteSigninHandler
from app.utils.string import StringUtils from app.utils.string import StringUtils

View File

@ -6,7 +6,7 @@ from apscheduler.executors.pool import ThreadPoolExecutor
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from app.chain.cookiecloud import CookieCloudChain from app.chain.cookiecloud import CookieCloudChain
from app.chain.douban_sync import DoubanSyncChain from app.chain.douban import DoubanChain
from app.chain.subscribe import SubscribeChain from app.chain.subscribe import SubscribeChain
from app.chain.transfer import TransferChain from app.chain.transfer import TransferChain
from app.core.config import settings from app.core.config import settings
@ -51,7 +51,7 @@ class Scheduler(metaclass=Singleton):
self._scheduler.add_job(SubscribeChain().refresh, "cron", hour=trigger.hour, minute=trigger.minute) self._scheduler.add_job(SubscribeChain().refresh, "cron", hour=trigger.hour, minute=trigger.minute)
# 豆瓣同步每30分钟 # 豆瓣同步每30分钟
self._scheduler.add_job(DoubanSyncChain().process, "interval", minutes=30) self._scheduler.add_job(DoubanChain().sync, "interval", minutes=30)
# 下载器文件转移每5分钟 # 下载器文件转移每5分钟
self._scheduler.add_job(TransferChain().process, "interval", minutes=5) self._scheduler.add_job(TransferChain().process, "interval", minutes=5)

View File

@ -3,5 +3,6 @@ from .user import User, UserCreate, UserInDB, UserUpdate
from .response import Response from .response import Response
from .site import Site from .site import Site
from .subscribe import Subscribe from .subscribe import Subscribe
from .context import Context from .context import Context, MediaInfo, MetaInfo, TransferTorrent, DownloadingTorrent, TransferInfo, ExistMediaInfo, \
NotExistMediaInfo, RefreshMediaItem
from .servarr import RadarrMovie, SonarrSeries from .servarr import RadarrMovie, SonarrSeries

View File

@ -80,6 +80,57 @@ class MediaInfo(BaseModel):
overview: Optional[str] = None overview: Optional[str] = None
# 二级分类 # 二级分类
category: str = "" category: str = ""
# 季集
seasons: Dict[int, list] = {}
# 别名和译名
names: list = []
class TorrentInfo(BaseModel):
# 站点ID
site: int = None
# 站点名称
site_name: Optional[str] = None
# 站点Cookie
site_cookie: Optional[str] = None
# 站点UA
site_ua: Optional[str] = None
# 站点是否使用代理
site_proxy: bool = False
# 站点优先级
site_order: int = 0
# 种子名称
title: Optional[str] = None
# 种子副标题
description: Optional[str] = None
# IMDB ID
imdbid: str = None
# 种子链接
enclosure: Optional[str] = None
# 详情页面
page_url: Optional[str] = None
# 种子大小
size: float = 0
# 做种者
seeders: int = 0
# 下载者
peers: int = 0
# 完成者
grabs: int = 0
# 发布时间
pubdate: Optional[str] = None
# 已过时间
date_elapsed: Optional[str] = None
# 上传因子
uploadvolumefactor: Optional[float] = None
# 下载因子
downloadvolumefactor: Optional[float] = None
# HR
hit_and_run: bool = False
# 种子标签
labels: Optional[list] = []
# 种子优先级
pri_order: int = 0
class Context(BaseModel): class Context(BaseModel):
@ -87,6 +138,8 @@ class Context(BaseModel):
meta_info: Optional[MetaInfo] meta_info: Optional[MetaInfo]
# 媒体信息 # 媒体信息
media_info: Optional[MediaInfo] media_info: Optional[MediaInfo]
# 种子信息
torrent_info: Optional[TorrentInfo]
class TransferTorrent(BaseModel): class TransferTorrent(BaseModel):
@ -124,7 +177,7 @@ class ExistMediaInfo(BaseModel):
# 类型 电影、电视剧 # 类型 电影、电视剧
type: MediaType type: MediaType
# 季 # 季
seasons: Optional[Dict[int, list]] = None seasons: Dict[int, list] = {}
class NotExistMediaInfo(BaseModel): class NotExistMediaInfo(BaseModel):

View File

@ -2,7 +2,7 @@
from unittest import TestCase from unittest import TestCase
from app.chain.douban_sync import DoubanSyncChain from app.chain.douban import DoubanChain
class DoubanSyncTest(TestCase): class DoubanSyncTest(TestCase):
@ -14,4 +14,4 @@ class DoubanSyncTest(TestCase):
@staticmethod @staticmethod
def test_doubansync(): def test_doubansync():
DoubanSyncChain().process() DoubanChain().sync()

View File

@ -3,7 +3,7 @@
from unittest import TestCase from unittest import TestCase
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.chain.identify import IdentifyChain from app.chain.media import MediaChain
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
@ -15,7 +15,7 @@ class RecognizeTest(TestCase):
pass pass
def test_recognize(self): def test_recognize(self):
result = IdentifyChain().process(title="我和我的祖国 2019") result = MediaChain().recognize_by_title(title="我和我的祖国 2019")
self.assertEqual(result.media_info.tmdb_id, 612845) self.assertEqual(result.media_info.tmdb_id, 612845)
exists = DownloadChain().get_no_exists_info(MetaInfo("我和我的祖国 2019"), result.media_info) exists = DownloadChain().get_no_exists_info(MetaInfo("我和我的祖国 2019"), result.media_info)
self.assertTrue(exists[0]) self.assertTrue(exists[0])