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 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.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(subscribe.router, prefix="/subscribe", tags=["subscribe"])
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"])

View File

@ -1,7 +1,9 @@
from typing import Any
from fastapi import APIRouter, Depends, BackgroundTasks
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.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)
async def sync_douban(
background_tasks: BackgroundTasks,
_: User = Depends(get_current_active_superuser)):
_: User = Depends(get_current_active_superuser)) -> Any:
"""
查询所有订阅
同步豆瓣想看
"""
background_tasks.add_task(start_douban_chain)
return {"success": True}

View File

@ -1,20 +1,59 @@
from typing import List, Any
from fastapi import APIRouter, Depends
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.userauth import get_current_active_user
from app.schemas.types import MediaType
router = APIRouter()
@router.post("/recognize", response_model=schemas.Context)
@router.get("/recognize", response_model=schemas.Context)
async def recognize(title: str,
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()
@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 app import schemas
from app.chain.user_message import UserMessageChain
from app.chain.message import MessageChain
from app.core.config import settings
from app.log import logger
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)
@ -33,7 +33,8 @@ async def user_message(background_tasks: BackgroundTasks, request: Request):
@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)
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同步站点信息
"""

View File

@ -20,14 +20,14 @@ def start_subscribe_chain(title: str, year: str,
"""
启动订阅链式任务
"""
SubscribeChain().process(title=title, year=year,
mtype=mtype, tmdbid=tmdbid, season=season, username=username)
SubscribeChain().add(title=title, year=year,
mtype=mtype, tmdbid=tmdbid, season=season, username=username)
@router.get("/", response_model=List[schemas.Subscribe])
async def read_subscribes(
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}
@ -83,7 +83,7 @@ async def delete_subscribe(
@router.post("/seerr", response_model=schemas.Response)
async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
authorization: str = Header(None)):
authorization: str = Header(None)) -> Any:
"""
Jellyseerr/Overseerr订阅
"""
@ -136,7 +136,7 @@ async def seerr_subscribe(request: Request, background_tasks: BackgroundTasks,
@router.get("/refresh", response_model=schemas.Response)
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)
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 app import schemas
from app.chain.webhook_message import WebhookMessageChain
from app.chain.webhook import WebhookChain
from app.core.config import settings
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)
async def webhook_message(background_tasks: BackgroundTasks, token: str, request: Request):
async def webhook_message(background_tasks: BackgroundTasks,
token: str, request: Request) -> Any:
"""
Webhook响应
"""

View File

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

View File

@ -1,19 +1,18 @@
import traceback
from abc import abstractmethod
from pathlib import Path
from typing import Optional, Any, Tuple, List, Set, Union, Dict
from ruamel.yaml import CommentedMap
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.event import EventManager
from app.core.meta import MetaBase
from app.core.module import ModuleManager
from app.log import logger
from app.schemas.context import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent
from app.utils.singleton import AbstractSingleton, Singleton
from app.schemas import TransferInfo, TransferTorrent, ExistMediaInfo, DownloadingTorrent
from app.schemas.types import TorrentStatus, MediaType
from app.utils.singleton import AbstractSingleton, Singleton
class ChainBase(AbstractSingleton, metaclass=Singleton):
@ -28,14 +27,7 @@ class ChainBase(AbstractSingleton, metaclass=Singleton):
self.modulemanager = ModuleManager()
self.eventmanager = EventManager()
@abstractmethod
def process(self, *args, **kwargs) -> Optional[Context]:
"""
处理链返回上下文
"""
pass
def run_module(self, method: str, *args, **kwargs) -> Any:
def __run_module(self, method: str, *args, **kwargs) -> Any:
"""
运行包含该方法的所有模块然后返回结果
"""
@ -69,84 +61,84 @@ class ChainBase(AbstractSingleton, metaclass=Singleton):
def prepare_recognize(self, title: 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,
mtype: MediaType = None,
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]:
return self.run_module("obtain_image", mediainfo=mediainfo)
return self.__run_module("obtain_image", mediainfo=mediainfo)
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]:
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]:
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]:
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]]:
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],
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]]:
return self.run_module("refresh_torrents", sites=sites)
return self.__run_module("refresh_torrents", sites=sites)
def filter_torrents(self, torrent_list: 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,
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:
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,
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]:
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:
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:
return self.run_module("remove_torrents", hashs=hashs)
return self.__run_module("remove_torrents", hashs=hashs)
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]:
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,
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],
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],
mediainfo: MediaInfo,
userid: Union[str, int] = None) -> Optional[bool]:
return self.run_module("post_torrents_message", title=title, mediainfo=mediainfo,
items=items, userid=userid)
return self.__run_module("post_torrents_message", title=title, mediainfo=mediainfo,
items=items, userid=userid)
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:
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
class DoubanSyncChain(ChainBase):
class DoubanChain(ChainBase):
"""
同步豆瓣想看数据
"""
@ -28,7 +28,7 @@ class DoubanSyncChain(ChainBase):
self.searchchain = SearchChain()
self.subscribechain = SubscribeChain()
def process(self):
def sync(self):
"""
通过用户RSS同步豆瓣想看数据
"""
@ -80,8 +80,7 @@ class DoubanSyncChain(ChainBase):
continue
logger.info(f'{mediainfo.title_year} 媒体库中不存在,开始搜索 ...')
# 搜索
contexts = self.searchchain.process(meta=meta,
mediainfo=mediainfo,
contexts = self.searchchain.process(mediainfo=mediainfo,
no_exists=no_exists)
if not contexts:
logger.warn(f'{mediainfo.title_year} 未搜索到资源')
@ -95,12 +94,12 @@ class DoubanSyncChain(ChainBase):
# 未完成下载
logger.info(f'{mediainfo.title_year} 未下载未完整,添加订阅 ...')
# 添加订阅
self.subscribechain.process(title=mediainfo.title,
year=mediainfo.year,
mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id,
season=meta.begin_season,
username="豆瓣想看")
self.subscribechain.add(title=mediainfo.title,
year=mediainfo.year,
mtype=mediainfo.type,
tmdbid=mediainfo.tmdb_id,
season=meta.begin_season,
username="豆瓣想看")
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.helper.torrent import TorrentHelper
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.utils.string import StringUtils
@ -20,9 +20,6 @@ class DownloadChain(ChainBase):
self.torrent = TorrentHelper()
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):
"""
发送添加下载的消息

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

