mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-10 17:42:45 +08:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e2ad34864 | ||
|
|
e9a147d43c | ||
|
|
a340ee045e | ||
|
|
12405f3c34 | ||
|
|
1e465ee231 | ||
|
|
f06c24c23e | ||
|
|
4b93ee4843 | ||
|
|
c022e05ab9 | ||
|
|
c2a0d9d657 | ||
|
|
6fcf2c2f1f | ||
|
|
bc37daef58 | ||
|
|
fab5995c4e | ||
|
|
0ba8aa75f5 | ||
|
|
e24b3ed07a | ||
|
|
f9bddcb406 | ||
|
|
247b3b24a1 | ||
|
|
759c18acda | ||
|
|
b2462c5950 | ||
|
|
3d947f712c | ||
|
|
89d917e487 | ||
|
|
28b0a20b26 | ||
|
|
6d4396f4ba | ||
|
|
75dd0f27cf | ||
|
|
cb9be86c10 | ||
|
|
0b8f021505 | ||
|
|
f2d3b1c13f | ||
|
|
6f24c6ba49 | ||
|
|
c5a9df88dc | ||
|
|
20b2df364a | ||
|
|
e89103b96f | ||
|
|
49f1c9c10b | ||
|
|
b320c84c4c | ||
|
|
e916b84ee5 | ||
|
|
18633a3b41 | ||
|
|
0683498497 | ||
|
|
7468fa4f1e | ||
|
|
ab2b33a9fd | ||
|
|
8bedac023b | ||
|
|
7893b41175 | ||
|
|
ab73dbb3cd | ||
|
|
cb042dbe68 | ||
|
|
bba0d363d7 | ||
|
|
8635d8c53f | ||
|
|
dae6894e8b | ||
|
|
b76991a027 | ||
|
|
de61c43db4 | ||
|
|
890afc2a72 | ||
|
|
8d4e1f3af6 | ||
|
|
85507a4fff | ||
|
|
6d395f9866 | ||
|
|
c589f42181 | ||
|
|
87bb121060 | ||
|
|
42cd35ab3c | ||
|
|
669da0d882 | ||
|
|
9ac1346f80 | ||
|
|
f6981734d0 | ||
|
|
cb6aa61b6b | ||
|
|
2ed9cfcc9a | ||
|
|
2e796f41cb | ||
|
|
7d13e43c6f | ||
|
|
db684de6e9 | ||
|
|
510ef59aa0 | ||
|
|
d56083a29e | ||
|
|
8aed2b334e | ||
|
|
3bf27f224c | ||
|
|
dc9a54e74f | ||
|
|
79dc194dd6 | ||
|
|
8e12249201 | ||
|
|
4fa8f5b248 | ||
|
|
3089c0c524 | ||
|
|
ba1ca0819e |
@@ -2,7 +2,7 @@ from fastapi import APIRouter
|
|||||||
|
|
||||||
from app.api.endpoints import login, user, site, message, webhook, subscribe, \
|
from app.api.endpoints import login, user, site, message, webhook, subscribe, \
|
||||||
media, douban, search, plugin, tmdb, history, system, download, dashboard, \
|
media, douban, search, plugin, tmdb, history, system, download, dashboard, \
|
||||||
transfer, mediaserver, bangumi, storage
|
transfer, mediaserver, bangumi, storage, discover, recommend
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(login.router, prefix="/login", tags=["login"])
|
api_router.include_router(login.router, prefix="/login", tags=["login"])
|
||||||
@@ -24,3 +24,5 @@ api_router.include_router(storage.router, prefix="/storage", tags=["storage"])
|
|||||||
api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"])
|
api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"])
|
||||||
api_router.include_router(mediaserver.router, prefix="/mediaserver", tags=["mediaserver"])
|
api_router.include_router(mediaserver.router, prefix="/mediaserver", tags=["mediaserver"])
|
||||||
api_router.include_router(bangumi.router, prefix="/bangumi", tags=["bangumi"])
|
api_router.include_router(bangumi.router, prefix="/bangumi", tags=["bangumi"])
|
||||||
|
api_router.include_router(discover.router, prefix="/discover", tags=["discover"])
|
||||||
|
api_router.include_router(recommend.router, prefix="/recommend", tags=["recommend"])
|
||||||
|
|||||||
@@ -4,23 +4,12 @@ from fastapi import APIRouter, Depends
|
|||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.bangumi import BangumiChain
|
from app.chain.bangumi import BangumiChain
|
||||||
from app.chain.recommend import RecommendChain
|
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/calendar", summary="Bangumi每日放送", response_model=List[schemas.MediaInfo])
|
|
||||||
def calendar(page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
浏览Bangumi每日放送
|
|
||||||
"""
|
|
||||||
return RecommendChain().bangumi_calendar(page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/credits/{bangumiid}", summary="查询Bangumi演职员表", response_model=List[schemas.MediaPerson])
|
@router.get("/credits/{bangumiid}", summary="查询Bangumi演职员表", response_model=List[schemas.MediaPerson])
|
||||||
def bangumi_credits(bangumiid: int,
|
def bangumi_credits(bangumiid: int,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
@@ -61,13 +50,14 @@ def bangumi_person(person_id: int,
|
|||||||
@router.get("/person/credits/{person_id}", summary="人物参演作品", response_model=List[schemas.MediaInfo])
|
@router.get("/person/credits/{person_id}", summary="人物参演作品", response_model=List[schemas.MediaInfo])
|
||||||
def bangumi_person_credits(person_id: int,
|
def bangumi_person_credits(person_id: int,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
|
count: int = 20,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据人物ID查询人物参演作品
|
根据人物ID查询人物参演作品
|
||||||
"""
|
"""
|
||||||
medias = BangumiChain().person_credits(person_id=person_id)
|
medias = BangumiChain().person_credits(person_id=person_id)
|
||||||
if medias:
|
if medias:
|
||||||
return [media.to_dict() for media in medias[(page - 1) * 20: page * 20]]
|
return [media.to_dict() for media in medias[(page - 1) * count: page * count]]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
130
app/api/endpoints/discover.py
Normal file
130
app/api/endpoints/discover.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
|
from app.core.event import eventmanager
|
||||||
|
from app.core.security import verify_token
|
||||||
|
from app.schemas import DiscoverSourceEventData
|
||||||
|
from app.schemas.types import ChainEventType, MediaType
|
||||||
|
from chain.bangumi import BangumiChain
|
||||||
|
from chain.douban import DoubanChain
|
||||||
|
from chain.tmdb import TmdbChain
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/source", summary="获取探索数据源", response_model=List[schemas.DiscoverMediaSource])
|
||||||
|
def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
获取探索数据源
|
||||||
|
"""
|
||||||
|
# 广播事件,请示额外的探索数据源支持
|
||||||
|
event_data = DiscoverSourceEventData()
|
||||||
|
event = eventmanager.send_event(ChainEventType.DiscoverSource, event_data)
|
||||||
|
# 使用事件返回的上下文数据
|
||||||
|
if event and event.event_data:
|
||||||
|
event_data: DiscoverSourceEventData = event.event_data
|
||||||
|
if event_data.extra_sources:
|
||||||
|
return event_data.extra_sources
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/bangumi", summary="探索Bangumi", response_model=List[schemas.MediaInfo])
|
||||||
|
def bangumi(type: int = 2,
|
||||||
|
cat: int = None,
|
||||||
|
sort: str = 'rank',
|
||||||
|
year: int = None,
|
||||||
|
page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
探索Bangumi
|
||||||
|
"""
|
||||||
|
medias = BangumiChain().discover(type=type, cat=cat, sort=sort, year=year,
|
||||||
|
limit=count, offset=(page - 1) * count)
|
||||||
|
if medias:
|
||||||
|
return [media.to_dict() for media in medias]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_movies", summary="探索豆瓣电影", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_movies(sort: str = "R",
|
||||||
|
tags: str = "",
|
||||||
|
page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览豆瓣电影信息
|
||||||
|
"""
|
||||||
|
movies = DoubanChain().douban_discover(mtype=MediaType.MOVIE,
|
||||||
|
sort=sort, tags=tags, page=page, count=count)
|
||||||
|
return [media.to_dict() for media in movies] if movies else []
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_tvs", summary="探索豆瓣剧集", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_tvs(sort: str = "R",
|
||||||
|
tags: str = "",
|
||||||
|
page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览豆瓣剧集信息
|
||||||
|
"""
|
||||||
|
tvs = DoubanChain().douban_discover(mtype=MediaType.TV,
|
||||||
|
sort=sort, tags=tags, page=page, count=count)
|
||||||
|
return [media.to_dict() for media in tvs] if tvs else []
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tmdb_movies", summary="探索TMDB电影", response_model=List[schemas.MediaInfo])
|
||||||
|
def tmdb_movies(sort_by: str = "popularity.desc",
|
||||||
|
with_genres: str = "",
|
||||||
|
with_original_language: str = "",
|
||||||
|
with_keywords: str = "",
|
||||||
|
with_watch_providers: str = "",
|
||||||
|
vote_average: float = 0,
|
||||||
|
vote_count: int = 0,
|
||||||
|
release_date: str = "",
|
||||||
|
page: int = 1,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览TMDB电影信息
|
||||||
|
"""
|
||||||
|
movies = TmdbChain().tmdb_discover(mtype=MediaType.MOVIE,
|
||||||
|
sort_by=sort_by,
|
||||||
|
with_genres=with_genres,
|
||||||
|
with_original_language=with_original_language,
|
||||||
|
with_keywords=with_keywords,
|
||||||
|
with_watch_providers=with_watch_providers,
|
||||||
|
vote_average=vote_average,
|
||||||
|
vote_count=vote_count,
|
||||||
|
release_date=release_date,
|
||||||
|
page=page)
|
||||||
|
return [movie.to_dict() for movie in movies] if movies else []
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tmdb_tvs", summary="探索TMDB剧集", response_model=List[schemas.MediaInfo])
|
||||||
|
def tmdb_tvs(sort_by: str = "popularity.desc",
|
||||||
|
with_genres: str = "",
|
||||||
|
with_original_language: str = "",
|
||||||
|
with_keywords: str = "",
|
||||||
|
with_watch_providers: str = "",
|
||||||
|
vote_average: float = 0,
|
||||||
|
vote_count: int = 0,
|
||||||
|
release_date: str = "",
|
||||||
|
page: int = 1,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览TMDB剧集信息
|
||||||
|
"""
|
||||||
|
tvs = TmdbChain().tmdb_discover(mtype=MediaType.TV,
|
||||||
|
sort_by=sort_by,
|
||||||
|
with_genres=with_genres,
|
||||||
|
with_original_language=with_original_language,
|
||||||
|
with_keywords=with_keywords,
|
||||||
|
with_watch_providers=with_watch_providers,
|
||||||
|
vote_average=vote_average,
|
||||||
|
vote_count=vote_count,
|
||||||
|
release_date=release_date,
|
||||||
|
page=page)
|
||||||
|
return [tv.to_dict() for tv in tvs] if tvs else []
|
||||||
@@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends
|
|||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.douban import DoubanChain
|
from app.chain.douban import DoubanChain
|
||||||
from app.chain.recommend import RecommendChain
|
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.schemas import MediaType
|
from app.schemas import MediaType
|
||||||
@@ -34,100 +33,6 @@ def douban_person_credits(person_id: int,
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
@router.get("/showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
|
|
||||||
def movie_showing(page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
浏览豆瓣正在热映
|
|
||||||
"""
|
|
||||||
return RecommendChain().douban_movie_showing(page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo])
|
|
||||||
def douban_movies(sort: str = "R",
|
|
||||||
tags: str = "",
|
|
||||||
page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
浏览豆瓣电影信息
|
|
||||||
"""
|
|
||||||
return RecommendChain().douban_movies(sort=sort, tags=tags, page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo])
|
|
||||||
def douban_tvs(sort: str = "R",
|
|
||||||
tags: str = "",
|
|
||||||
page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
浏览豆瓣剧集信息
|
|
||||||
"""
|
|
||||||
return RecommendChain().douban_tvs(sort=sort, tags=tags, page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
|
|
||||||
def movie_top250(page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
浏览豆瓣剧集信息
|
|
||||||
"""
|
|
||||||
return RecommendChain().douban_movie_top250(page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
|
|
||||||
def tv_weekly_chinese(page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
中国每周剧集口碑榜
|
|
||||||
"""
|
|
||||||
return RecommendChain().douban_tv_weekly_chinese(page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
|
|
||||||
def tv_weekly_global(page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
全球每周剧集口碑榜
|
|
||||||
"""
|
|
||||||
return RecommendChain().douban_tv_weekly_global(page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
|
|
||||||
def tv_animation(page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
热门动画剧集
|
|
||||||
"""
|
|
||||||
return RecommendChain().douban_tv_animation(page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo])
|
|
||||||
def movie_hot(page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
热门电影
|
|
||||||
"""
|
|
||||||
return RecommendChain().douban_movie_hot(page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo])
|
|
||||||
def tv_hot(page: int = 1,
|
|
||||||
count: int = 30,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
热门电视剧
|
|
||||||
"""
|
|
||||||
return RecommendChain().douban_tv_hot(page=page, count=count)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/credits/{doubanid}/{type_name}", summary="豆瓣演员阵容", response_model=List[schemas.MediaPerson])
|
@router.get("/credits/{doubanid}/{type_name}", summary="豆瓣演员阵容", response_model=List[schemas.MediaPerson])
|
||||||
def douban_credits(doubanid: str,
|
def douban_credits(doubanid: str,
|
||||||
type_name: str,
|
type_name: str,
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ from fastapi import APIRouter, Depends
|
|||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.media import MediaChain
|
from app.chain.media import MediaChain
|
||||||
|
from app.chain.tmdb import TmdbChain
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.context import Context
|
from app.core.context import Context
|
||||||
|
from app.core.event import eventmanager
|
||||||
from app.core.metainfo import MetaInfo, MetaInfoPath
|
from app.core.metainfo import MetaInfo, MetaInfoPath
|
||||||
from app.core.security import verify_token, verify_apitoken
|
from app.core.security import verify_token, verify_apitoken
|
||||||
from app.schemas import MediaType
|
from app.schemas import MediaType, MediaRecognizeConvertEventData
|
||||||
|
from app.schemas.types import ChainEventType
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -72,6 +75,7 @@ def search(title: str,
|
|||||||
"""
|
"""
|
||||||
模糊搜索媒体/人物信息列表 media:媒体信息,person:人物信息
|
模糊搜索媒体/人物信息列表 media:媒体信息,person:人物信息
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __get_source(obj: Union[schemas.MediaInfo, schemas.MediaPerson, dict]):
|
def __get_source(obj: Union[schemas.MediaInfo, schemas.MediaPerson, dict]):
|
||||||
"""
|
"""
|
||||||
获取对象属性
|
获取对象属性
|
||||||
@@ -131,25 +135,90 @@ def category(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|||||||
return MediaChain().media_category() or {}
|
return MediaChain().media_category() or {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/seasons", summary="查询媒体季信息", response_model=List[schemas.MediaSeason])
|
||||||
|
def seasons(mediaid: str = None,
|
||||||
|
title: str = None,
|
||||||
|
year: int = None,
|
||||||
|
season: int = None,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
查询媒体季信息
|
||||||
|
"""
|
||||||
|
if mediaid:
|
||||||
|
if mediaid.startswith("tmdb:"):
|
||||||
|
tmdbid = int(mediaid[5:])
|
||||||
|
seasons_info = TmdbChain().tmdb_seasons(tmdbid=tmdbid)
|
||||||
|
if seasons_info:
|
||||||
|
if season:
|
||||||
|
return [sea for sea in seasons_info if sea.season_number == season]
|
||||||
|
return seasons_info
|
||||||
|
if title:
|
||||||
|
meta = MetaInfo(title)
|
||||||
|
if year:
|
||||||
|
meta.year = year
|
||||||
|
mediainfo = MediaChain().recognize_media(meta, mtype=MediaType.TV)
|
||||||
|
if mediainfo:
|
||||||
|
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
||||||
|
seasons_info = TmdbChain().tmdb_seasons(tmdbid=mediainfo.tmdb_id)
|
||||||
|
if seasons_info:
|
||||||
|
if season:
|
||||||
|
return [sea for sea in seasons_info if sea.season_number == season]
|
||||||
|
return seasons_info
|
||||||
|
else:
|
||||||
|
sea = season or 1
|
||||||
|
return schemas.MediaSeason(
|
||||||
|
season_number=sea,
|
||||||
|
poster_path=mediainfo.poster_path,
|
||||||
|
name=f"第 {sea} 季",
|
||||||
|
air_date=mediainfo.release_date,
|
||||||
|
overview=mediainfo.overview,
|
||||||
|
vote_average=mediainfo.vote_average,
|
||||||
|
episode_count=mediainfo.number_of_episodes
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo)
|
@router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo)
|
||||||
def media_info(mediaid: str, type_name: str,
|
def detail(mediaid: str, type_name: str, title: str = None, year: int = None,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据媒体ID查询themoviedb或豆瓣媒体信息,type_name: 电影/电视剧
|
根据媒体ID查询themoviedb或豆瓣媒体信息,type_name: 电影/电视剧
|
||||||
"""
|
"""
|
||||||
mtype = MediaType(type_name)
|
mtype = MediaType(type_name)
|
||||||
tmdbid, doubanid, bangumiid = None, None, None
|
mediainfo = None
|
||||||
if mediaid.startswith("tmdb:"):
|
if mediaid.startswith("tmdb:"):
|
||||||
tmdbid = int(mediaid[5:])
|
mediainfo = MediaChain().recognize_media(tmdbid=int(mediaid[5:]), mtype=mtype)
|
||||||
elif mediaid.startswith("douban:"):
|
elif mediaid.startswith("douban:"):
|
||||||
doubanid = mediaid[7:]
|
mediainfo = MediaChain().recognize_media(doubanid=mediaid[7:], mtype=mtype)
|
||||||
elif mediaid.startswith("bangumi:"):
|
elif mediaid.startswith("bangumi:"):
|
||||||
bangumiid = int(mediaid[8:])
|
mediainfo = MediaChain().recognize_media(bangumiid=int(mediaid[8:]), mtype=mtype)
|
||||||
if not tmdbid and not doubanid and not bangumiid:
|
else:
|
||||||
return schemas.MediaInfo()
|
# 广播事件解析媒体信息
|
||||||
|
event_data = MediaRecognizeConvertEventData(
|
||||||
|
mediaid=mediaid,
|
||||||
|
convert_type=settings.RECOGNIZE_SOURCE
|
||||||
|
)
|
||||||
|
event = eventmanager.send_event(ChainEventType.MediaRecognizeConvert, event_data)
|
||||||
|
# 使用事件返回的上下文数据
|
||||||
|
if event and event.event_data:
|
||||||
|
event_data: MediaRecognizeConvertEventData = event.event_data
|
||||||
|
if event_data.media_dict:
|
||||||
|
new_id = event_data.media_dict.get("id")
|
||||||
|
if event_data.convert_type == "themoviedb":
|
||||||
|
mediainfo = MediaChain().recognize_media(tmdbid=new_id, mtype=mtype)
|
||||||
|
elif event_data.convert_type == "douban":
|
||||||
|
mediainfo = MediaChain().recognize_media(doubanid=new_id, mtype=mtype)
|
||||||
|
elif title:
|
||||||
|
# 使用名称识别兜底
|
||||||
|
meta = MetaInfo(title)
|
||||||
|
if year:
|
||||||
|
meta.year = year
|
||||||
|
if mtype:
|
||||||
|
meta.type = mtype
|
||||||
|
mediainfo = MediaChain().recognize_media(meta=meta)
|
||||||
# 识别
|
# 识别
|
||||||
mediainfo = MediaChain().recognize_media(tmdbid=tmdbid, doubanid=doubanid, bangumiid=bangumiid, mtype=mtype)
|
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
MediaChain().obtain_images(mediainfo)
|
MediaChain().obtain_images(mediainfo)
|
||||||
return mediainfo.to_dict()
|
return mediainfo.to_dict()
|
||||||
|
|
||||||
return schemas.MediaInfo()
|
return schemas.MediaInfo()
|
||||||
|
|||||||
191
app/api/endpoints/recommend.py
Normal file
191
app/api/endpoints/recommend.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
|
from app.core.event import eventmanager
|
||||||
|
from app.core.security import verify_token
|
||||||
|
from app.schemas.types import ChainEventType
|
||||||
|
from chain.recommend import RecommendChain
|
||||||
|
from schemas import RecommendSourceEventData
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/source", summary="获取推荐数据源", response_model=List[schemas.RecommendMediaSource])
|
||||||
|
def source(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
获取推荐数据源
|
||||||
|
"""
|
||||||
|
# 广播事件,请示额外的推荐数据源支持
|
||||||
|
event_data = RecommendSourceEventData()
|
||||||
|
event = eventmanager.send_event(ChainEventType.RecommendSource, event_data)
|
||||||
|
# 使用事件返回的上下文数据
|
||||||
|
if event and event.event_data:
|
||||||
|
event_data: RecommendSourceEventData = event.event_data
|
||||||
|
if event_data.extra_sources:
|
||||||
|
return event_data.extra_sources
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/bangumi_calendar", summary="Bangumi每日放送", response_model=List[schemas.MediaInfo])
|
||||||
|
def bangumi_calendar(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览Bangumi每日放送
|
||||||
|
"""
|
||||||
|
return RecommendChain().bangumi_calendar(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_showing", summary="豆瓣正在热映", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_showing(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览豆瓣正在热映
|
||||||
|
"""
|
||||||
|
return RecommendChain().douban_movie_showing(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_movies", summary="豆瓣电影", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_movies(sort: str = "R",
|
||||||
|
tags: str = "",
|
||||||
|
page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览豆瓣电影信息
|
||||||
|
"""
|
||||||
|
return RecommendChain().douban_movies(sort=sort, tags=tags, page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_tvs", summary="豆瓣剧集", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_tvs(sort: str = "R",
|
||||||
|
tags: str = "",
|
||||||
|
page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览豆瓣剧集信息
|
||||||
|
"""
|
||||||
|
return RecommendChain().douban_tvs(sort=sort, tags=tags, page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_movie_top250", summary="豆瓣电影TOP250", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_movie_top250(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览豆瓣剧集信息
|
||||||
|
"""
|
||||||
|
return RecommendChain().douban_movie_top250(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_tv_weekly_chinese", summary="豆瓣国产剧集周榜", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_tv_weekly_chinese(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
中国每周剧集口碑榜
|
||||||
|
"""
|
||||||
|
return RecommendChain().douban_tv_weekly_chinese(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_tv_weekly_global", summary="豆瓣全球剧集周榜", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_tv_weekly_global(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
全球每周剧集口碑榜
|
||||||
|
"""
|
||||||
|
return RecommendChain().douban_tv_weekly_global(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_tv_animation", summary="豆瓣动画剧集", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_tv_animation(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
热门动画剧集
|
||||||
|
"""
|
||||||
|
return RecommendChain().douban_tv_animation(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_movie_hot", summary="豆瓣热门电影", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_movie_hot(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
热门电影
|
||||||
|
"""
|
||||||
|
return RecommendChain().douban_movie_hot(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/douban_tv_hot", summary="豆瓣热门电视剧", response_model=List[schemas.MediaInfo])
|
||||||
|
def douban_tv_hot(page: int = 1,
|
||||||
|
count: int = 30,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
热门电视剧
|
||||||
|
"""
|
||||||
|
return RecommendChain().douban_tv_hot(page=page, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tmdb_movies", summary="TMDB电影", response_model=List[schemas.MediaInfo])
|
||||||
|
def tmdb_movies(sort_by: str = "popularity.desc",
|
||||||
|
with_genres: str = "",
|
||||||
|
with_original_language: str = "",
|
||||||
|
with_keywords: str = "",
|
||||||
|
with_watch_providers: str = "",
|
||||||
|
vote_average: float = 0,
|
||||||
|
vote_count: int = 0,
|
||||||
|
release_date: str = "",
|
||||||
|
page: int = 1,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览TMDB电影信息
|
||||||
|
"""
|
||||||
|
return RecommendChain().tmdb_movies(sort_by=sort_by,
|
||||||
|
with_genres=with_genres,
|
||||||
|
with_original_language=with_original_language,
|
||||||
|
with_keywords=with_keywords,
|
||||||
|
with_watch_providers=with_watch_providers,
|
||||||
|
vote_average=vote_average,
|
||||||
|
vote_count=vote_count,
|
||||||
|
release_date=release_date,
|
||||||
|
page=page)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tmdb_tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo])
|
||||||
|
def tmdb_tvs(sort_by: str = "popularity.desc",
|
||||||
|
with_genres: str = "",
|
||||||
|
with_original_language: str = "",
|
||||||
|
with_keywords: str = "",
|
||||||
|
with_watch_providers: str = "",
|
||||||
|
vote_average: float = 0,
|
||||||
|
vote_count: int = 0,
|
||||||
|
release_date: str = "",
|
||||||
|
page: int = 1,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
浏览TMDB剧集信息
|
||||||
|
"""
|
||||||
|
return RecommendChain().tmdb_tvs(sort_by=sort_by,
|
||||||
|
with_genres=with_genres,
|
||||||
|
with_original_language=with_original_language,
|
||||||
|
with_keywords=with_keywords,
|
||||||
|
with_watch_providers=with_watch_providers,
|
||||||
|
vote_average=vote_average,
|
||||||
|
vote_count=vote_count,
|
||||||
|
release_date=release_date,
|
||||||
|
page=page)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tmdb_trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
|
||||||
|
def tmdb_trending(page: int = 1,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
TMDB流行趋势
|
||||||
|
"""
|
||||||
|
return RecommendChain().tmdb_trending(page=page)
|
||||||
@@ -6,8 +6,11 @@ from app import schemas
|
|||||||
from app.chain.media import MediaChain
|
from app.chain.media import MediaChain
|
||||||
from app.chain.search import SearchChain
|
from app.chain.search import SearchChain
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
from app.core.event import eventmanager
|
||||||
|
from app.core.metainfo import MetaInfo
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.schemas.types import MediaType
|
from app.schemas import MediaRecognizeConvertEventData
|
||||||
|
from app.schemas.types import MediaType, ChainEventType
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -25,6 +28,8 @@ def search_latest(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|||||||
def search_by_id(mediaid: str,
|
def search_by_id(mediaid: str,
|
||||||
mtype: str = None,
|
mtype: str = None,
|
||||||
area: str = "title",
|
area: str = "title",
|
||||||
|
title: str = None,
|
||||||
|
year: int = None,
|
||||||
season: str = None,
|
season: str = None,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
@@ -34,6 +39,8 @@ def search_by_id(mediaid: str,
|
|||||||
mtype = MediaType(mtype)
|
mtype = MediaType(mtype)
|
||||||
if season:
|
if season:
|
||||||
season = int(season)
|
season = int(season)
|
||||||
|
torrents = None
|
||||||
|
# 根据前缀识别媒体ID
|
||||||
if mediaid.startswith("tmdb:"):
|
if mediaid.startswith("tmdb:"):
|
||||||
tmdbid = int(mediaid.replace("tmdb:", ""))
|
tmdbid = int(mediaid.replace("tmdb:", ""))
|
||||||
if settings.RECOGNIZE_SOURCE == "douban":
|
if settings.RECOGNIZE_SOURCE == "douban":
|
||||||
@@ -79,8 +86,44 @@ def search_by_id(mediaid: str,
|
|||||||
else:
|
else:
|
||||||
return schemas.Response(success=False, message="未识别到豆瓣媒体信息")
|
return schemas.Response(success=False, message="未识别到豆瓣媒体信息")
|
||||||
else:
|
else:
|
||||||
return schemas.Response(success=False, message="未知的媒体ID")
|
# 未知前缀,广播事件解析媒体信息
|
||||||
|
event_data = MediaRecognizeConvertEventData(
|
||||||
|
mediaid=mediaid,
|
||||||
|
convert_type=settings.RECOGNIZE_SOURCE
|
||||||
|
)
|
||||||
|
event = eventmanager.send_event(ChainEventType.MediaRecognizeConvert, event_data)
|
||||||
|
# 使用事件返回的上下文数据
|
||||||
|
if event and event.event_data:
|
||||||
|
event_data: MediaRecognizeConvertEventData = event.event_data
|
||||||
|
if event_data.media_dict:
|
||||||
|
search_id = event_data.media_dict.get("id")
|
||||||
|
if event_data.convert_type == "themoviedb":
|
||||||
|
torrents = SearchChain().search_by_id(tmdbid=search_id,
|
||||||
|
mtype=mtype, area=area, season=season)
|
||||||
|
elif event_data.convert_type == "douban":
|
||||||
|
torrents = SearchChain().search_by_id(doubanid=search_id,
|
||||||
|
mtype=mtype, area=area, season=season)
|
||||||
|
else:
|
||||||
|
if not title:
|
||||||
|
return schemas.Response(success=False, message="未知的媒体ID")
|
||||||
|
# 使用名称识别兜底
|
||||||
|
meta = MetaInfo(title)
|
||||||
|
if year:
|
||||||
|
meta.year = year
|
||||||
|
if mtype:
|
||||||
|
meta.type = mtype
|
||||||
|
if season:
|
||||||
|
meta.type = MediaType.TV
|
||||||
|
meta.begin_season = season
|
||||||
|
mediainfo = MediaChain().recognize_media(meta=meta)
|
||||||
|
if mediainfo:
|
||||||
|
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
||||||
|
torrents = SearchChain().search_by_id(tmdbid=mediainfo.tmdb_id,
|
||||||
|
mtype=mtype, area=area, season=season)
|
||||||
|
else:
|
||||||
|
torrents = SearchChain().search_by_id(doubanid=mediainfo.douban_id,
|
||||||
|
mtype=mtype, area=area, season=season)
|
||||||
|
# 返回搜索结果
|
||||||
if not torrents:
|
if not torrents:
|
||||||
return schemas.Response(success=False, message="未搜索到任何资源")
|
return schemas.Response(success=False, message="未搜索到任何资源")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ def create_subscribe(
|
|||||||
season=subscribe_in.season,
|
season=subscribe_in.season,
|
||||||
doubanid=subscribe_in.doubanid,
|
doubanid=subscribe_in.doubanid,
|
||||||
bangumiid=subscribe_in.bangumiid,
|
bangumiid=subscribe_in.bangumiid,
|
||||||
|
mediaid=subscribe_in.mediaid,
|
||||||
username=current_user.name,
|
username=current_user.name,
|
||||||
best_version=subscribe_in.best_version,
|
best_version=subscribe_in.best_version,
|
||||||
save_path=subscribe_in.save_path,
|
save_path=subscribe_in.save_path,
|
||||||
@@ -109,6 +110,7 @@ def update_subscribe(
|
|||||||
if not subscribe:
|
if not subscribe:
|
||||||
return schemas.Response(success=False, message="订阅不存在")
|
return schemas.Response(success=False, message="订阅不存在")
|
||||||
# 避免更新缺失集数
|
# 避免更新缺失集数
|
||||||
|
old_subscribe_dict = subscribe.to_dict()
|
||||||
subscribe_dict = subscribe_in.dict()
|
subscribe_dict = subscribe_in.dict()
|
||||||
if not subscribe_in.lack_episode:
|
if not subscribe_in.lack_episode:
|
||||||
# 没有缺失集数时,缺失集数清空,避免更新为0
|
# 没有缺失集数时,缺失集数清空,避免更新为0
|
||||||
@@ -126,7 +128,8 @@ def update_subscribe(
|
|||||||
# 发送订阅调整事件
|
# 发送订阅调整事件
|
||||||
eventmanager.send_event(EventType.SubscribeModified, {
|
eventmanager.send_event(EventType.SubscribeModified, {
|
||||||
"subscribe_id": subscribe.id,
|
"subscribe_id": subscribe.id,
|
||||||
"subscribe_info": subscribe_dict,
|
"old_subscribe_info": old_subscribe_dict,
|
||||||
|
"subscribe_info": subscribe.to_dict(),
|
||||||
})
|
})
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
@@ -146,9 +149,16 @@ def update_subscribe_status(
|
|||||||
valid_states = ["R", "P", "S"]
|
valid_states = ["R", "P", "S"]
|
||||||
if state not in valid_states:
|
if state not in valid_states:
|
||||||
return schemas.Response(success=False, message="无效的订阅状态")
|
return schemas.Response(success=False, message="无效的订阅状态")
|
||||||
|
old_subscribe_dict = subscribe.to_dict()
|
||||||
subscribe.update(db, {
|
subscribe.update(db, {
|
||||||
"state": state
|
"state": state
|
||||||
})
|
})
|
||||||
|
# 发送订阅调整事件
|
||||||
|
eventmanager.send_event(EventType.SubscribeModified, {
|
||||||
|
"subscribe_id": subscribe.id,
|
||||||
|
"old_subscribe_info": old_subscribe_dict,
|
||||||
|
"subscribe_info": subscribe.to_dict(),
|
||||||
|
})
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -162,7 +172,6 @@ def subscribe_mediaid(
|
|||||||
"""
|
"""
|
||||||
根据 TMDBID/豆瓣ID/BangumiId 查询订阅 tmdb:/douban:
|
根据 TMDBID/豆瓣ID/BangumiId 查询订阅 tmdb:/douban:
|
||||||
"""
|
"""
|
||||||
result = None
|
|
||||||
title_check = False
|
title_check = False
|
||||||
if mediaid.startswith("tmdb:"):
|
if mediaid.startswith("tmdb:"):
|
||||||
tmdbid = mediaid[5:]
|
tmdbid = mediaid[5:]
|
||||||
@@ -183,6 +192,10 @@ def subscribe_mediaid(
|
|||||||
result = Subscribe.get_by_bangumiid(db, int(bangumiid))
|
result = Subscribe.get_by_bangumiid(db, int(bangumiid))
|
||||||
if not result and title:
|
if not result and title:
|
||||||
title_check = True
|
title_check = True
|
||||||
|
else:
|
||||||
|
result = Subscribe.get_by_mediaid(db, mediaid)
|
||||||
|
if not result and title:
|
||||||
|
title_check = True
|
||||||
# 使用名称检查订阅
|
# 使用名称检查订阅
|
||||||
if title_check and title:
|
if title_check and title:
|
||||||
meta = MetaInfo(title)
|
meta = MetaInfo(title)
|
||||||
@@ -213,11 +226,18 @@ def reset_subscribes(
|
|||||||
"""
|
"""
|
||||||
subscribe = Subscribe.get(db, subid)
|
subscribe = Subscribe.get(db, subid)
|
||||||
if subscribe:
|
if subscribe:
|
||||||
|
old_subscribe_dict = subscribe.to_dict()
|
||||||
subscribe.update(db, {
|
subscribe.update(db, {
|
||||||
"note": [],
|
"note": [],
|
||||||
"lack_episode": subscribe.total_episode,
|
"lack_episode": subscribe.total_episode,
|
||||||
"state": "R"
|
"state": "R"
|
||||||
})
|
})
|
||||||
|
# 发送订阅调整事件
|
||||||
|
eventmanager.send_event(EventType.SubscribeModified, {
|
||||||
|
"subscribe_id": subscribe.id,
|
||||||
|
"old_subscribe_info": old_subscribe_dict,
|
||||||
|
"subscribe_info": subscribe.to_dict(),
|
||||||
|
})
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
return schemas.Response(success=False, message="订阅不存在")
|
return schemas.Response(success=False, message="订阅不存在")
|
||||||
|
|
||||||
@@ -295,6 +315,10 @@ def delete_subscribe_by_mediaid(
|
|||||||
subscribe = Subscribe().get_by_doubanid(db, doubanid)
|
subscribe = Subscribe().get_by_doubanid(db, doubanid)
|
||||||
if subscribe:
|
if subscribe:
|
||||||
delete_subscribes.append(subscribe)
|
delete_subscribes.append(subscribe)
|
||||||
|
else:
|
||||||
|
subscribe = Subscribe().get_by_mediaid(db, mediaid)
|
||||||
|
if subscribe:
|
||||||
|
delete_subscribes.append(subscribe)
|
||||||
for subscribe in delete_subscribes:
|
for subscribe in delete_subscribes:
|
||||||
Subscribe().delete(db, subscribe.id)
|
Subscribe().delete(db, subscribe.id)
|
||||||
# 发送事件
|
# 发送事件
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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.recommend import RecommendChain
|
|
||||||
from app.chain.tmdb import TmdbChain
|
from app.chain.tmdb import TmdbChain
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.schemas.types import MediaType
|
from app.schemas.types import MediaType
|
||||||
@@ -114,45 +113,6 @@ def tmdb_person_credits(person_id: int,
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
@router.get("/movies", summary="TMDB电影", response_model=List[schemas.MediaInfo])
|
|
||||||
def tmdb_movies(sort_by: str = "popularity.desc",
|
|
||||||
with_genres: str = "",
|
|
||||||
with_original_language: str = "",
|
|
||||||
page: int = 1,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
浏览TMDB电影信息
|
|
||||||
"""
|
|
||||||
return RecommendChain().tmdb_movies(sort_by=sort_by,
|
|
||||||
with_genres=with_genres,
|
|
||||||
with_original_language=with_original_language,
|
|
||||||
page=page)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/tvs", summary="TMDB剧集", response_model=List[schemas.MediaInfo])
|
|
||||||
def tmdb_tvs(sort_by: str = "popularity.desc",
|
|
||||||
with_genres: str = "",
|
|
||||||
with_original_language: str = "",
|
|
||||||
page: int = 1,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
浏览TMDB剧集信息
|
|
||||||
"""
|
|
||||||
return RecommendChain().tmdb_tvs(sort_by=sort_by,
|
|
||||||
with_genres=with_genres,
|
|
||||||
with_original_language=with_original_language,
|
|
||||||
page=page)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/trending", summary="TMDB流行趋势", response_model=List[schemas.MediaInfo])
|
|
||||||
def tmdb_trending(page: int = 1,
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|
||||||
"""
|
|
||||||
TMDB流行趋势
|
|
||||||
"""
|
|
||||||
return RecommendChain().tmdb_trending(page=page)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode])
|
@router.get("/{tmdbid}/{season}", summary="TMDB季所有集", response_model=List[schemas.TmdbEpisode])
|
||||||
def tmdb_season_episodes(tmdbid: int, season: int,
|
def tmdb_season_episodes(tmdbid: int, season: int,
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ class BangumiChain(ChainBase, metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
return self.run_module("bangumi_calendar")
|
return self.run_module("bangumi_calendar")
|
||||||
|
|
||||||
|
def discover(self, **kwargs) -> Optional[List[MediaInfo]]:
|
||||||
|
"""
|
||||||
|
发现Bangumi番剧
|
||||||
|
"""
|
||||||
|
return self.run_module("bangumi_discover", **kwargs)
|
||||||
|
|
||||||
def bangumi_info(self, bangumiid: int) -> Optional[dict]:
|
def bangumi_info(self, bangumiid: int) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
获取Bangumi信息
|
获取Bangumi信息
|
||||||
|
|||||||
@@ -444,7 +444,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
for file in files:
|
for file in files:
|
||||||
self.scrape_metadata(fileitem=file,
|
self.scrape_metadata(fileitem=file,
|
||||||
meta=meta, mediainfo=mediainfo,
|
meta=meta, mediainfo=mediainfo,
|
||||||
init_folder=False, parent=fileitem)
|
init_folder=False, parent=fileitem,
|
||||||
|
overwrite=overwrite)
|
||||||
# 生成目录内图片文件
|
# 生成目录内图片文件
|
||||||
if init_folder:
|
if init_folder:
|
||||||
# 图片
|
# 图片
|
||||||
@@ -515,7 +516,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
self.scrape_metadata(fileitem=file,
|
self.scrape_metadata(fileitem=file,
|
||||||
meta=meta, mediainfo=mediainfo,
|
meta=meta, mediainfo=mediainfo,
|
||||||
parent=fileitem if file.type == "file" else None,
|
parent=fileitem if file.type == "file" else None,
|
||||||
init_folder=True if file.type == "dir" else False)
|
init_folder=True if file.type == "dir" else False,
|
||||||
|
overwrite=overwrite)
|
||||||
# 生成目录的nfo和图片
|
# 生成目录的nfo和图片
|
||||||
if init_folder:
|
if init_folder:
|
||||||
# 识别文件夹名称
|
# 识别文件夹名称
|
||||||
|
|||||||
@@ -295,6 +295,8 @@ class MessageChain(ChainBase):
|
|||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
best_version = True
|
best_version = True
|
||||||
|
# 转换用户名
|
||||||
|
mp_name = self.useroper.get_name(**{f"{channel.name.lower()}_userid": userid}) if channel else None
|
||||||
# 添加订阅,状态为N
|
# 添加订阅,状态为N
|
||||||
self.subscribechain.add(title=mediainfo.title,
|
self.subscribechain.add(title=mediainfo.title,
|
||||||
year=mediainfo.year,
|
year=mediainfo.year,
|
||||||
@@ -304,7 +306,7 @@ class MessageChain(ChainBase):
|
|||||||
channel=channel,
|
channel=channel,
|
||||||
source=source,
|
source=source,
|
||||||
userid=userid,
|
userid=userid,
|
||||||
username=username,
|
username=mp_name or username,
|
||||||
best_version=best_version)
|
best_version=best_version)
|
||||||
elif cache_type == "Torrent":
|
elif cache_type == "Torrent":
|
||||||
if int(text) == 0:
|
if int(text) == 0:
|
||||||
@@ -505,6 +507,8 @@ class MessageChain(ChainBase):
|
|||||||
note = downloaded
|
note = downloaded
|
||||||
else:
|
else:
|
||||||
note = None
|
note = None
|
||||||
|
# 转换用户名
|
||||||
|
mp_name = self.useroper.get_name(**{f"{channel.name.lower()}_userid": userid}) if channel else None
|
||||||
# 添加订阅,状态为R
|
# 添加订阅,状态为R
|
||||||
self.subscribechain.add(title=_current_media.title,
|
self.subscribechain.add(title=_current_media.title,
|
||||||
year=_current_media.year,
|
year=_current_media.year,
|
||||||
@@ -514,7 +518,7 @@ class MessageChain(ChainBase):
|
|||||||
channel=channel,
|
channel=channel,
|
||||||
source=source,
|
source=source,
|
||||||
userid=userid,
|
userid=userid,
|
||||||
username=username,
|
username=mp_name or username,
|
||||||
state="R",
|
state="R",
|
||||||
note=note)
|
note=note)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import io
|
import io
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, List
|
from typing import List
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -157,8 +157,15 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def tmdb_movies(self, sort_by: str = "popularity.desc", with_genres: str = "",
|
def tmdb_movies(self, sort_by: str = "popularity.desc",
|
||||||
with_original_language: str = "", page: int = 1) -> Any:
|
with_genres: str = "",
|
||||||
|
with_original_language: str = "",
|
||||||
|
with_keywords: str = "",
|
||||||
|
with_watch_providers: str = "",
|
||||||
|
vote_average: float = 0,
|
||||||
|
vote_count: int = 0,
|
||||||
|
release_date: str = "",
|
||||||
|
page: int = 1) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
TMDB热门电影
|
TMDB热门电影
|
||||||
"""
|
"""
|
||||||
@@ -166,13 +173,25 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
|
with_keywords=with_keywords,
|
||||||
|
with_watch_providers=with_watch_providers,
|
||||||
|
vote_average=vote_average,
|
||||||
|
vote_count=vote_count,
|
||||||
|
release_date=release_date,
|
||||||
page=page)
|
page=page)
|
||||||
return [movie.to_dict() for movie in movies] if movies else []
|
return [movie.to_dict() for movie in movies] if movies else []
|
||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def tmdb_tvs(self, sort_by: str = "popularity.desc", with_genres: str = "",
|
def tmdb_tvs(self, sort_by: str = "popularity.desc",
|
||||||
with_original_language: str = "zh|en|ja|ko", page: int = 1) -> Any:
|
with_genres: str = "",
|
||||||
|
with_original_language: str = "zh|en|ja|ko",
|
||||||
|
with_keywords: str = "",
|
||||||
|
with_watch_providers: str = "",
|
||||||
|
vote_average: float = 0,
|
||||||
|
vote_count: int = 0,
|
||||||
|
release_date: str = "",
|
||||||
|
page: int = 1) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
TMDB热门电视剧
|
TMDB热门电视剧
|
||||||
"""
|
"""
|
||||||
@@ -180,12 +199,17 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
sort_by=sort_by,
|
sort_by=sort_by,
|
||||||
with_genres=with_genres,
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
|
with_keywords=with_keywords,
|
||||||
|
with_watch_providers=with_watch_providers,
|
||||||
|
vote_average=vote_average,
|
||||||
|
vote_count=vote_count,
|
||||||
|
release_date=release_date,
|
||||||
page=page)
|
page=page)
|
||||||
return [tv.to_dict() for tv in tvs] if tvs else []
|
return [tv.to_dict() for tv in tvs] if tvs else []
|
||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def tmdb_trending(self, page: int = 1) -> Any:
|
def tmdb_trending(self, page: int = 1) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
TMDB流行趋势
|
TMDB流行趋势
|
||||||
"""
|
"""
|
||||||
@@ -194,7 +218,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def bangumi_calendar(self, page: int = 1, count: int = 30) -> Any:
|
def bangumi_calendar(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Bangumi每日放送
|
Bangumi每日放送
|
||||||
"""
|
"""
|
||||||
@@ -203,7 +227,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def douban_movie_showing(self, page: int = 1, count: int = 30) -> Any:
|
def douban_movie_showing(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
豆瓣正在热映
|
豆瓣正在热映
|
||||||
"""
|
"""
|
||||||
@@ -212,7 +236,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def douban_movies(self, sort: str = "R", tags: str = "", page: int = 1, count: int = 30) -> Any:
|
def douban_movies(self, sort: str = "R", tags: str = "", page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
豆瓣最新电影
|
豆瓣最新电影
|
||||||
"""
|
"""
|
||||||
@@ -222,7 +246,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def douban_tvs(self, sort: str = "R", tags: str = "", page: int = 1, count: int = 30) -> Any:
|
def douban_tvs(self, sort: str = "R", tags: str = "", page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
豆瓣最新电视剧
|
豆瓣最新电视剧
|
||||||
"""
|
"""
|
||||||
@@ -232,7 +256,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def douban_movie_top250(self, page: int = 1, count: int = 30) -> Any:
|
def douban_movie_top250(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
豆瓣电影TOP250
|
豆瓣电影TOP250
|
||||||
"""
|
"""
|
||||||
@@ -241,7 +265,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def douban_tv_weekly_chinese(self, page: int = 1, count: int = 30) -> Any:
|
def douban_tv_weekly_chinese(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
豆瓣国产剧集榜
|
豆瓣国产剧集榜
|
||||||
"""
|
"""
|
||||||
@@ -250,7 +274,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def douban_tv_weekly_global(self, page: int = 1, count: int = 30) -> Any:
|
def douban_tv_weekly_global(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
豆瓣全球剧集榜
|
豆瓣全球剧集榜
|
||||||
"""
|
"""
|
||||||
@@ -259,7 +283,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def douban_tv_animation(self, page: int = 1, count: int = 30) -> Any:
|
def douban_tv_animation(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
豆瓣热门动漫
|
豆瓣热门动漫
|
||||||
"""
|
"""
|
||||||
@@ -268,7 +292,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def douban_movie_hot(self, page: int = 1, count: int = 30) -> Any:
|
def douban_movie_hot(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
豆瓣热门电影
|
豆瓣热门电影
|
||||||
"""
|
"""
|
||||||
@@ -277,7 +301,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
@log_execution_time(logger=logger)
|
@log_execution_time(logger=logger)
|
||||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||||
def douban_tv_hot(self, page: int = 1, count: int = 30) -> Any:
|
def douban_tv_hot(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
豆瓣热门电视剧
|
豆瓣热门电视剧
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class SearchChain(ChainBase):
|
|||||||
def search_by_id(self, tmdbid: int = None, doubanid: str = None,
|
def search_by_id(self, tmdbid: int = None, doubanid: str = None,
|
||||||
mtype: MediaType = None, area: str = "title", season: int = None) -> List[Context]:
|
mtype: MediaType = None, area: str = "title", season: int = None) -> List[Context]:
|
||||||
"""
|
"""
|
||||||
根据TMDBID/豆瓣ID搜索资源,精确匹配,但不不过滤本地存在的资源
|
根据TMDBID/豆瓣ID搜索资源,精确匹配,不过滤本地存在的资源
|
||||||
:param tmdbid: TMDB ID
|
:param tmdbid: TMDB ID
|
||||||
:param doubanid: 豆瓣 ID
|
:param doubanid: 豆瓣 ID
|
||||||
:param mtype: 媒体,电影 or 电视剧
|
:param mtype: 媒体,电影 or 电视剧
|
||||||
|
|||||||
@@ -138,10 +138,14 @@ class SiteChain(ChainBase):
|
|||||||
proxies=settings.PROXY if site.proxy else None,
|
proxies=settings.PROXY if site.proxy else None,
|
||||||
timeout=site.timeout or 15
|
timeout=site.timeout or 15
|
||||||
).get_res(url=site.url)
|
).get_res(url=site.url)
|
||||||
if res and res.status_code == 200:
|
if res is None:
|
||||||
|
return False, "无法打开网站!"
|
||||||
|
if res.status_code == 200:
|
||||||
csrf_token = re.search(r'<meta name="x-csrf-token" content="(.+?)">', res.text)
|
csrf_token = re.search(r'<meta name="x-csrf-token" content="(.+?)">', res.text)
|
||||||
if csrf_token:
|
if csrf_token:
|
||||||
token = csrf_token.group(1)
|
token = csrf_token.group(1)
|
||||||
|
else:
|
||||||
|
return False, f"错误:{res.status_code} {res.reason}"
|
||||||
if not token:
|
if not token:
|
||||||
return False, "无法获取Token"
|
return False, "无法获取Token"
|
||||||
# 调用查询用户信息接口
|
# 调用查询用户信息接口
|
||||||
@@ -155,11 +159,15 @@ class SiteChain(ChainBase):
|
|||||||
proxies=settings.PROXY if site.proxy else None,
|
proxies=settings.PROXY if site.proxy else None,
|
||||||
timeout=site.timeout or 15
|
timeout=site.timeout or 15
|
||||||
).get_res(url=f"{site.url}api/user/getInfo")
|
).get_res(url=f"{site.url}api/user/getInfo")
|
||||||
if user_res and user_res.status_code == 200:
|
if user_res is None:
|
||||||
|
return False, "无法打开网站!"
|
||||||
|
if user_res.status_code == 200:
|
||||||
user_info = user_res.json()
|
user_info = user_res.json()
|
||||||
if user_info and user_info.get("data"):
|
if user_info and user_info.get("data"):
|
||||||
return True, "连接成功"
|
return True, "连接成功"
|
||||||
return False, "Cookie已失效"
|
return False, "Cookie已失效"
|
||||||
|
else:
|
||||||
|
return False, f"错误:{user_res.status_code} {user_res.reason}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __mteam_test(site: Site) -> Tuple[bool, str]:
|
def __mteam_test(site: Site) -> Tuple[bool, str]:
|
||||||
@@ -182,9 +190,11 @@ class SiteChain(ChainBase):
|
|||||||
proxies=settings.PROXY if site.proxy else None,
|
proxies=settings.PROXY if site.proxy else None,
|
||||||
timeout=site.timeout or 15
|
timeout=site.timeout or 15
|
||||||
).post_res(url=url)
|
).post_res(url=url)
|
||||||
state = False
|
if res is None:
|
||||||
message = "鉴权已过期或无效"
|
return False, "无法打开网站!"
|
||||||
if res and res.status_code == 200:
|
if res.status_code == 200:
|
||||||
|
state = False
|
||||||
|
message = "鉴权已过期或无效"
|
||||||
user_info = res.json() or {}
|
user_info = res.json() or {}
|
||||||
if user_info.get("data"):
|
if user_info.get("data"):
|
||||||
# 更新最后访问时间
|
# 更新最后访问时间
|
||||||
@@ -203,7 +213,9 @@ class SiteChain(ChainBase):
|
|||||||
elif user_info.get("message"):
|
elif user_info.get("message"):
|
||||||
# 使用馒头的错误提示
|
# 使用馒头的错误提示
|
||||||
message = user_info.get("message")
|
message = user_info.get("message")
|
||||||
return state, message
|
return state, message
|
||||||
|
else:
|
||||||
|
return False, f"错误:{res.status_code} {res.reason}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __yema_test(site: Site) -> Tuple[bool, str]:
|
def __yema_test(site: Site) -> Tuple[bool, str]:
|
||||||
@@ -223,11 +235,15 @@ class SiteChain(ChainBase):
|
|||||||
proxies=settings.PROXY if site.proxy else None,
|
proxies=settings.PROXY if site.proxy else None,
|
||||||
timeout=site.timeout or 15
|
timeout=site.timeout or 15
|
||||||
).get_res(url=url)
|
).get_res(url=url)
|
||||||
if res and res.status_code == 200:
|
if res is None:
|
||||||
|
return False, "无法打开网站!"
|
||||||
|
if res.status_code == 200:
|
||||||
user_info = res.json()
|
user_info = res.json()
|
||||||
if user_info and user_info.get("success"):
|
if user_info and user_info.get("success"):
|
||||||
return True, "连接成功"
|
return True, "连接成功"
|
||||||
return False, "Cookie已过期"
|
return False, "Cookie已过期"
|
||||||
|
else:
|
||||||
|
return False, f"错误:{res.status_code} {res.reason}"
|
||||||
|
|
||||||
def __indexphp_test(self, site: Site) -> Tuple[bool, str]:
|
def __indexphp_test(self, site: Site) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
@@ -557,12 +573,12 @@ class SiteChain(ChainBase):
|
|||||||
elif res.status_code == 200:
|
elif res.status_code == 200:
|
||||||
msg = "Cookie已失效"
|
msg = "Cookie已失效"
|
||||||
else:
|
else:
|
||||||
msg = f"状态码:{res.status_code}"
|
msg = f"错误:{res.status_code} {res.reason}"
|
||||||
return False, f"{msg}!"
|
return False, f"{msg}!"
|
||||||
elif public and res.status_code != 200:
|
elif public and res.status_code != 200:
|
||||||
return False, f"状态码:{res.status_code}!"
|
return False, f"错误:{res.status_code} {res.reason}!"
|
||||||
elif res is not None:
|
elif res is not None:
|
||||||
return False, f"状态码:{res.status_code}!"
|
return False, f"错误:{res.status_code} {res.reason}!"
|
||||||
else:
|
else:
|
||||||
return False, f"无法打开网站!"
|
return False, f"无法打开网站!"
|
||||||
return True, "连接成功"
|
return True, "连接成功"
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ from app.helper.message import MessageHelper
|
|||||||
from app.helper.subscribe import SubscribeHelper
|
from app.helper.subscribe import SubscribeHelper
|
||||||
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.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType
|
from app.schemas import MediaRecognizeConvertEventData
|
||||||
|
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType, ChainEventType
|
||||||
from app.utils.singleton import Singleton
|
from app.utils.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
|||||||
tmdbid: int = None,
|
tmdbid: int = None,
|
||||||
doubanid: str = None,
|
doubanid: str = None,
|
||||||
bangumiid: int = None,
|
bangumiid: int = None,
|
||||||
|
mediaid: str = None,
|
||||||
season: int = None,
|
season: int = None,
|
||||||
channel: MessageChannel = None,
|
channel: MessageChannel = None,
|
||||||
source: str = None,
|
source: str = None,
|
||||||
@@ -69,7 +71,29 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
识别媒体信息并添加订阅
|
识别媒体信息并添加订阅
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __get_event_meida(_mediaid: str, _meta: MetaBase) -> Optional[MediaInfo]:
|
||||||
|
"""
|
||||||
|
广播事件解析媒体信息
|
||||||
|
"""
|
||||||
|
event_data = MediaRecognizeConvertEventData(
|
||||||
|
mediaid=_mediaid,
|
||||||
|
convert_type=settings.RECOGNIZE_SOURCE
|
||||||
|
)
|
||||||
|
event = eventmanager.send_event(ChainEventType.MediaRecognizeConvert, event_data)
|
||||||
|
# 使用事件返回的上下文数据
|
||||||
|
if event and event.event_data:
|
||||||
|
event_data: MediaRecognizeConvertEventData = event.event_data
|
||||||
|
if event_data.media_dict:
|
||||||
|
new_id = event_data.media_dict.get("id")
|
||||||
|
if event_data.convert_type == "themoviedb":
|
||||||
|
return self.mediachain.recognize_media(meta=_meta, tmdbid=new_id)
|
||||||
|
elif event_data.convert_type == "douban":
|
||||||
|
return self.mediachain.recognize_media(meta=_meta, doubanid=new_id)
|
||||||
|
return None
|
||||||
|
|
||||||
logger.info(f'开始添加订阅,标题:{title} ...')
|
logger.info(f'开始添加订阅,标题:{title} ...')
|
||||||
|
|
||||||
mediainfo = None
|
mediainfo = None
|
||||||
metainfo = MetaInfo(title)
|
metainfo = MetaInfo(title)
|
||||||
if year:
|
if year:
|
||||||
@@ -82,27 +106,41 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
|||||||
# 识别媒体信息
|
# 识别媒体信息
|
||||||
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
||||||
# TMDB识别模式
|
# TMDB识别模式
|
||||||
if not tmdbid and doubanid:
|
if not tmdbid:
|
||||||
# 将豆瓣信息转换为TMDB信息
|
if doubanid:
|
||||||
tmdbinfo = self.mediachain.get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype)
|
# 将豆瓣信息转换为TMDB信息
|
||||||
if tmdbinfo:
|
tmdbinfo = self.mediachain.get_tmdbinfo_by_doubanid(doubanid=doubanid, mtype=mtype)
|
||||||
mediainfo = MediaInfo(tmdb_info=tmdbinfo)
|
if tmdbinfo:
|
||||||
|
mediainfo = MediaInfo(tmdb_info=tmdbinfo)
|
||||||
|
elif mediaid:
|
||||||
|
# 未知前缀,广播事件解析媒体信息
|
||||||
|
mediainfo = __get_event_meida(mediaid, metainfo)
|
||||||
else:
|
else:
|
||||||
# 识别TMDB信息,不使用缓存
|
# 使用TMDBID识别
|
||||||
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid, cache=False)
|
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, tmdbid=tmdbid, cache=False)
|
||||||
else:
|
else:
|
||||||
# 豆瓣识别模式,不使用缓存
|
if doubanid:
|
||||||
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, doubanid=doubanid, cache=False)
|
# 豆瓣识别模式,不使用缓存
|
||||||
|
mediainfo = self.recognize_media(meta=metainfo, mtype=mtype, doubanid=doubanid, cache=False)
|
||||||
|
elif mediaid:
|
||||||
|
# 未知前缀,广播事件解析媒体信息
|
||||||
|
mediainfo = __get_event_meida(mediaid, metainfo)
|
||||||
if mediainfo:
|
if mediainfo:
|
||||||
# 豆瓣标题处理
|
# 豆瓣标题处理
|
||||||
meta = MetaInfo(mediainfo.title)
|
meta = MetaInfo(mediainfo.title)
|
||||||
mediainfo.title = meta.name
|
mediainfo.title = meta.name
|
||||||
if not season:
|
if not season:
|
||||||
season = meta.begin_season
|
season = meta.begin_season
|
||||||
|
|
||||||
|
# 使用名称识别兜底
|
||||||
|
if not mediainfo:
|
||||||
|
mediainfo = self.recognize_media(meta=metainfo)
|
||||||
|
|
||||||
# 识别失败
|
# 识别失败
|
||||||
if not mediainfo:
|
if not mediainfo:
|
||||||
logger.warn(f'未识别到媒体信息,标题:{title},tmdbid:{tmdbid},doubanid:{doubanid}')
|
logger.warn(f'未识别到媒体信息,标题:{title},tmdbid:{tmdbid},doubanid:{doubanid}')
|
||||||
return None, "未识别到媒体信息"
|
return None, "未识别到媒体信息"
|
||||||
|
|
||||||
# 总集数
|
# 总集数
|
||||||
if mediainfo.type == MediaType.TV:
|
if mediainfo.type == MediaType.TV:
|
||||||
if not season:
|
if not season:
|
||||||
@@ -137,6 +175,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
|||||||
else:
|
else:
|
||||||
# 避免season为0的问题
|
# 避免season为0的问题
|
||||||
season = None
|
season = None
|
||||||
|
|
||||||
# 更新媒体图片
|
# 更新媒体图片
|
||||||
self.obtain_images(mediainfo=mediainfo)
|
self.obtain_images(mediainfo=mediainfo)
|
||||||
# 合并信息
|
# 合并信息
|
||||||
@@ -144,6 +183,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
|||||||
mediainfo.douban_id = doubanid
|
mediainfo.douban_id = doubanid
|
||||||
if bangumiid:
|
if bangumiid:
|
||||||
mediainfo.bangumi_id = bangumiid
|
mediainfo.bangumi_id = bangumiid
|
||||||
|
|
||||||
# 添加订阅
|
# 添加订阅
|
||||||
kwargs.update({
|
kwargs.update({
|
||||||
'quality': self.__get_default_subscribe_config(mediainfo.type, "quality") if not kwargs.get(
|
'quality': self.__get_default_subscribe_config(mediainfo.type, "quality") if not kwargs.get(
|
||||||
@@ -165,7 +205,9 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
|||||||
'downloader': self.__get_default_subscribe_config(mediainfo.type, "downloader") if not kwargs.get(
|
'downloader': self.__get_default_subscribe_config(mediainfo.type, "downloader") if not kwargs.get(
|
||||||
"downloader") else kwargs.get("downloader"),
|
"downloader") else kwargs.get("downloader"),
|
||||||
'save_path': self.__get_default_subscribe_config(mediainfo.type, "save_path") if not kwargs.get(
|
'save_path': self.__get_default_subscribe_config(mediainfo.type, "save_path") if not kwargs.get(
|
||||||
"save_path") else kwargs.get("save_path")
|
"save_path") else kwargs.get("save_path"),
|
||||||
|
'filter_groups': self.__get_default_subscribe_config(mediainfo.type, "filter_groups") if not kwargs.get(
|
||||||
|
"filter_groups") else kwargs.get("filter_groups"),
|
||||||
})
|
})
|
||||||
sid, err_msg = self.subscribeoper.add(mediainfo=mediainfo, season=season, username=username, **kwargs)
|
sid, err_msg = self.subscribeoper.add(mediainfo=mediainfo, season=season, username=username, **kwargs)
|
||||||
if not sid:
|
if not sid:
|
||||||
@@ -798,6 +840,11 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
|||||||
doubanid=share_sub.get("doubanid"),
|
doubanid=share_sub.get("doubanid"),
|
||||||
season=share_sub.get("season")):
|
season=share_sub.get("season")):
|
||||||
continue
|
continue
|
||||||
|
# 已经订阅过跳过
|
||||||
|
if self.subscribeoper.exist_history(tmdbid=share_sub.get("tmdbid"),
|
||||||
|
doubanid=share_sub.get("doubanid"),
|
||||||
|
season=share_sub.get("season")):
|
||||||
|
continue
|
||||||
# 去除无效属性
|
# 去除无效属性
|
||||||
for key in list(share_sub.keys()):
|
for key in list(share_sub.keys()):
|
||||||
if not hasattr(schemas.Subscribe(), key):
|
if not hasattr(schemas.Subscribe(), key):
|
||||||
|
|||||||
@@ -14,19 +14,38 @@ class TmdbChain(ChainBase, metaclass=Singleton):
|
|||||||
TheMovieDB处理链,单例运行
|
TheMovieDB处理链,单例运行
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str,
|
def tmdb_discover(self, mtype: MediaType,
|
||||||
with_original_language: str, page: int = 1) -> Optional[List[MediaInfo]]:
|
sort_by: str,
|
||||||
|
with_genres: str,
|
||||||
|
with_original_language: str,
|
||||||
|
with_keywords: str,
|
||||||
|
with_watch_providers: str,
|
||||||
|
vote_average: float,
|
||||||
|
vote_count: int,
|
||||||
|
release_date: str,
|
||||||
|
page: int = 1) -> Optional[List[MediaInfo]]:
|
||||||
"""
|
"""
|
||||||
:param mtype: 媒体类型
|
:param mtype: 媒体类型
|
||||||
:param sort_by: 排序方式
|
:param sort_by: 排序方式
|
||||||
:param with_genres: 类型
|
:param with_genres: 类型
|
||||||
:param with_original_language: 语言
|
:param with_original_language: 语言
|
||||||
|
:param with_keywords: 关键字
|
||||||
|
:param with_watch_providers: 提供商
|
||||||
|
:param vote_average: 评分
|
||||||
|
:param vote_count: 评分人数
|
||||||
|
:param release_date: 上映日期
|
||||||
:param page: 页码
|
:param page: 页码
|
||||||
:return: 媒体信息列表
|
:return: 媒体信息列表
|
||||||
"""
|
"""
|
||||||
return self.run_module("tmdb_discover", mtype=mtype,
|
return self.run_module("tmdb_discover", mtype=mtype,
|
||||||
sort_by=sort_by, with_genres=with_genres,
|
sort_by=sort_by,
|
||||||
|
with_genres=with_genres,
|
||||||
with_original_language=with_original_language,
|
with_original_language=with_original_language,
|
||||||
|
with_keywords=with_keywords,
|
||||||
|
with_watch_providers=with_watch_providers,
|
||||||
|
vote_average=vote_average,
|
||||||
|
vote_count=vote_count,
|
||||||
|
release_date=release_date,
|
||||||
page=page)
|
page=page)
|
||||||
|
|
||||||
def tmdb_trending(self, page: int = 1) -> Optional[List[MediaInfo]]:
|
def tmdb_trending(self, page: int = 1) -> Optional[List[MediaInfo]]:
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ class JobManager:
|
|||||||
# 计算状态为完成的任务数
|
# 计算状态为完成的任务数
|
||||||
if __mediaid__ not in self._job_view:
|
if __mediaid__ not in self._job_view:
|
||||||
return 0
|
return 0
|
||||||
return sum([task.fileitem.size for task in self._job_view[__mediaid__].tasks if task.state == "completed"])
|
return sum([task.fileitem.size for task in self._job_view[__mediaid__].tasks if task.state == "completed" and task.fileitem.size is not None])
|
||||||
|
|
||||||
def total(self) -> int:
|
def total(self) -> int:
|
||||||
"""
|
"""
|
||||||
@@ -663,22 +663,22 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
|||||||
if transfer_history:
|
if transfer_history:
|
||||||
mediainfo.title = transfer_history.title
|
mediainfo.title = transfer_history.title
|
||||||
|
|
||||||
# 获取集数据
|
|
||||||
if not task.episodes_info and mediainfo.type == MediaType.TV:
|
|
||||||
if task.meta.begin_season is None:
|
|
||||||
task.meta.begin_season = 1
|
|
||||||
mediainfo.season = mediainfo.season or task.meta.begin_season
|
|
||||||
task.episodes_info = self.tmdbchain.tmdb_episodes(
|
|
||||||
tmdbid=mediainfo.tmdb_id,
|
|
||||||
season=mediainfo.season
|
|
||||||
)
|
|
||||||
|
|
||||||
# 更新任务信息
|
# 更新任务信息
|
||||||
task.mediainfo = mediainfo
|
task.mediainfo = mediainfo
|
||||||
# 更新队列任务
|
# 更新队列任务
|
||||||
curr_task = self.jobview.remove_task(task.fileitem)
|
curr_task = self.jobview.remove_task(task.fileitem)
|
||||||
self.jobview.add_task(task, state=curr_task.state if curr_task else "waiting")
|
self.jobview.add_task(task, state=curr_task.state if curr_task else "waiting")
|
||||||
|
|
||||||
|
# 获取集数据
|
||||||
|
if not task.episodes_info and task.mediainfo.type == MediaType.TV:
|
||||||
|
if task.meta.begin_season is None:
|
||||||
|
task.meta.begin_season = 1
|
||||||
|
task.mediainfo.season = task.mediainfo.season or task.meta.begin_season
|
||||||
|
task.episodes_info = self.tmdbchain.tmdb_episodes(
|
||||||
|
tmdbid=task.mediainfo.tmdb_id,
|
||||||
|
season=task.mediainfo.season
|
||||||
|
)
|
||||||
|
|
||||||
# 查询整理目标目录
|
# 查询整理目标目录
|
||||||
if not task.target_directory:
|
if not task.target_directory:
|
||||||
if task.target_path:
|
if task.target_path:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import copy
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Union, Dict, Optional
|
from typing import Any, Union, Dict, Optional
|
||||||
@@ -303,7 +304,7 @@ class Command(metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 命令
|
# 命令
|
||||||
cmd_data = command['data'] if command.get('data') else {}
|
cmd_data = copy.deepcopy(command['data']) if command.get('data') else {}
|
||||||
args_num = ObjectUtils.arguments(command['func'])
|
args_num = ObjectUtils.arguments(command['func'])
|
||||||
if args_num > 0:
|
if args_num > 0:
|
||||||
if cmd_data:
|
if cmd_data:
|
||||||
|
|||||||
@@ -500,6 +500,21 @@ def cached(region: Optional[str] = None, maxsize: int = 1000, ttl: int = 1800,
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def is_valid_cache_value(cache_key: str, cached_value: Any, cache_region: str) -> bool:
|
||||||
|
"""
|
||||||
|
判断指定的值是否为一个有效的缓存值
|
||||||
|
|
||||||
|
:param cache_key: 缓存的键
|
||||||
|
:param cached_value: 缓存的值
|
||||||
|
:param cache_region: 缓存的区
|
||||||
|
:return: 若值是有效的缓存值返回 True,否则返回 False
|
||||||
|
"""
|
||||||
|
# 如果 skip_none 为 False,且 value 为 None,需要判断缓存实际是否存在
|
||||||
|
if not skip_none and cached_value is None:
|
||||||
|
if not cache_backend.exists(key=cache_key, region=cache_region):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
|
|
||||||
# 获取缓存区
|
# 获取缓存区
|
||||||
@@ -511,7 +526,7 @@ def cached(region: Optional[str] = None, maxsize: int = 1000, ttl: int = 1800,
|
|||||||
cache_key = cache_backend.get_cache_key(func, args, kwargs)
|
cache_key = cache_backend.get_cache_key(func, args, kwargs)
|
||||||
# 尝试获取缓存
|
# 尝试获取缓存
|
||||||
cached_value = cache_backend.get(cache_key, region=cache_region)
|
cached_value = cache_backend.get(cache_key, region=cache_region)
|
||||||
if should_cache(cached_value):
|
if should_cache(cached_value) and is_valid_cache_value(cache_key, cached_value, cache_region):
|
||||||
return cached_value
|
return cached_value
|
||||||
# 执行函数并缓存结果
|
# 执行函数并缓存结果
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class ConfigModel(BaseModel):
|
|||||||
# 自动检查和更新站点资源包(站点索引、认证等)
|
# 自动检查和更新站点资源包(站点索引、认证等)
|
||||||
AUTO_UPDATE_RESOURCE: bool = True
|
AUTO_UPDATE_RESOURCE: bool = True
|
||||||
# 是否启用DOH解析域名
|
# 是否启用DOH解析域名
|
||||||
DOH_ENABLE: bool = True
|
DOH_ENABLE: bool = False
|
||||||
# 使用 DOH 解析的域名列表
|
# 使用 DOH 解析的域名列表
|
||||||
DOH_DOMAINS: str = ("api.themoviedb.org,"
|
DOH_DOMAINS: str = ("api.themoviedb.org,"
|
||||||
"api.tmdb.org,"
|
"api.tmdb.org,"
|
||||||
@@ -236,7 +236,14 @@ class ConfigModel(BaseModel):
|
|||||||
"doubanio.com",
|
"doubanio.com",
|
||||||
"lain.bgm.tv",
|
"lain.bgm.tv",
|
||||||
"raw.githubusercontent.com",
|
"raw.githubusercontent.com",
|
||||||
"github.com"]
|
"github.com",
|
||||||
|
"thetvdb.com",
|
||||||
|
"cctvpic.com",
|
||||||
|
"iqiyipic.com",
|
||||||
|
"hdslb.com",
|
||||||
|
"cmvideo.cn",
|
||||||
|
"ykimg.com",
|
||||||
|
"qpic.cn"]
|
||||||
)
|
)
|
||||||
# 允许的图片文件后缀格式
|
# 允许的图片文件后缀格式
|
||||||
SECURITY_IMAGE_SUFFIXES: List[str] = Field(
|
SECURITY_IMAGE_SUFFIXES: List[str] = Field(
|
||||||
|
|||||||
@@ -262,6 +262,8 @@ class MediaInfo:
|
|||||||
runtime: int = None
|
runtime: int = None
|
||||||
# 下一集
|
# 下一集
|
||||||
next_episode_to_air: dict = field(default_factory=dict)
|
next_episode_to_air: dict = field(default_factory=dict)
|
||||||
|
# 内容分级
|
||||||
|
content_rating: str = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
# 设置媒体信息
|
# 设置媒体信息
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class ReleaseGroupsMatcher(metaclass=Singleton):
|
|||||||
"U2": [],
|
"U2": [],
|
||||||
"ultrahd": [],
|
"ultrahd": [],
|
||||||
"others": ['B(?:MDru|eyondHD|TN)', 'C(?:fandora|trlhd|MRG)', 'DON', 'EVO', 'FLUX', 'HONE(?:|yG)',
|
"others": ['B(?:MDru|eyondHD|TN)', 'C(?:fandora|trlhd|MRG)', 'DON', 'EVO', 'FLUX', 'HONE(?:|yG)',
|
||||||
'N(?:oGroup|T(?:b|G))', 'PandaMoon', 'SMURF', 'T(?:EPES|aengoo|rollHD )'],
|
'N(?:oGroup|T(?:b|G))', 'PandaMoon', 'SMURF', 'T(?:EPES|aengoo|rollHD )', 'UBWEB'],
|
||||||
"anime": ['ANi', 'HYSUB', 'KTXP', 'LoliHouse', 'MCE', 'Nekomoe kissaten', 'SweetSub', 'MingY',
|
"anime": ['ANi', 'HYSUB', 'KTXP', 'LoliHouse', 'MCE', 'Nekomoe kissaten', 'SweetSub', 'MingY',
|
||||||
'(?:Lilith|NC)-Raws', '织梦字幕组', '枫叶字幕组', '猎户手抄部', '喵萌奶茶屋', '漫猫字幕社',
|
'(?:Lilith|NC)-Raws', '织梦字幕组', '枫叶字幕组', '猎户手抄部', '喵萌奶茶屋', '漫猫字幕社',
|
||||||
'霜庭云花Sub', '北宇治字幕组', '氢气烤肉架', '云歌字幕组', '萌樱字幕组', '极影字幕社',
|
'霜庭云花Sub', '北宇治字幕组', '氢气烤肉架', '云歌字幕组', '萌樱字幕组', '极影字幕社',
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Subscribe(Base):
|
|||||||
tvdbid = Column(Integer)
|
tvdbid = Column(Integer)
|
||||||
doubanid = Column(String, index=True)
|
doubanid = Column(String, index=True)
|
||||||
bangumiid = Column(Integer, index=True)
|
bangumiid = Column(Integer, index=True)
|
||||||
|
mediaid = Column(String, index=True)
|
||||||
# 季号
|
# 季号
|
||||||
season = Column(Integer)
|
season = Column(Integer)
|
||||||
# 海报
|
# 海报
|
||||||
@@ -107,6 +108,14 @@ class Subscribe(Base):
|
|||||||
result = db.query(Subscribe).filter(Subscribe.state.in_(states)).all()
|
result = db.query(Subscribe).filter(Subscribe.state.in_(states)).all()
|
||||||
return list(result)
|
return list(result)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@db_query
|
||||||
|
def get_by_title(db: Session, title: str, season: int = None):
|
||||||
|
if season:
|
||||||
|
return db.query(Subscribe).filter(Subscribe.name == title,
|
||||||
|
Subscribe.season == season).first()
|
||||||
|
return db.query(Subscribe).filter(Subscribe.name == title).first()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@db_query
|
@db_query
|
||||||
def get_by_tmdbid(db: Session, tmdbid: int, season: int = None):
|
def get_by_tmdbid(db: Session, tmdbid: int, season: int = None):
|
||||||
@@ -117,14 +126,6 @@ class Subscribe(Base):
|
|||||||
result = db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid).all()
|
result = db.query(Subscribe).filter(Subscribe.tmdbid == tmdbid).all()
|
||||||
return list(result)
|
return list(result)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@db_query
|
|
||||||
def get_by_title(db: Session, title: str, season: int = None):
|
|
||||||
if season:
|
|
||||||
return db.query(Subscribe).filter(Subscribe.name == title,
|
|
||||||
Subscribe.season == season).first()
|
|
||||||
return db.query(Subscribe).filter(Subscribe.name == title).first()
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@db_query
|
@db_query
|
||||||
def get_by_doubanid(db: Session, doubanid: str):
|
def get_by_doubanid(db: Session, doubanid: str):
|
||||||
@@ -135,6 +136,11 @@ class Subscribe(Base):
|
|||||||
def get_by_bangumiid(db: Session, bangumiid: int):
|
def get_by_bangumiid(db: Session, bangumiid: int):
|
||||||
return db.query(Subscribe).filter(Subscribe.bangumiid == bangumiid).first()
|
return db.query(Subscribe).filter(Subscribe.bangumiid == bangumiid).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@db_query
|
||||||
|
def get_by_mediaid(db: Session, mediaid: str):
|
||||||
|
return db.query(Subscribe).filter(Subscribe.mediaid == mediaid).first()
|
||||||
|
|
||||||
@db_update
|
@db_update
|
||||||
def delete_by_tmdbid(self, db: Session, tmdbid: int, season: int):
|
def delete_by_tmdbid(self, db: Session, tmdbid: int, season: int):
|
||||||
subscrbies = self.get_by_tmdbid(db, tmdbid, season)
|
subscrbies = self.get_by_tmdbid(db, tmdbid, season)
|
||||||
@@ -149,6 +155,13 @@ class Subscribe(Base):
|
|||||||
subscribe.delete(db, subscribe.id)
|
subscribe.delete(db, subscribe.id)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@db_update
|
||||||
|
def delete_by_mediaid(self, db: Session, mediaid: str):
|
||||||
|
subscribe = self.get_by_mediaid(db, mediaid)
|
||||||
|
if subscribe:
|
||||||
|
subscribe.delete(db, subscribe.id)
|
||||||
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@db_query
|
@db_query
|
||||||
def list_by_username(db: Session, username: str, state: str = None, mtype: str = None):
|
def list_by_username(db: Session, username: str, state: str = None, mtype: str = None):
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class SubscribeHistory(Base):
|
|||||||
tvdbid = Column(Integer)
|
tvdbid = Column(Integer)
|
||||||
doubanid = Column(String, index=True)
|
doubanid = Column(String, index=True)
|
||||||
bangumiid = Column(Integer, index=True)
|
bangumiid = Column(Integer, index=True)
|
||||||
|
mediaid = Column(String, index=True)
|
||||||
# 季号
|
# 季号
|
||||||
season = Column(Integer)
|
season = Column(Integer)
|
||||||
# 海报
|
# 海报
|
||||||
@@ -73,6 +74,18 @@ class SubscribeHistory(Base):
|
|||||||
result = db.query(SubscribeHistory).filter(
|
result = db.query(SubscribeHistory).filter(
|
||||||
SubscribeHistory.type == mtype
|
SubscribeHistory.type == mtype
|
||||||
).order_by(
|
).order_by(
|
||||||
SubscribeHistory.date.desc()
|
SubscribeHistory.date.desc()
|
||||||
).offset((page - 1) * count).limit(count).all()
|
).offset((page - 1) * count).limit(count).all()
|
||||||
return list(result)
|
return list(result)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@db_query
|
||||||
|
def exists(db: Session, tmdbid: int = None, doubanid: str = None, season: int = None):
|
||||||
|
if tmdbid:
|
||||||
|
if season:
|
||||||
|
return db.query(SubscribeHistory).filter(SubscribeHistory.tmdbid == tmdbid,
|
||||||
|
SubscribeHistory.season == season).first()
|
||||||
|
return db.query(SubscribeHistory).filter(SubscribeHistory.tmdbid == tmdbid).first()
|
||||||
|
elif doubanid:
|
||||||
|
return db.query(SubscribeHistory).filter(SubscribeHistory.doubanid == doubanid).first()
|
||||||
|
return None
|
||||||
|
|||||||
@@ -118,3 +118,16 @@ class SubscribeOper(DbOper):
|
|||||||
kwargs.pop("id")
|
kwargs.pop("id")
|
||||||
subscribe = SubscribeHistory(**kwargs)
|
subscribe = SubscribeHistory(**kwargs)
|
||||||
subscribe.create(self._db)
|
subscribe.create(self._db)
|
||||||
|
|
||||||
|
def exist_history(self, tmdbid: int = None, doubanid: str = None, season: int = None):
|
||||||
|
"""
|
||||||
|
判断是否存在订阅历史
|
||||||
|
"""
|
||||||
|
if tmdbid:
|
||||||
|
if season:
|
||||||
|
return True if SubscribeHistory.exists(self._db, tmdbid=tmdbid, season=season) else False
|
||||||
|
else:
|
||||||
|
return True if SubscribeHistory.exists(self._db, tmdbid=tmdbid) else False
|
||||||
|
elif doubanid:
|
||||||
|
return True if SubscribeHistory.exists(self._db, doubanid=doubanid) else False
|
||||||
|
return False
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException
|
from fastapi import Depends, HTTPException
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -51,6 +51,12 @@ class UserOper(DbOper):
|
|||||||
用户管理
|
用户管理
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def list(self) -> List[User]:
|
||||||
|
"""
|
||||||
|
获取用户列表
|
||||||
|
"""
|
||||||
|
return User.list(self._db)
|
||||||
|
|
||||||
def add(self, **kwargs):
|
def add(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
新增用户
|
新增用户
|
||||||
@@ -90,3 +96,16 @@ class UserOper(DbOper):
|
|||||||
if settings:
|
if settings:
|
||||||
return settings.get(key)
|
return settings.get(key)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_name(self, **kwargs) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
根据绑定账号获取用户名称
|
||||||
|
"""
|
||||||
|
users = self.list()
|
||||||
|
for user in users:
|
||||||
|
user_setting = user.settings
|
||||||
|
if user_setting:
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
if user_setting.get(k) == str(v):
|
||||||
|
return user.name
|
||||||
|
return None
|
||||||
|
|||||||
@@ -165,3 +165,12 @@ class BangumiModule(_ModuleBase):
|
|||||||
if credits_info:
|
if credits_info:
|
||||||
return [MediaInfo(bangumi_info=credit) for credit in credits_info]
|
return [MediaInfo(bangumi_info=credit) for credit in credits_info]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def bangumi_discover(self, **kwargs) -> Optional[List[MediaInfo]]:
|
||||||
|
"""
|
||||||
|
发现Bangumi番剧
|
||||||
|
"""
|
||||||
|
infos = self.bangumiapi.discover(**kwargs)
|
||||||
|
if infos:
|
||||||
|
return [MediaInfo(bangumi_info=info) for info in infos]
|
||||||
|
return []
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class BangumiApi(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_urls = {
|
_urls = {
|
||||||
|
"discover": "v0/subjects",
|
||||||
"search": "search/subjects/%s?type=2",
|
"search": "search/subjects/%s?type=2",
|
||||||
"calendar": "calendar",
|
"calendar": "calendar",
|
||||||
"detail": "v0/subjects/%s",
|
"detail": "v0/subjects/%s",
|
||||||
@@ -30,14 +31,17 @@ class BangumiApi(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@cached(maxsize=settings.CACHE_CONF["bangumi"], ttl=settings.CACHE_CONF["meta"])
|
@cached(maxsize=settings.CACHE_CONF["bangumi"], ttl=settings.CACHE_CONF["meta"])
|
||||||
def __invoke(cls, url, **kwargs):
|
def __invoke(cls, url, key: str = None, **kwargs):
|
||||||
req_url = cls._base_url + url
|
req_url = cls._base_url + url
|
||||||
params = {}
|
params = {}
|
||||||
if kwargs:
|
if kwargs:
|
||||||
params.update(kwargs)
|
params.update(kwargs)
|
||||||
resp = cls._req.get_res(url=req_url, params=params)
|
resp = cls._req.get_res(url=req_url, params=params)
|
||||||
try:
|
try:
|
||||||
return resp.json() if resp else None
|
if not resp:
|
||||||
|
return None
|
||||||
|
result = resp.json()
|
||||||
|
return result.get(key) if key else result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return None
|
return None
|
||||||
@@ -194,3 +198,11 @@ class BangumiApi(object):
|
|||||||
for item in result:
|
for item in result:
|
||||||
ret_list.append(item)
|
ret_list.append(item)
|
||||||
return ret_list
|
return ret_list
|
||||||
|
|
||||||
|
def discover(self, **kwargs):
|
||||||
|
"""
|
||||||
|
发现
|
||||||
|
"""
|
||||||
|
return self.__invoke(self._urls["discover"],
|
||||||
|
key="data",
|
||||||
|
_ts=datetime.strftime(datetime.now(), '%Y%m%d'), **kwargs)
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ class Emby:
|
|||||||
case "tvshows":
|
case "tvshows":
|
||||||
library_type = MediaType.TV.value
|
library_type = MediaType.TV.value
|
||||||
case _:
|
case _:
|
||||||
continue
|
library_type = MediaType.UNKNOWN.value
|
||||||
image = self.__get_local_image_by_id(library.get("Id"))
|
image = self.__get_local_image_by_id(library.get("Id"))
|
||||||
libraries.append(
|
libraries.append(
|
||||||
schemas.MediaServerLibrary(
|
schemas.MediaServerLibrary(
|
||||||
|
|||||||
@@ -420,16 +420,19 @@ class FanartModule(_ModuleBase):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@cached(maxsize=settings.CACHE_CONF["fanart"], ttl=settings.CACHE_CONF["meta"], skip_none=False)
|
@cached(maxsize=settings.CACHE_CONF["fanart"], ttl=settings.CACHE_CONF["meta"])
|
||||||
def __request_fanart(cls, media_type: MediaType, queryid: Union[str, int]) -> Optional[dict]:
|
def __request_fanart(cls, media_type: MediaType, queryid: Union[str, int]) -> Optional[dict]:
|
||||||
if media_type == MediaType.MOVIE:
|
if media_type == MediaType.MOVIE:
|
||||||
image_url = cls._movie_url % queryid
|
image_url = cls._movie_url % queryid
|
||||||
else:
|
else:
|
||||||
image_url = cls._tv_url % queryid
|
image_url = cls._tv_url % queryid
|
||||||
try:
|
try:
|
||||||
ret = RequestUtils(proxies=cls._proxies, timeout=10).get_res(image_url)
|
ret = RequestUtils(proxies=cls._proxies, timeout=10).get_res(image_url, raise_exception=True)
|
||||||
if ret:
|
if ret:
|
||||||
return ret.json()
|
return ret.json()
|
||||||
|
else:
|
||||||
|
logger.debug(f"未能获取到 {queryid} 的Fanart图片")
|
||||||
|
return {}
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"获取{queryid}的Fanart图片失败:{str(err)}")
|
logger.error(f"获取{queryid}的Fanart图片失败:{str(err)}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ from app.helper.module import ModuleHelper
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
from app.modules.filemanager.storages import StorageBase
|
from app.modules.filemanager.storages import StorageBase
|
||||||
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, TransferDirectoryConf, FileItem, StorageUsage, TransferRenameEventData
|
from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, TransferDirectoryConf, FileItem, StorageUsage, \
|
||||||
|
TransferRenameEventData, TransferInterceptEventData
|
||||||
from app.schemas.types import MediaType, ModuleType, ChainEventType, OtherModulesType
|
from app.schemas.types import MediaType, ModuleType, ChainEventType, OtherModulesType
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
|
|
||||||
@@ -745,11 +746,12 @@ class FileManagerModule(_ModuleBase):
|
|||||||
logger.error(f"音轨文件 {org_path.name} 整理失败:{str(error)}")
|
logger.error(f"音轨文件 {org_path.name} 整理失败:{str(error)}")
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
def __transfer_dir(self, fileitem: FileItem, transfer_type: str,
|
def __transfer_dir(self, fileitem: FileItem, mediainfo: MediaInfo, transfer_type: str,
|
||||||
target_storage: str, target_path: Path) -> Tuple[Optional[FileItem], str]:
|
target_storage: str, target_path: Path) -> Tuple[Optional[FileItem], str]:
|
||||||
"""
|
"""
|
||||||
整理整个文件夹
|
整理整个文件夹
|
||||||
:param fileitem: 源文件
|
:param fileitem: 源文件
|
||||||
|
:param mediainfo: 媒体信息
|
||||||
:param transfer_type: 整理方式
|
:param transfer_type: 整理方式
|
||||||
:param target_storage: 目标存储
|
:param target_storage: 目标存储
|
||||||
:param target_path: 目标路径
|
:param target_path: 目标路径
|
||||||
@@ -763,6 +765,22 @@ class FileManagerModule(_ModuleBase):
|
|||||||
target_item = target_oper.get_folder(target_path)
|
target_item = target_oper.get_folder(target_path)
|
||||||
if not target_item:
|
if not target_item:
|
||||||
return None, f"获取目标目录失败:{target_path}"
|
return None, f"获取目标目录失败:{target_path}"
|
||||||
|
event_data = TransferInterceptEventData(
|
||||||
|
fileitem=fileitem,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
target_storage=target_storage,
|
||||||
|
target_path=target_path,
|
||||||
|
transfer_type=transfer_type
|
||||||
|
)
|
||||||
|
event = eventmanager.send_event(ChainEventType.TransferIntercept, event_data)
|
||||||
|
if event and event.event_data:
|
||||||
|
event_data = event.event_data
|
||||||
|
# 如果事件被取消,跳过文件整理
|
||||||
|
if event_data.cancel:
|
||||||
|
logger.debug(
|
||||||
|
f"Transfer dir canceled by event: {event_data.source},"
|
||||||
|
f"Reason: {event_data.reason}")
|
||||||
|
return None, event_data.reason
|
||||||
# 处理所有文件
|
# 处理所有文件
|
||||||
state, errmsg = self.__transfer_dir_files(fileitem=fileitem,
|
state, errmsg = self.__transfer_dir_files(fileitem=fileitem,
|
||||||
target_storage=target_storage,
|
target_storage=target_storage,
|
||||||
@@ -811,16 +829,38 @@ class FileManagerModule(_ModuleBase):
|
|||||||
# 返回成功
|
# 返回成功
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
def __transfer_file(self, fileitem: FileItem, target_storage: str, target_file: Path,
|
def __transfer_file(self, fileitem: FileItem, mediainfo: MediaInfo, target_storage: str, target_file: Path,
|
||||||
transfer_type: str, over_flag: bool = False) -> Tuple[Optional[FileItem], str]:
|
transfer_type: str, over_flag: bool = False) -> Tuple[Optional[FileItem], str]:
|
||||||
"""
|
"""
|
||||||
整理一个文件,同时处理其他相关文件
|
整理一个文件,同时处理其他相关文件
|
||||||
:param fileitem: 原文件
|
:param fileitem: 原文件
|
||||||
|
:param mediainfo: 媒体信息
|
||||||
:param target_storage: 目标存储
|
:param target_storage: 目标存储
|
||||||
:param target_file: 新文件
|
:param target_file: 新文件
|
||||||
:param transfer_type: 整理方式
|
:param transfer_type: 整理方式
|
||||||
:param over_flag: 是否覆盖,为True时会先删除再整理
|
:param over_flag: 是否覆盖,为True时会先删除再整理
|
||||||
"""
|
"""
|
||||||
|
logger.info(f"正在整理文件:【{fileitem.storage}】{fileitem.path} 到 【{target_storage}】{target_file},"
|
||||||
|
f"操作类型:{transfer_type}")
|
||||||
|
event_data = TransferInterceptEventData(
|
||||||
|
fileitem=fileitem,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
target_storage=target_storage,
|
||||||
|
target_path=target_file,
|
||||||
|
transfer_type=transfer_type,
|
||||||
|
options={
|
||||||
|
"over_flag": over_flag
|
||||||
|
}
|
||||||
|
)
|
||||||
|
event = eventmanager.send_event(ChainEventType.TransferIntercept, event_data)
|
||||||
|
if event and event.event_data:
|
||||||
|
event_data = event.event_data
|
||||||
|
# 如果事件被取消,跳过文件整理
|
||||||
|
if event_data.cancel:
|
||||||
|
logger.debug(
|
||||||
|
f"Transfer file canceled by event: {event_data.source},"
|
||||||
|
f"Reason: {event_data.reason}")
|
||||||
|
return None, event_data.reason
|
||||||
if target_storage == "local" and (target_file.exists() or target_file.is_symlink()):
|
if target_storage == "local" and (target_file.exists() or target_file.is_symlink()):
|
||||||
if not over_flag:
|
if not over_flag:
|
||||||
logger.warn(f"文件已存在:{target_file}")
|
logger.warn(f"文件已存在:{target_file}")
|
||||||
@@ -828,8 +868,6 @@ class FileManagerModule(_ModuleBase):
|
|||||||
else:
|
else:
|
||||||
logger.info(f"正在删除已存在的文件:{target_file}")
|
logger.info(f"正在删除已存在的文件:{target_file}")
|
||||||
target_file.unlink()
|
target_file.unlink()
|
||||||
logger.info(f"正在整理文件:【{fileitem.storage}】{fileitem.path} 到 【{target_storage}】{target_file},"
|
|
||||||
f"操作类型:{transfer_type}")
|
|
||||||
new_item, errmsg = self.__transfer_command(fileitem=fileitem,
|
new_item, errmsg = self.__transfer_command(fileitem=fileitem,
|
||||||
target_storage=target_storage,
|
target_storage=target_storage,
|
||||||
target_file=target_file,
|
target_file=target_file,
|
||||||
@@ -934,6 +972,7 @@ class FileManagerModule(_ModuleBase):
|
|||||||
new_path = target_path / fileitem.name
|
new_path = target_path / fileitem.name
|
||||||
# 整理目录
|
# 整理目录
|
||||||
new_diritem, errmsg = self.__transfer_dir(fileitem=fileitem,
|
new_diritem, errmsg = self.__transfer_dir(fileitem=fileitem,
|
||||||
|
mediainfo=mediainfo,
|
||||||
target_storage=target_storage,
|
target_storage=target_storage,
|
||||||
target_path=new_path,
|
target_path=new_path,
|
||||||
transfer_type=transfer_type)
|
transfer_type=transfer_type)
|
||||||
@@ -1063,6 +1102,7 @@ class FileManagerModule(_ModuleBase):
|
|||||||
self.__delete_version_files(target_storage, new_file)
|
self.__delete_version_files(target_storage, new_file)
|
||||||
# 整理文件
|
# 整理文件
|
||||||
new_item, err_msg = self.__transfer_file(fileitem=fileitem,
|
new_item, err_msg = self.__transfer_file(fileitem=fileitem,
|
||||||
|
mediainfo=mediainfo,
|
||||||
target_storage=target_storage,
|
target_storage=target_storage,
|
||||||
target_file=new_file,
|
target_file=new_file,
|
||||||
transfer_type=transfer_type,
|
transfer_type=transfer_type,
|
||||||
@@ -1127,7 +1167,7 @@ class FileManagerModule(_ModuleBase):
|
|||||||
if episode.episode_number == meta.begin_episode:
|
if episode.episode_number == meta.begin_episode:
|
||||||
episode_date = episode.air_date
|
episode_date = episode.air_date
|
||||||
break
|
break
|
||||||
|
|
||||||
return {
|
return {
|
||||||
# 标题
|
# 标题
|
||||||
"title": __convert_invalid_characters(mediainfo.title),
|
"title": __convert_invalid_characters(mediainfo.title),
|
||||||
|
|||||||
@@ -70,10 +70,13 @@ class Alist(StorageBase, metaclass=Singleton):
|
|||||||
@cached(maxsize=1, ttl=60 * 60 * 24 * 2 - 60 * 5)
|
@cached(maxsize=1, ttl=60 * 60 * 24 * 2 - 60 * 5)
|
||||||
def __generate_token(self) -> str:
|
def __generate_token(self) -> str:
|
||||||
"""
|
"""
|
||||||
使用账号密码生成一个临时token
|
如果设置永久令牌则返回永久令牌,否则使用账号密码生成一个临时 token
|
||||||
缓存2天,提前5分钟更新
|
缓存2天,提前5分钟更新
|
||||||
"""
|
"""
|
||||||
conf = self.get_conf()
|
conf = self.get_conf()
|
||||||
|
token = conf.get("token")
|
||||||
|
if token:
|
||||||
|
return str(token)
|
||||||
resp: Response = RequestUtils(headers={
|
resp: Response = RequestUtils(headers={
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}).post_res(
|
}).post_res(
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
from app.modules.indexer.parser import SiteSchema
|
from app.modules.indexer.parser import SiteSchema
|
||||||
from app.modules.indexer.parser.nexus_php import NexusPhpSiteUserInfo
|
from app.modules.indexer.parser.nexus_php import NexusPhpSiteUserInfo
|
||||||
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
class NexusAudiencesSiteUserInfo(NexusPhpSiteUserInfo):
|
class NexusAudiencesSiteUserInfo(NexusPhpSiteUserInfo):
|
||||||
schema = SiteSchema.NexusAudiences
|
schema = SiteSchema.NexusAudiences
|
||||||
|
|
||||||
def _parse_site_page(self, html_text: str):
|
|
||||||
super()._parse_site_page(html_text)
|
|
||||||
self._torrent_seeding_page = f"usertorrentlist.php?userid={self.userid}&type=seeding"
|
|
||||||
|
|
||||||
def _parse_seeding_pages(self):
|
def _parse_seeding_pages(self):
|
||||||
|
if not self._torrent_seeding_page:
|
||||||
|
return
|
||||||
self._torrent_seeding_headers = {"Referer": urljoin(self._base_url, self._user_detail_page)}
|
self._torrent_seeding_headers = {"Referer": urljoin(self._base_url, self._user_detail_page)}
|
||||||
super()._parse_seeding_pages()
|
html_text = self._get_page_content(
|
||||||
|
url=urljoin(self._base_url, self._torrent_seeding_page),
|
||||||
|
params=self._torrent_seeding_params,
|
||||||
|
headers=self._torrent_seeding_headers
|
||||||
|
)
|
||||||
|
if not html_text:
|
||||||
|
return
|
||||||
|
html = etree.HTML(html_text)
|
||||||
|
if not StringUtils.is_valid_html_element(html):
|
||||||
|
return
|
||||||
|
total_row = html.xpath('//table[@class="table table-bordered"]//tr[td[1][normalize-space()="Total"]]')
|
||||||
|
if not total_row:
|
||||||
|
return
|
||||||
|
seeding_count = total_row[0].xpath('./td[2]/text()')
|
||||||
|
seeding_size = total_row[0].xpath('./td[3]/text()')
|
||||||
|
self.seeding = StringUtils.str_int(seeding_count[0]) if seeding_count else 0
|
||||||
|
self.seeding_size = StringUtils.num_filesize(seeding_size[0].strip()) if seeding_size else 0
|
||||||
|
|||||||
@@ -149,16 +149,17 @@ class Jellyfin:
|
|||||||
match library.get("CollectionType"):
|
match library.get("CollectionType"):
|
||||||
case "movies":
|
case "movies":
|
||||||
library_type = MediaType.MOVIE.value
|
library_type = MediaType.MOVIE.value
|
||||||
|
link = f"{self._playhost or self._host}web/index.html#!" \
|
||||||
|
f"/movies.html?topParentId={library.get('Id')}"
|
||||||
case "tvshows":
|
case "tvshows":
|
||||||
library_type = MediaType.TV.value
|
library_type = MediaType.TV.value
|
||||||
|
link = f"{self._playhost or self._host}web/index.html#!" \
|
||||||
|
f"/tv.html?topParentId={library.get('Id')}"
|
||||||
case _:
|
case _:
|
||||||
continue
|
library_type = MediaType.UNKNOWN.value
|
||||||
|
link = f"{self._playhost or self._host}web/index.html#!" \
|
||||||
|
f"/library.html?topParentId={library.get('Id')}"
|
||||||
image = self.__get_local_image_by_id(library.get("Id"))
|
image = self.__get_local_image_by_id(library.get("Id"))
|
||||||
link = f"{self._playhost or self._host}web/index.html#!" \
|
|
||||||
f"/movies.html?topParentId={library.get('Id')}" \
|
|
||||||
if library_type == MediaType.MOVIE.value \
|
|
||||||
else f"{self._playhost or self._host}web/index.html#!" \
|
|
||||||
f"/tv.html?topParentId={library.get('Id')}"
|
|
||||||
libraries.append(
|
libraries.append(
|
||||||
schemas.MediaServerLibrary(
|
schemas.MediaServerLibrary(
|
||||||
server="jellyfin",
|
server="jellyfin",
|
||||||
@@ -668,6 +669,12 @@ class Jellyfin:
|
|||||||
"S" + str(eventItem.season_id),
|
"S" + str(eventItem.season_id),
|
||||||
"E" + str(eventItem.episode_id),
|
"E" + str(eventItem.episode_id),
|
||||||
message.get('Name'))
|
message.get('Name'))
|
||||||
|
elif message.get("ItemType") == 'Audio':
|
||||||
|
# 音乐
|
||||||
|
eventItem.item_type = "AUD"
|
||||||
|
eventItem.item_name = message.get('Album')
|
||||||
|
eventItem.overview = message.get('Name')
|
||||||
|
eventItem.item_id = message.get('ItemId')
|
||||||
else:
|
else:
|
||||||
# 电影
|
# 电影
|
||||||
eventItem.item_type = "MOV"
|
eventItem.item_type = "MOV"
|
||||||
|
|||||||
@@ -356,26 +356,52 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
return None
|
return None
|
||||||
return self.scraper.get_metadata_img(mediainfo=mediainfo, season=season, episode=episode)
|
return self.scraper.get_metadata_img(mediainfo=mediainfo, season=season, episode=episode)
|
||||||
|
|
||||||
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str, with_original_language: str,
|
def tmdb_discover(self, mtype: MediaType, sort_by: str,
|
||||||
|
with_genres: str,
|
||||||
|
with_original_language: str,
|
||||||
|
with_keywords: str,
|
||||||
|
with_watch_providers: str,
|
||||||
|
vote_average: float,
|
||||||
|
vote_count: int,
|
||||||
|
release_date: str,
|
||||||
page: int = 1) -> Optional[List[MediaInfo]]:
|
page: int = 1) -> Optional[List[MediaInfo]]:
|
||||||
"""
|
"""
|
||||||
:param mtype: 媒体类型
|
:param mtype: 媒体类型
|
||||||
:param sort_by: 排序方式
|
:param sort_by: 排序方式
|
||||||
:param with_genres: 类型
|
:param with_genres: 类型
|
||||||
:param with_original_language: 语言
|
:param with_original_language: 语言
|
||||||
|
:param with_keywords: 关键字
|
||||||
|
:param with_watch_providers: 提供商
|
||||||
|
:param vote_average: 评分
|
||||||
|
:param vote_count: 评分人数
|
||||||
|
:param release_date: 发布日期
|
||||||
:param page: 页码
|
:param page: 页码
|
||||||
:return: 媒体信息列表
|
:return: 媒体信息列表
|
||||||
"""
|
"""
|
||||||
if mtype == MediaType.MOVIE:
|
if mtype == MediaType.MOVIE:
|
||||||
infos = self.tmdb.discover_movies(sort_by=sort_by,
|
infos = self.tmdb.discover_movies({
|
||||||
with_genres=with_genres,
|
"sort_by": sort_by,
|
||||||
with_original_language=with_original_language,
|
"with_genres": with_genres,
|
||||||
page=page)
|
"with_original_language": with_original_language,
|
||||||
|
"with_keywords": with_keywords,
|
||||||
|
"with_watch_providers": with_watch_providers,
|
||||||
|
"vote_average.gte": vote_average,
|
||||||
|
"vote_count.gte": vote_count,
|
||||||
|
"release_date.gte": release_date,
|
||||||
|
"page": page
|
||||||
|
})
|
||||||
elif mtype == MediaType.TV:
|
elif mtype == MediaType.TV:
|
||||||
infos = self.tmdb.discover_tvs(sort_by=sort_by,
|
infos = self.tmdb.discover_tvs({
|
||||||
with_genres=with_genres,
|
"sort_by": sort_by,
|
||||||
with_original_language=with_original_language,
|
"with_genres": with_genres,
|
||||||
page=page)
|
"with_original_language": with_original_language,
|
||||||
|
"with_keywords": with_keywords,
|
||||||
|
"with_watch_providers": with_watch_providers,
|
||||||
|
"vote_average.gte": vote_average,
|
||||||
|
"vote_count.gte": vote_count,
|
||||||
|
"first_air_date.gte": release_date,
|
||||||
|
"page": page
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
if infos:
|
if infos:
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ class TmdbScraper:
|
|||||||
DomUtils.add_node(doc, root, "genre", genre.get("name") or "")
|
DomUtils.add_node(doc, root, "genre", genre.get("name") or "")
|
||||||
# 评分
|
# 评分
|
||||||
DomUtils.add_node(doc, root, "rating", mediainfo.vote_average or "0")
|
DomUtils.add_node(doc, root, "rating", mediainfo.vote_average or "0")
|
||||||
|
# 内容分级
|
||||||
|
if content_rating := mediainfo.content_rating:
|
||||||
|
DomUtils.add_node(doc, root, "mpaa", content_rating)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|||||||
@@ -601,6 +601,8 @@ class TmdbApi:
|
|||||||
tmdb_info['genre_ids'] = __get_genre_ids(tmdb_info.get('genres'))
|
tmdb_info['genre_ids'] = __get_genre_ids(tmdb_info.get('genres'))
|
||||||
# 别名和译名
|
# 别名和译名
|
||||||
tmdb_info['names'] = self.__get_names(tmdb_info)
|
tmdb_info['names'] = self.__get_names(tmdb_info)
|
||||||
|
# 内容分级
|
||||||
|
tmdb_info['content_rating'] = self.__get_content_rating(tmdb_info)
|
||||||
# 转换多语种标题
|
# 转换多语种标题
|
||||||
self.__update_tmdbinfo_extra_title(tmdb_info)
|
self.__update_tmdbinfo_extra_title(tmdb_info)
|
||||||
# 转换中文标题
|
# 转换中文标题
|
||||||
@@ -608,6 +610,68 @@ class TmdbApi:
|
|||||||
|
|
||||||
return tmdb_info
|
return tmdb_info
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_content_rating(tmdb_info: dict) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
获得tmdb中的内容评级
|
||||||
|
:param tmdb_info: TMDB信息
|
||||||
|
:return: 内容评级
|
||||||
|
"""
|
||||||
|
if not tmdb_info:
|
||||||
|
return None
|
||||||
|
# dict[地区:分级]
|
||||||
|
ratings = {}
|
||||||
|
if results := (tmdb_info.get("release_dates") or {}).get("results"):
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"iso_3166_1": "AR",
|
||||||
|
"release_dates": [
|
||||||
|
{
|
||||||
|
"certification": "+13",
|
||||||
|
"descriptors": [],
|
||||||
|
"iso_639_1": "",
|
||||||
|
"note": "",
|
||||||
|
"release_date": "2025-01-23T00:00:00.000Z",
|
||||||
|
"type": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
for item in results:
|
||||||
|
iso_3166_1 = item.get("iso_3166_1")
|
||||||
|
if not iso_3166_1:
|
||||||
|
continue
|
||||||
|
dates = item.get("release_dates")
|
||||||
|
if not dates:
|
||||||
|
continue
|
||||||
|
certification = dates[0].get("certification")
|
||||||
|
if not certification:
|
||||||
|
continue
|
||||||
|
ratings[iso_3166_1] = certification
|
||||||
|
elif results := (tmdb_info.get("content_ratings") or {}).get("results"):
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"descriptors": [],
|
||||||
|
"iso_3166_1": "US",
|
||||||
|
"rating": "TV-MA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
for item in results:
|
||||||
|
iso_3166_1 = item.get("iso_3166_1")
|
||||||
|
if not iso_3166_1:
|
||||||
|
continue
|
||||||
|
rating = item.get("rating")
|
||||||
|
if not rating:
|
||||||
|
continue
|
||||||
|
ratings[iso_3166_1] = rating
|
||||||
|
if not ratings:
|
||||||
|
return None
|
||||||
|
return ratings.get("CN") or ratings.get("US")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __update_tmdbinfo_cn_title(tmdb_info: dict):
|
def __update_tmdbinfo_cn_title(tmdb_info: dict):
|
||||||
"""
|
"""
|
||||||
@@ -700,6 +764,7 @@ class TmdbApi:
|
|||||||
"credits,"
|
"credits,"
|
||||||
"alternative_titles,"
|
"alternative_titles,"
|
||||||
"translations,"
|
"translations,"
|
||||||
|
"release_dates,"
|
||||||
"external_ids") -> Optional[dict]:
|
"external_ids") -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
获取电影的详情
|
获取电影的详情
|
||||||
@@ -812,6 +877,7 @@ class TmdbApi:
|
|||||||
"credits,"
|
"credits,"
|
||||||
"alternative_titles,"
|
"alternative_titles,"
|
||||||
"translations,"
|
"translations,"
|
||||||
|
"content_ratings,"
|
||||||
"external_ids") -> Optional[dict]:
|
"external_ids") -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
获取电视剧的详情
|
获取电视剧的详情
|
||||||
@@ -1080,18 +1146,17 @@ class TmdbApi:
|
|||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def discover_movies(self, **kwargs) -> List[dict]:
|
def discover_movies(self, params: dict) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
发现电影
|
发现电影
|
||||||
:param kwargs:
|
:param params: 参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if not self.discover:
|
if not self.discover:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
logger.debug(f"正在发现电影:{kwargs}...")
|
logger.debug(f"正在发现电影:{params}...")
|
||||||
params_tuple = tuple(kwargs.items())
|
tmdbinfo = self.discover.discover_movies(tuple(params.items()))
|
||||||
tmdbinfo = self.discover.discover_movies(params_tuple)
|
|
||||||
if tmdbinfo:
|
if tmdbinfo:
|
||||||
for info in tmdbinfo:
|
for info in tmdbinfo:
|
||||||
info['media_type'] = MediaType.MOVIE
|
info['media_type'] = MediaType.MOVIE
|
||||||
@@ -1100,18 +1165,17 @@ class TmdbApi:
|
|||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def discover_tvs(self, **kwargs) -> List[dict]:
|
def discover_tvs(self, params: dict) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
发现电视剧
|
发现电视剧
|
||||||
:param kwargs:
|
:param params: 参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if not self.discover:
|
if not self.discover:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
logger.debug(f"正在发现电视剧:{kwargs}...")
|
logger.debug(f"正在发现电视剧:{params}...")
|
||||||
params_tuple = tuple(kwargs.items())
|
tmdbinfo = self.discover.discover_tv_shows(tuple(params.items()))
|
||||||
tmdbinfo = self.discover.discover_tv_shows(params_tuple)
|
|
||||||
if tmdbinfo:
|
if tmdbinfo:
|
||||||
for info in tmdbinfo:
|
for info in tmdbinfo:
|
||||||
info['media_type'] = MediaType.TV
|
info['media_type'] = MediaType.TV
|
||||||
|
|||||||
@@ -225,7 +225,8 @@ class _PluginBase(metaclass=ABCMeta):
|
|||||||
return self.plugindata.del_data(plugin_id, key)
|
return self.plugindata.del_data(plugin_id, key)
|
||||||
|
|
||||||
def post_message(self, channel: MessageChannel = None, mtype: NotificationType = None, title: str = None,
|
def post_message(self, channel: MessageChannel = None, mtype: NotificationType = None, title: str = None,
|
||||||
text: str = None, image: str = None, link: str = None, userid: str = None, username: str = None):
|
text: str = None, image: str = None, link: str = None, userid: str = None, username: str = None,
|
||||||
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
发送消息
|
发送消息
|
||||||
"""
|
"""
|
||||||
@@ -233,7 +234,7 @@ class _PluginBase(metaclass=ABCMeta):
|
|||||||
link = settings.MP_DOMAIN(f"#/plugins?tab=installed&id={self.__class__.__name__}")
|
link = settings.MP_DOMAIN(f"#/plugins?tab=installed&id={self.__class__.__name__}")
|
||||||
self.chain.post_message(Notification(
|
self.chain.post_message(Notification(
|
||||||
channel=channel, mtype=mtype, title=title, text=text,
|
channel=channel, mtype=mtype, title=title, text=text,
|
||||||
image=image, link=link, userid=userid, username=username
|
image=image, link=link, userid=userid, username=username, **kwargs
|
||||||
))
|
))
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|||||||
@@ -79,8 +79,6 @@ class MediaInfo(BaseModel):
|
|||||||
title_year: Optional[str] = None
|
title_year: Optional[str] = None
|
||||||
# 当前指定季,如有
|
# 当前指定季,如有
|
||||||
season: Optional[int] = None
|
season: Optional[int] = None
|
||||||
# 合集等id
|
|
||||||
collection_id: Optional[int] = None
|
|
||||||
# TMDB ID
|
# TMDB ID
|
||||||
tmdb_id: Optional[int] = None
|
tmdb_id: Optional[int] = None
|
||||||
# IMDB ID
|
# IMDB ID
|
||||||
@@ -91,6 +89,12 @@ class MediaInfo(BaseModel):
|
|||||||
douban_id: Optional[str] = None
|
douban_id: Optional[str] = None
|
||||||
# Bangumi ID
|
# Bangumi ID
|
||||||
bangumi_id: Optional[int] = None
|
bangumi_id: Optional[int] = None
|
||||||
|
# 合集ID
|
||||||
|
collection_id: Optional[int] = None
|
||||||
|
# 其它媒体ID前缀
|
||||||
|
mediaid_prefix: Optional[str] = None
|
||||||
|
# 其它媒体ID值
|
||||||
|
media_id: Optional[str] = None
|
||||||
# 媒体原语种
|
# 媒体原语种
|
||||||
original_language: Optional[str] = None
|
original_language: Optional[str] = None
|
||||||
# 媒体原发行标题
|
# 媒体原发行标题
|
||||||
@@ -238,6 +242,19 @@ class Context(BaseModel):
|
|||||||
torrent_info: Optional[TorrentInfo] = None
|
torrent_info: Optional[TorrentInfo] = None
|
||||||
|
|
||||||
|
|
||||||
|
class MediaSeason(BaseModel):
|
||||||
|
"""
|
||||||
|
季信息
|
||||||
|
"""
|
||||||
|
air_date: Optional[str] = None
|
||||||
|
episode_count: Optional[int] = None
|
||||||
|
name: Optional[str] = None
|
||||||
|
overview: Optional[str] = None
|
||||||
|
poster_path: Optional[str] = None
|
||||||
|
season_number: Optional[int] = None
|
||||||
|
vote_average: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
class MediaPerson(BaseModel):
|
class MediaPerson(BaseModel):
|
||||||
"""
|
"""
|
||||||
媒体人物信息
|
媒体人物信息
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import Optional, Dict, Any, List, Set
|
|||||||
|
|
||||||
from pydantic import BaseModel, Field, root_validator
|
from pydantic import BaseModel, Field, root_validator
|
||||||
|
|
||||||
from app.schemas import MessageChannel
|
from app.schemas import MessageChannel, FileItem
|
||||||
|
|
||||||
|
|
||||||
class BaseEventData(BaseModel):
|
class BaseEventData(BaseModel):
|
||||||
@@ -50,7 +50,7 @@ class AuthCredentials(ChainEventData):
|
|||||||
service: Optional[str] = Field(default=None, description="服务名称")
|
service: Optional[str] = Field(default=None, description="服务名称")
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@root_validator(pre=True)
|
||||||
def check_fields_based_on_grant_type(cls, values): # noqa
|
def check_fields_based_on_grant_type(cls, values): # noqa
|
||||||
grant_type = values.get("grant_type")
|
grant_type = values.get("grant_type")
|
||||||
if not grant_type:
|
if not grant_type:
|
||||||
values["grant_type"] = "password"
|
values["grant_type"] = "password"
|
||||||
@@ -202,3 +202,97 @@ class ResourceDownloadEventData(ChainEventData):
|
|||||||
cancel: bool = Field(default=False, description="是否取消下载")
|
cancel: bool = Field(default=False, description="是否取消下载")
|
||||||
source: str = Field(default="未知拦截源", description="拦截源")
|
source: str = Field(default="未知拦截源", description="拦截源")
|
||||||
reason: str = Field(default="", description="拦截原因")
|
reason: str = Field(default="", description="拦截原因")
|
||||||
|
|
||||||
|
|
||||||
|
class TransferInterceptEventData(ChainEventData):
|
||||||
|
"""
|
||||||
|
TransferIntercept 事件的数据模型
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
# 输入参数
|
||||||
|
fileitem (FileItem): 源文件
|
||||||
|
target_storage (str): 目标存储
|
||||||
|
target_path (Path): 目标路径
|
||||||
|
transfer_type (str): 整理方式(copy、move、link、softlink等)
|
||||||
|
options (dict): 其他参数
|
||||||
|
|
||||||
|
# 输出参数
|
||||||
|
cancel (bool): 是否取消下载,默认值为 False
|
||||||
|
source (str): 拦截源,默认值为 "未知拦截源"
|
||||||
|
reason (str): 拦截原因,描述拦截的具体原因
|
||||||
|
"""
|
||||||
|
# 输入参数
|
||||||
|
fileitem: FileItem = Field(..., description="源文件")
|
||||||
|
mediainfo: Any = Field(..., description="媒体信息")
|
||||||
|
target_storage: str = Field(..., description="目标存储")
|
||||||
|
target_path: Path = Field(..., description="目标路径")
|
||||||
|
transfer_type: str = Field(..., description="整理方式")
|
||||||
|
options: Optional[dict] = Field(default=None, description="其他参数")
|
||||||
|
|
||||||
|
# 输出参数
|
||||||
|
cancel: bool = Field(default=False, description="是否取消整理")
|
||||||
|
source: str = Field(default="未知拦截源", description="拦截源")
|
||||||
|
reason: str = Field(default="", description="拦截原因")
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoverMediaSource(BaseModel):
|
||||||
|
"""
|
||||||
|
探索媒体数据源的基类
|
||||||
|
"""
|
||||||
|
name: str = Field(..., description="数据源名称")
|
||||||
|
mediaid_prefix: str = Field(..., description="媒体ID的前缀,不含:")
|
||||||
|
api_path: str = Field(..., description="媒体数据源API地址")
|
||||||
|
filter_params: Optional[Dict[str, Any]] = Field(default=None, description="过滤参数")
|
||||||
|
filter_ui: Optional[List[dict]] = Field(default=[], description="过滤参数UI配置")
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoverSourceEventData(ChainEventData):
|
||||||
|
"""
|
||||||
|
DiscoverSource 事件的数据模型
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
# 输出参数
|
||||||
|
extra_sources (List[DiscoverMediaSource]): 额外媒体数据源
|
||||||
|
"""
|
||||||
|
# 输出参数
|
||||||
|
extra_sources: List[DiscoverMediaSource] = Field(default_factory=list, description="额外媒体数据源")
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendMediaSource(BaseModel):
|
||||||
|
"""
|
||||||
|
推荐媒体数据源的基类
|
||||||
|
"""
|
||||||
|
name: str = Field(..., description="数据源名称")
|
||||||
|
api_path: str = Field(..., description="媒体数据源API地址")
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendSourceEventData(ChainEventData):
|
||||||
|
"""
|
||||||
|
RecommendSource 事件的数据模型
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
# 输出参数
|
||||||
|
extra_sources (List[RecommendMediaSource]): 额外媒体数据源
|
||||||
|
"""
|
||||||
|
# 输出参数
|
||||||
|
extra_sources: List[RecommendMediaSource] = Field(default_factory=list, description="额外媒体数据源")
|
||||||
|
|
||||||
|
|
||||||
|
class MediaRecognizeConvertEventData(ChainEventData):
|
||||||
|
"""
|
||||||
|
MediaRecognizeConvert 事件的数据模型
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
# 输入参数
|
||||||
|
mediaid (str): 媒体ID,格式为`前缀:ID值`,如 tmdb:12345、douban:1234567
|
||||||
|
convert_type (str): 转换类型 仅支持:themoviedb/douban,需要转换为对应的媒体数据并返回
|
||||||
|
|
||||||
|
# 输出参数
|
||||||
|
media_dict (dict): TheMovieDb/豆瓣的媒体数据
|
||||||
|
"""
|
||||||
|
# 输入参数
|
||||||
|
mediaid: str = Field(..., description="媒体ID")
|
||||||
|
convert_type: str = Field(..., description="转换类型(themoviedb/douban)")
|
||||||
|
|
||||||
|
# 输出参数
|
||||||
|
media_dict: dict = Field(default=dict, description="转换后的媒体信息(TheMovieDb/豆瓣)")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class Subscribe(BaseModel):
|
|||||||
tmdbid: Optional[int] = None
|
tmdbid: Optional[int] = None
|
||||||
doubanid: Optional[str] = None
|
doubanid: Optional[str] = None
|
||||||
bangumiid: Optional[int] = None
|
bangumiid: Optional[int] = None
|
||||||
|
mediaid: Optional[str] = None
|
||||||
# 季号
|
# 季号
|
||||||
season: Optional[int] = None
|
season: Optional[int] = None
|
||||||
# 海报
|
# 海报
|
||||||
|
|||||||
@@ -75,10 +75,18 @@ class ChainEventType(Enum):
|
|||||||
CommandRegister = "command.register"
|
CommandRegister = "command.register"
|
||||||
# 整理重命名
|
# 整理重命名
|
||||||
TransferRename = "transfer.rename"
|
TransferRename = "transfer.rename"
|
||||||
|
# 整理拦截
|
||||||
|
TransferIntercept = "transfer.intercept"
|
||||||
# 资源选择
|
# 资源选择
|
||||||
ResourceSelection = "resource.selection"
|
ResourceSelection = "resource.selection"
|
||||||
# 资源下载
|
# 资源下载
|
||||||
ResourceDownload = "resource.download"
|
ResourceDownload = "resource.download"
|
||||||
|
# 探索数据源
|
||||||
|
DiscoverSource = "discover.source"
|
||||||
|
# 媒体识别转换
|
||||||
|
MediaRecognizeConvert = "media.recognize.convert"
|
||||||
|
# 推荐数据源
|
||||||
|
RecommendSource = "recommend.source"
|
||||||
|
|
||||||
|
|
||||||
# 系统配置Key字典
|
# 系统配置Key字典
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class SecurityUtils:
|
|||||||
netloc = parsed_url.netloc.lower()
|
netloc = parsed_url.netloc.lower()
|
||||||
if not netloc:
|
if not netloc:
|
||||||
return False
|
return False
|
||||||
|
netloc_no_port = netloc.split(":")[0]
|
||||||
|
|
||||||
# 检查每个允许的域名
|
# 检查每个允许的域名
|
||||||
allowed_domains = {d.lower() for d in allowed_domains}
|
allowed_domains = {d.lower() for d in allowed_domains}
|
||||||
@@ -78,7 +79,7 @@ class SecurityUtils:
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# 非严格模式下,允许子域名匹配
|
# 非严格模式下,允许子域名匹配
|
||||||
if netloc == allowed_netloc or netloc.endswith('.' + allowed_netloc):
|
if netloc_no_port == allowed_netloc or netloc_no_port.endswith('.' + allowed_netloc):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
32
database/versions/ca5461f314f2_2_1_0.py
Normal file
32
database/versions/ca5461f314f2_2_1_0.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""2.1.0
|
||||||
|
|
||||||
|
Revision ID: ca5461f314f2
|
||||||
|
Revises: 55390f1f77c1
|
||||||
|
Create Date: 2025-02-06 18:28:00.644571
|
||||||
|
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import sqlite
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ca5461f314f2'
|
||||||
|
down_revision = '55390f1f77c1'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
# 订阅增加mediaid
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
op.add_column('subscribe', sa.Column('mediaid', sa.String(), nullable=True))
|
||||||
|
op.create_index('ix_subscribe_mediaid', 'subscribe', ['mediaid'], unique=False)
|
||||||
|
op.add_column('subscribehistory', sa.Column('mediaid', sa.String(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -65,4 +65,5 @@ aligo~=6.2.4
|
|||||||
aiofiles~=24.1.0
|
aiofiles~=24.1.0
|
||||||
jieba~=0.42.1
|
jieba~=0.42.1
|
||||||
rsa~=4.9
|
rsa~=4.9
|
||||||
redis~=5.2.1
|
redis~=5.2.1
|
||||||
|
async_timeout~=5.0.1; python_full_version < "3.11.3"
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
APP_VERSION = 'v2.2.4'
|
APP_VERSION = 'v2.2.8'
|
||||||
FRONTEND_VERSION = 'v2.2.4'
|
FRONTEND_VERSION = 'v2.2.8'
|
||||||
|
|||||||
Reference in New Issue
Block a user