View File

@ -3,13 +3,12 @@ from typing import Optional, List, Dict
from app.chain import ChainBase
from app.core.config import settings
from app.core.context import Context, MediaInfo, TorrentInfo
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
from app.helper.sites import SitesHelper
from app.log import logger
from app.schemas.context import NotExistMediaInfo
from app.utils.string import StringUtils
from app.schemas import NotExistMediaInfo
from app.schemas.types import MediaType
from app.utils.string import StringUtils
class SearchChain(ChainBase):
@ -21,12 +20,23 @@ class SearchChain(ChainBase):
super().__init__()
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,
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None) -> Optional[List[Context]]:
"""
根据媒体信息执行搜索
:param meta: 元数据
根据媒体信息搜索种子资源
:param mediainfo: 媒体信息
:param keyword: 搜索关键词
:param no_exists: 缺失的媒体信息
@ -124,6 +134,6 @@ class SearchChain(ChainBase):
_match_torrents = 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,
torrentinfo=torrent) for torrent in _match_torrents]

View File

@ -7,7 +7,7 @@ from app.helper.cookie import CookieHelper
from app.log import logger
class SiteMessageChain(ChainBase):
class SiteChain(ChainBase):
"""
站点远程管理处理链
"""
@ -20,7 +20,7 @@ class SiteMessageChain(ChainBase):
self._siteoper = SiteOper()
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
})
# 重新发送消息
self.process()
self.list()
def enable(self, arg_str, userid: Union[str, int] = None):
"""
@ -84,7 +84,7 @@ class SiteMessageChain(ChainBase):
"is_active": True
})
# 重新发送消息
self.process()
self.list()
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.helper.sites import SitesHelper
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
@ -29,13 +29,13 @@ class SubscribeChain(ChainBase):
self.subscribehelper = SubscribeOper()
self.siteshelper = SitesHelper()
def process(self, title: str, year: str,
mtype: MediaType = None,
tmdbid: int = None,
season: int = None,
userid: str = None,
username: str = None,
**kwargs) -> Optional[int]:
def add(self, title: str, year: str,
mtype: MediaType = None,
tmdbid: int = None,
season: int = None,
userid: str = None,
username: str = None,
**kwargs) -> Optional[int]:
"""
识别媒体信息并添加订阅
"""
@ -153,8 +153,7 @@ class SubscribeChain(ChainBase):
)
# 搜索
contexts = self.searchchain.process(meta=meta,
mediainfo=mediainfo,
contexts = self.searchchain.process(mediainfo=mediainfo,
keyword=subscribe.keyword,
no_exists=no_exists)
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.models.downloadhistory import DownloadHistory
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.utils.string import StringUtils

View File

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

View File

@ -4,9 +4,9 @@ from typing import Any, Union
from app.chain import ChainBase
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.site_message import SiteMessageChain
from app.chain.site import SiteChain
from app.chain.subscribe import SubscribeChain
from app.chain.transfer import TransferChain
from app.core.event import eventmanager, EventManager
@ -50,27 +50,27 @@ class Command(metaclass=Singleton):
"data": {}
},
"/sites": {
"func": SiteMessageChain().process,
"func": SiteChain().list,
"description": "查询站点",
"data": {}
},
"/site_cookie": {
"func": SiteMessageChain().get_cookie,
"func": SiteChain().get_cookie,
"description": "更新站点Cookie",
"data": {}
},
"/site_enable": {
"func": SiteMessageChain().enable,
"func": SiteChain().enable,
"description": "启用站点",
"data": {}
},
"/site_disable": {
"func": SiteMessageChain().disable,
"func": SiteChain().disable,
"description": "禁用站点",
"data": {}
},
"/douban_sync": {
"func": DoubanSyncChain().process,
"func": DoubanChain().sync,
"description": "同步豆瓣想看",
"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.meta import MetaBase
@ -121,7 +122,7 @@ class MediaInfo:
# 所有别名和译名
names: Optional[list] = []
# 各季的剧集清单信息
seasons: Optional[dict] = {}
seasons: Optional[Dict[int, list]] = {}
# 各季的年份
season_years: Optional[dict] = {}
# 二级分类
@ -302,44 +303,61 @@ class MediaInfo:
self.douban_info = info
# 豆瓣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:
rating = info.get('rating')
rating = info.get("rating")
if rating:
vote_average = float(rating.get("value"))
else:
vote_average = 0
self.vote_average = vote_average
# 标题
if not self.title:
self.title = info.get('title')
# 年份
if not self.year:
self.year = info.get('year')[:4] if info.get('year') else None
# 原语种标题
if not self.original_title:
self.original_title = info.get("original_title")
# 类型
if not self.type:
self.type = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV
# 发行日期
if not self.release_date:
if info.get("release_date"):
self.release_date = info.get("release_date")
elif info.get("pubdate") and isinstance(info.get("pubdate"), list):
release_date = info.get("pubdate")[0]
if release_date:
match = re.search(r'\d{4}-\d{2}-\d{2}', release_date)
if match:
self.release_date = match.group()
# 海报
if not self.poster_path:
if self.type == MediaType.MOVIE:
# 海报
poster_path = info.get('cover', {}).get("url")
if not poster_path:
poster_path = info.get('cover_url')
if not poster_path:
poster_path = info.get('pic', {}).get("large")
else:
# 海报
poster_path = info.get('pic', {}).get("normal")
self.poster_path = poster_path
self.poster_path = info.get("pic", {}).get("large")
if not self.poster_path and info.get("cover_url"):
self.poster_path = info.get("cover_url")
if self.poster_path:
self.poster_path = self.poster_path.replace("m_ratio_poster", "l_ratio_poster")
# 简介
if not self.overview:
overview = info.get("card_subtitle") or ""
if not self.year and overview:
if overview.split("/")[0].strip().isdigit():
self.year = overview.split("/")[0].strip()
self.overview = info.get("intro") or info.get("card_subtitle") or ""
# 导演和演员
if not self.directors:
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
def title_year(self):
@ -420,6 +438,20 @@ class MediaInfo:
return []
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:
"""
@ -505,5 +537,6 @@ class Context:
return {
"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.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

View File

@ -5,7 +5,7 @@ from app.core.context import MediaInfo
from app.log import logger
from app.modules import _ModuleBase
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

View File

@ -5,7 +5,7 @@ from typing import List, Optional, Union, Dict
from app.core.config import settings
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.singleton import Singleton
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.log import logger
from app.modules import _ModuleBase
from app.schemas.context import TransferInfo
from app.schemas import TransferInfo
from app.utils.system import SystemUtils
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.modules import _ModuleBase
from app.modules.jellyfin.jellyfin import Jellyfin
from app.schemas.context import ExistMediaInfo
from app.schemas import ExistMediaInfo
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.modules import _ModuleBase
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

View File

@ -8,7 +8,7 @@ from plexapi.server import PlexServer
from app.core.config import settings
from app.log import logger
from app.schemas.context import RefreshMediaItem
from app.schemas import RefreshMediaItem
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.modules import _ModuleBase
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.schemas.types import TorrentStatus

View File

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

View File

@ -6,7 +6,7 @@ from app.core.metainfo import MetaInfo
from app.log import logger
from app.modules import _ModuleBase
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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ from typing import Tuple
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
from app.plugins.autosignin.sites import _ISiteSigninHandler
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 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.transfer import TransferChain
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)
# 豆瓣同步每30分钟
self._scheduler.add_job(DoubanSyncChain().process, "interval", minutes=30)
self._scheduler.add_job(DoubanChain().sync, "interval", minutes=30)
# 下载器文件转移每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 .site import Site
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

View File

@ -80,6 +80,57 @@ class MediaInfo(BaseModel):
overview: Optional[str] = None
# 二级分类
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):
@ -87,6 +138,8 @@ class Context(BaseModel):
meta_info: Optional[MetaInfo]
# 媒体信息
media_info: Optional[MediaInfo]
# 种子信息
torrent_info: Optional[TorrentInfo]
class TransferTorrent(BaseModel):
@ -124,7 +177,7 @@ class ExistMediaInfo(BaseModel):
# 类型 电影、电视剧
type: MediaType
# 季
seasons: Optional[Dict[int, list]] = None
seasons: Dict[int, list] = {}
class NotExistMediaInfo(BaseModel):

View File

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

View File

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