mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-09 10:02:39 +08:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4666b9051d | ||
|
|
56c524a822 | ||
|
|
43e8df1b9f | ||
|
|
dbc465f6e5 | ||
|
|
bfbd3c527c | ||
|
|
412405f69b | ||
|
|
12b74eb04f | ||
|
|
2305a6287a | ||
|
|
68245be081 | ||
|
|
29e01294bd | ||
|
|
d35bee54a6 | ||
|
|
bf63be18e4 | ||
|
|
3dc7adc61a | ||
|
|
047d1e0afd | ||
|
|
7c017faf31 | ||
|
|
7a59565761 | ||
|
|
9afb904d40 | ||
|
|
8189de589a | ||
|
|
c458d7525d | ||
|
|
5c7bd95f6b | ||
|
|
70c4509682 | ||
|
|
f34e36c571 | ||
|
|
5054ffe7e4 | ||
|
|
ed30933ca2 |
@@ -21,6 +21,21 @@ def calendar(page: int = 1,
|
||||
return RecommendChain().bangumi_calendar(page=page, count=count)
|
||||
|
||||
|
||||
@router.get("/subjects", summary="搜索Bangumi", response_model=List[schemas.MediaInfo])
|
||||
def bangumi_subjects(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
|
||||
"""
|
||||
return RecommendChain().bangumi_discover(type=type, cat=cat, sort=sort, year=year,
|
||||
page=page, count=count)
|
||||
|
||||
|
||||
@router.get("/credits/{bangumiid}", summary="查询Bangumi演职员表", response_model=List[schemas.MediaPerson])
|
||||
def bangumi_credits(bangumiid: int,
|
||||
page: int = 1,
|
||||
@@ -61,13 +76,14 @@ def bangumi_person(person_id: int,
|
||||
@router.get("/person/credits/{person_id}", summary="人物参演作品", response_model=List[schemas.MediaInfo])
|
||||
def bangumi_person_credits(person_id: int,
|
||||
page: int = 1,
|
||||
count: int = 20,
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
根据人物ID查询人物参演作品
|
||||
"""
|
||||
medias = BangumiChain().person_credits(person_id=person_id)
|
||||
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 []
|
||||
|
||||
|
||||
|
||||
@@ -15,10 +15,11 @@ from app.db import get_db
|
||||
from app.db.models.subscribe import Subscribe
|
||||
from app.db.models.subscribehistory import SubscribeHistory
|
||||
from app.db.models.user import User
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.db.user_oper import get_current_active_user
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import MediaType, EventType
|
||||
from app.schemas.types import MediaType, EventType, SystemConfigKey
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -108,6 +109,7 @@ def update_subscribe(
|
||||
if not subscribe:
|
||||
return schemas.Response(success=False, message="订阅不存在")
|
||||
# 避免更新缺失集数
|
||||
old_subscribe_dict = subscribe.to_dict()
|
||||
subscribe_dict = subscribe_in.dict()
|
||||
if not subscribe_in.lack_episode:
|
||||
# 没有缺失集数时,缺失集数清空,避免更新为0
|
||||
@@ -125,7 +127,8 @@ def update_subscribe(
|
||||
# 发送订阅调整事件
|
||||
eventmanager.send_event(EventType.SubscribeModified, {
|
||||
"subscribe_id": subscribe.id,
|
||||
"subscribe_info": subscribe_dict,
|
||||
"old_subscribe_info": old_subscribe_dict,
|
||||
"subscribe_info": subscribe.to_dict(),
|
||||
})
|
||||
return schemas.Response(success=True)
|
||||
|
||||
@@ -145,9 +148,16 @@ def update_subscribe_status(
|
||||
valid_states = ["R", "P", "S"]
|
||||
if state not in valid_states:
|
||||
return schemas.Response(success=False, message="无效的订阅状态")
|
||||
old_subscribe_dict = subscribe.to_dict()
|
||||
subscribe.update(db, {
|
||||
"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)
|
||||
|
||||
|
||||
@@ -212,11 +222,18 @@ def reset_subscribes(
|
||||
"""
|
||||
subscribe = Subscribe.get(db, subid)
|
||||
if subscribe:
|
||||
old_subscribe_dict = subscribe.to_dict()
|
||||
subscribe.update(db, {
|
||||
"note": [],
|
||||
"lack_episode": subscribe.total_episode,
|
||||
"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=False, message="订阅不存在")
|
||||
|
||||
@@ -497,6 +514,42 @@ def subscribe_fork(
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/follow", summary="查询已Follow的订阅分享人", response_model=List[str])
|
||||
def followed_subscribers(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询已Follow的订阅分享人
|
||||
"""
|
||||
return SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
|
||||
|
||||
|
||||
@router.post("/follow", summary="Follow订阅分享人", response_model=schemas.Response)
|
||||
def follow_subscriber(
|
||||
share_uid: str = None,
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
Follow订阅分享人
|
||||
"""
|
||||
subscribers = SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
|
||||
if share_uid and share_uid not in subscribers:
|
||||
subscribers.append(share_uid)
|
||||
SystemConfigOper().set(SystemConfigKey.FollowSubscribers, subscribers)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.delete("/follow", summary="取消Follow订阅分享人", response_model=schemas.Response)
|
||||
def unfollow_subscriber(
|
||||
share_uid: str = None,
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
取消Follow订阅分享人
|
||||
"""
|
||||
subscribers = SystemConfigOper().get(SystemConfigKey.FollowSubscribers) or []
|
||||
if share_uid and share_uid in subscribers:
|
||||
subscribers.remove(share_uid)
|
||||
SystemConfigOper().set(SystemConfigKey.FollowSubscribers, subscribers)
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/shares", summary="查询分享的订阅", response_model=List[schemas.SubscribeShare])
|
||||
def popular_subscribes(
|
||||
name: str = None,
|
||||
|
||||
@@ -118,6 +118,11 @@ def tmdb_person_credits(person_id: int,
|
||||
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:
|
||||
"""
|
||||
@@ -126,6 +131,11 @@ def tmdb_movies(sort_by: str = "popularity.desc",
|
||||
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)
|
||||
|
||||
|
||||
@@ -133,6 +143,11 @@ def tmdb_movies(sort_by: str = "popularity.desc",
|
||||
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:
|
||||
"""
|
||||
@@ -141,6 +156,11 @@ def tmdb_tvs(sort_by: str = "popularity.desc",
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ class BangumiChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
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]:
|
||||
"""
|
||||
获取Bangumi信息
|
||||
|
||||
@@ -444,7 +444,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
for file in files:
|
||||
self.scrape_metadata(fileitem=file,
|
||||
meta=meta, mediainfo=mediainfo,
|
||||
init_folder=False, parent=fileitem)
|
||||
init_folder=False, parent=fileitem,
|
||||
overwrite=overwrite)
|
||||
# 生成目录内图片文件
|
||||
if init_folder:
|
||||
# 图片
|
||||
@@ -515,7 +516,8 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
||||
self.scrape_metadata(fileitem=file,
|
||||
meta=meta, mediainfo=mediainfo,
|
||||
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和图片
|
||||
if init_folder:
|
||||
# 识别文件夹名称
|
||||
|
||||
@@ -295,6 +295,8 @@ class MessageChain(ChainBase):
|
||||
return
|
||||
else:
|
||||
best_version = True
|
||||
# 转换用户名
|
||||
mp_name = self.useroper.get_name(**{f"{channel.name.lower()}_userid": userid}) if channel else None
|
||||
# 添加订阅,状态为N
|
||||
self.subscribechain.add(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
@@ -304,7 +306,7 @@ class MessageChain(ChainBase):
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
username=mp_name or username,
|
||||
best_version=best_version)
|
||||
elif cache_type == "Torrent":
|
||||
if int(text) == 0:
|
||||
@@ -505,6 +507,8 @@ class MessageChain(ChainBase):
|
||||
note = downloaded
|
||||
else:
|
||||
note = None
|
||||
# 转换用户名
|
||||
mp_name = self.useroper.get_name(**{f"{channel.name.lower()}_userid": userid}) if channel else None
|
||||
# 添加订阅,状态为R
|
||||
self.subscribechain.add(title=_current_media.title,
|
||||
year=_current_media.year,
|
||||
@@ -514,7 +518,7 @@ class MessageChain(ChainBase):
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
username=mp_name or username,
|
||||
state="R",
|
||||
note=note)
|
||||
|
||||
|
||||
@@ -157,8 +157,15 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||
def tmdb_movies(self, sort_by: str = "popularity.desc", with_genres: str = "",
|
||||
with_original_language: str = "", page: int = 1) -> Any:
|
||||
def tmdb_movies(self, 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) -> List[dict]:
|
||||
"""
|
||||
TMDB热门电影
|
||||
"""
|
||||
@@ -166,13 +173,25 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
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 []
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||
def tmdb_tvs(self, sort_by: str = "popularity.desc", with_genres: str = "",
|
||||
with_original_language: str = "zh|en|ja|ko", page: int = 1) -> Any:
|
||||
def tmdb_tvs(self, sort_by: str = "popularity.desc",
|
||||
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热门电视剧
|
||||
"""
|
||||
@@ -180,12 +199,17 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
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 []
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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流行趋势
|
||||
"""
|
||||
@@ -194,7 +218,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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每日放送
|
||||
"""
|
||||
@@ -203,7 +227,24 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||
def douban_movie_showing(self, page: int = 1, count: int = 30) -> Any:
|
||||
def bangumi_discover(self, type: int = 2,
|
||||
cat: int = None,
|
||||
sort: str = 'rank',
|
||||
year: int = None,
|
||||
count: int = 30,
|
||||
page: int = 1) -> List[dict]:
|
||||
"""
|
||||
搜索Bangumi
|
||||
"""
|
||||
medias = self.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 []
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@cached(ttl=recommend_ttl, region=recommend_cache_region)
|
||||
def douban_movie_showing(self, page: int = 1, count: int = 30) -> List[dict]:
|
||||
"""
|
||||
豆瓣正在热映
|
||||
"""
|
||||
@@ -212,7 +253,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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 +263,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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 +273,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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
|
||||
"""
|
||||
@@ -241,7 +282,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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 +291,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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 +300,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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 +309,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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 +318,7 @@ class RecommendChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@log_execution_time(logger=logger)
|
||||
@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]:
|
||||
"""
|
||||
豆瓣热门电视剧
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,7 @@ import time
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Union, Tuple
|
||||
|
||||
from app import schemas
|
||||
from app.chain import ChainBase
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.media import MediaChain
|
||||
@@ -27,8 +28,6 @@ from app.helper.message import MessageHelper
|
||||
from app.helper.subscribe import SubscribeHelper
|
||||
from app.helper.torrent import TorrentHelper
|
||||
from app.log import logger
|
||||
from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo, SubscribeDownloadFileInfo, \
|
||||
SubscribeLibraryFileInfo
|
||||
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
@@ -166,21 +165,23 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
'downloader': self.__get_default_subscribe_config(mediainfo.type, "downloader") if not kwargs.get(
|
||||
"downloader") else kwargs.get("downloader"),
|
||||
'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)
|
||||
if not sid:
|
||||
logger.error(f'{mediainfo.title_year} {err_msg}')
|
||||
if not exist_ok and message:
|
||||
# 失败发回原用户
|
||||
self.post_message(Notification(channel=channel,
|
||||
source=source,
|
||||
mtype=NotificationType.Subscribe,
|
||||
title=f"{mediainfo.title_year} {metainfo.season} "
|
||||
f"添加订阅失败!",
|
||||
text=f"{err_msg}",
|
||||
image=mediainfo.get_message_image(),
|
||||
userid=userid))
|
||||
self.post_message(schemas.Notification(channel=channel,
|
||||
source=source,
|
||||
mtype=NotificationType.Subscribe,
|
||||
title=f"{mediainfo.title_year} {metainfo.season} "
|
||||
f"添加订阅失败!",
|
||||
text=f"{err_msg}",
|
||||
image=mediainfo.get_message_image(),
|
||||
userid=userid))
|
||||
return None, err_msg
|
||||
elif message:
|
||||
logger.info(f'{mediainfo.title_year} {metainfo.season} 添加订阅成功')
|
||||
@@ -193,12 +194,12 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
else:
|
||||
link = settings.MP_DOMAIN('#/subscribe/movie?tab=mysub')
|
||||
# 订阅成功按规则发送消息
|
||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||
title=f"{mediainfo.title_year} {metainfo.season} 已添加订阅",
|
||||
text=text,
|
||||
image=mediainfo.get_message_image(),
|
||||
link=link,
|
||||
username=username))
|
||||
self.post_message(schemas.Notification(mtype=NotificationType.Subscribe,
|
||||
title=f"{mediainfo.title_year} {metainfo.season} 已添加订阅",
|
||||
text=text,
|
||||
image=mediainfo.get_message_image(),
|
||||
link=link,
|
||||
username=username))
|
||||
# 发送事件
|
||||
EventManager().send_event(EventType.SubscribeAdded, {
|
||||
"subscribe_id": sid,
|
||||
@@ -409,7 +410,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
def finish_subscribe_or_not(self, subscribe: Subscribe, meta: MetaBase, mediainfo: MediaInfo,
|
||||
downloads: List[Context] = None,
|
||||
lefts: Dict[Union[int | str], Dict[int, NotExistMediaInfo]] = None,
|
||||
lefts: Dict[Union[int | str], Dict[int, schemas.NotExistMediaInfo]] = None,
|
||||
force: bool = False):
|
||||
"""
|
||||
判断是否应完成订阅
|
||||
@@ -464,18 +465,16 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
# 从系统配置获取默认订阅站点
|
||||
default_sites = self.systemconfig.get(SystemConfigKey.RssSites) or []
|
||||
# 如果订阅未指定站点信息,直接返回默认站点
|
||||
# 如果订阅未指定站点,直接返回默认站点
|
||||
if not subscribe.sites:
|
||||
return default_sites
|
||||
# 如果默认订阅站点未设置,直接返回订阅指定站点
|
||||
if not default_sites:
|
||||
return subscribe.sites or []
|
||||
# 尝试解析订阅中的站点数据
|
||||
user_sites = subscribe.sites
|
||||
# 计算 user_sites 和 default_sites 的交集
|
||||
intersection_sites = [site for site in user_sites if site in default_sites]
|
||||
# 如果交集与原始订阅不一致,更新数据库
|
||||
if set(intersection_sites) != set(user_sites):
|
||||
self.subscribeoper.update(subscribe.id, {
|
||||
"sites": intersection_sites
|
||||
})
|
||||
# 如果交集为空,返回默认站点
|
||||
return intersection_sites if intersection_sites else default_sites
|
||||
|
||||
@@ -574,9 +573,9 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
continue
|
||||
|
||||
# 有自定义识别词时,需要判断是否需要重新识别
|
||||
apply_words = None
|
||||
if custom_words_list:
|
||||
_, apply_words = WordsMatcher().prepare(torrent_info.title,
|
||||
# 使用org_string,应用一次后理论上不能再次应用
|
||||
_, apply_words = WordsMatcher().prepare(torrent_meta.org_string,
|
||||
custom_words=custom_words_list)
|
||||
if apply_words:
|
||||
logger.info(
|
||||
@@ -584,6 +583,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
# 重新识别元数据
|
||||
torrent_meta = MetaInfo(title=torrent_info.title, subtitle=torrent_info.description,
|
||||
custom_words=custom_words_list)
|
||||
# 更新元数据缓存
|
||||
context.meta_info = torrent_meta
|
||||
# 媒体信息需要重新识别
|
||||
torrent_mediainfo = None
|
||||
|
||||
@@ -594,8 +595,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
torrent_mediainfo = self.recognize_media(meta=torrent_meta)
|
||||
if torrent_mediainfo:
|
||||
# 更新种子缓存
|
||||
if not apply_words:
|
||||
context.media_info = torrent_mediainfo
|
||||
context.media_info = torrent_mediainfo
|
||||
else:
|
||||
# 通过标题匹配兜底
|
||||
logger.warn(
|
||||
@@ -607,9 +607,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
logger.info(
|
||||
f'{mediainfo.title_year} 通过标题匹配到可选资源:{torrent_info.site_name} - {torrent_info.title}')
|
||||
torrent_mediainfo = mediainfo
|
||||
# 更新种子缓存
|
||||
if not apply_words:
|
||||
context.media_info = mediainfo
|
||||
context.media_info = torrent_mediainfo
|
||||
else:
|
||||
continue
|
||||
|
||||
@@ -784,6 +782,68 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
})
|
||||
logger.info(f'{subscribe.name} 订阅元数据更新完成')
|
||||
|
||||
def follow(self):
|
||||
"""
|
||||
刷新follow的用户分享,并自动添加订阅
|
||||
"""
|
||||
follow_users: List[str] = self.systemconfig.get(SystemConfigKey.FollowSubscribers)
|
||||
if not follow_users:
|
||||
return
|
||||
share_subs = self.subscribehelper.get_shares()
|
||||
logger.info(f'开始刷新follow用户分享订阅 ...')
|
||||
success_count = 0
|
||||
for share_sub in share_subs:
|
||||
uid = share_sub.get("share_uid")
|
||||
if uid and uid in follow_users:
|
||||
# 订阅已存在则跳过
|
||||
if self.subscribeoper.exists(tmdbid=share_sub.get("tmdbid"),
|
||||
doubanid=share_sub.get("doubanid"),
|
||||
season=share_sub.get("season")):
|
||||
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()):
|
||||
if not hasattr(schemas.Subscribe(), key):
|
||||
share_sub.pop(key)
|
||||
# 类型转换
|
||||
subscribe_in = schemas.Subscribe(**share_sub)
|
||||
mtype = MediaType(subscribe_in.type)
|
||||
# 豆瓣标题处理
|
||||
if subscribe_in.doubanid or subscribe_in.bangumiid:
|
||||
meta = MetaInfo(subscribe_in.name)
|
||||
subscribe_in.name = meta.name
|
||||
subscribe_in.season = meta.begin_season
|
||||
# 标题转换
|
||||
if subscribe_in.name:
|
||||
title = subscribe_in.name
|
||||
else:
|
||||
title = None
|
||||
sid, message = SubscribeChain().add(mtype=mtype,
|
||||
title=title,
|
||||
year=subscribe_in.year,
|
||||
tmdbid=subscribe_in.tmdbid,
|
||||
season=subscribe_in.season,
|
||||
doubanid=subscribe_in.doubanid,
|
||||
bangumiid=subscribe_in.bangumiid,
|
||||
username="订阅分享",
|
||||
best_version=subscribe_in.best_version,
|
||||
save_path=subscribe_in.save_path,
|
||||
search_imdbid=subscribe_in.search_imdbid,
|
||||
custom_words=subscribe_in.custom_words,
|
||||
media_category=subscribe_in.media_category,
|
||||
filter_groups=subscribe_in.filter_groups,
|
||||
exist_ok=True)
|
||||
if sid:
|
||||
success_count += 1
|
||||
logger.info(f'follow用户分享订阅 {title} 添加成功')
|
||||
else:
|
||||
logger.error(f'follow用户分享订阅 {title} 添加失败:{message}')
|
||||
logger.info(f'follow用户分享订阅刷新完成,共添加 {success_count} 个订阅')
|
||||
|
||||
def __update_subscribe_note(self, subscribe: Subscribe, downloads: List[Context]):
|
||||
"""
|
||||
更新已下载信息到note字段
|
||||
@@ -840,7 +900,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
return note
|
||||
return []
|
||||
|
||||
def __update_lack_episodes(self, lefts: Dict[Union[int, str], Dict[int, NotExistMediaInfo]],
|
||||
def __update_lack_episodes(self, lefts: Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]],
|
||||
subscribe: Subscribe,
|
||||
mediainfo: MediaInfo,
|
||||
update_date: bool = False):
|
||||
@@ -895,11 +955,11 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
else:
|
||||
link = settings.MP_DOMAIN('#/subscribe/movie?tab=mysub')
|
||||
# 完成订阅按规则发送消息
|
||||
self.post_message(Notification(mtype=NotificationType.Subscribe,
|
||||
title=f'{mediainfo.title_year} {meta.season} 已完成{msgstr}',
|
||||
image=mediainfo.get_message_image(),
|
||||
link=link,
|
||||
username=subscribe.username))
|
||||
self.post_message(schemas.Notification(mtype=NotificationType.Subscribe,
|
||||
title=f'{mediainfo.title_year} {meta.season} 已完成{msgstr}',
|
||||
image=mediainfo.get_message_image(),
|
||||
link=link,
|
||||
username=subscribe.username))
|
||||
# 发送事件
|
||||
EventManager().send_event(EventType.SubscribeComplete, {
|
||||
"subscribe_id": subscribe.id,
|
||||
@@ -919,9 +979,9 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
"""
|
||||
subscribes = self.subscribeoper.list()
|
||||
if not subscribes:
|
||||
self.post_message(Notification(channel=channel,
|
||||
source=source,
|
||||
title='没有任何订阅!', userid=userid))
|
||||
self.post_message(schemas.Notification(channel=channel,
|
||||
source=source,
|
||||
title='没有任何订阅!', userid=userid))
|
||||
return
|
||||
title = f"共有 {len(subscribes)} 个订阅,回复对应指令操作: " \
|
||||
f"\n- 删除订阅:/subscribe_delete [id]" \
|
||||
@@ -937,8 +997,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
f"[{subscribe.total_episode - (subscribe.lack_episode or subscribe.total_episode)}"
|
||||
f"/{subscribe.total_episode}]")
|
||||
# 发送列表
|
||||
self.post_message(Notification(channel=channel, source=source,
|
||||
title=title, text='\n'.join(messages), userid=userid))
|
||||
self.post_message(schemas.Notification(channel=channel, source=source,
|
||||
title=title, text='\n'.join(messages), userid=userid))
|
||||
|
||||
def remote_delete(self, arg_str: str, channel: MessageChannel,
|
||||
userid: Union[str, int] = None, source: str = None):
|
||||
@@ -946,9 +1006,9 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
删除订阅
|
||||
"""
|
||||
if not arg_str:
|
||||
self.post_message(Notification(channel=channel, source=source,
|
||||
title="请输入正确的命令格式:/subscribe_delete [id],"
|
||||
"[id]为订阅编号", userid=userid))
|
||||
self.post_message(schemas.Notification(channel=channel, source=source,
|
||||
title="请输入正确的命令格式:/subscribe_delete [id],"
|
||||
"[id]为订阅编号", userid=userid))
|
||||
return
|
||||
arg_strs = str(arg_str).split()
|
||||
for arg_str in arg_strs:
|
||||
@@ -958,8 +1018,8 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
subscribe_id = int(arg_str)
|
||||
subscribe = self.subscribeoper.get(subscribe_id)
|
||||
if not subscribe:
|
||||
self.post_message(Notification(channel=channel, source=source,
|
||||
title=f"订阅编号 {subscribe_id} 不存在!", userid=userid))
|
||||
self.post_message(schemas.Notification(channel=channel, source=source,
|
||||
title=f"订阅编号 {subscribe_id} 不存在!", userid=userid))
|
||||
return
|
||||
# 删除订阅
|
||||
self.subscribeoper.delete(subscribe_id)
|
||||
@@ -973,13 +1033,13 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
|
||||
@staticmethod
|
||||
def __get_subscribe_no_exits(subscribe_name: str,
|
||||
no_exists: Dict[Union[int, str], Dict[int, NotExistMediaInfo]],
|
||||
no_exists: Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]],
|
||||
mediakey: Union[str, int],
|
||||
begin_season: int,
|
||||
total_episode: int,
|
||||
start_episode: int,
|
||||
downloaded_episodes: List[int] = None
|
||||
) -> Tuple[bool, Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]:
|
||||
) -> Tuple[bool, Dict[Union[int, str], Dict[int, schemas.NotExistMediaInfo]]]:
|
||||
"""
|
||||
根据订阅开始集数和总集数,结合TMDB信息计算当前订阅的缺失集数
|
||||
:param subscribe_name: 订阅名称
|
||||
@@ -1029,7 +1089,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
# 与原集列表取交集
|
||||
episodes = list(set(episode_list).intersection(set(new_episodes)))
|
||||
# 更新集合
|
||||
no_exists[mediakey][begin_season] = NotExistMediaInfo(
|
||||
no_exists[mediakey][begin_season] = schemas.NotExistMediaInfo(
|
||||
season=begin_season,
|
||||
episodes=episodes,
|
||||
total_episode=total_episode,
|
||||
@@ -1056,7 +1116,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
if not episodes:
|
||||
return True, {}
|
||||
# 更新集合
|
||||
no_exists[mediakey][begin_season] = NotExistMediaInfo(
|
||||
no_exists[mediakey][begin_season] = schemas.NotExistMediaInfo(
|
||||
season=begin_season,
|
||||
episodes=episodes,
|
||||
total_episode=total,
|
||||
@@ -1070,7 +1130,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
# 如果存在已下载剧集,则差集为空时,说明所有均已存在
|
||||
if not episodes:
|
||||
return True, {}
|
||||
no_exists[mediakey][begin_season] = NotExistMediaInfo(
|
||||
no_exists[mediakey][begin_season] = schemas.NotExistMediaInfo(
|
||||
season=begin_season,
|
||||
episodes=episodes,
|
||||
total_episode=total_episode,
|
||||
@@ -1157,7 +1217,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
"min_seeders_time": default_rule.get("min_seeders_time"),
|
||||
}.items() if value is not None}
|
||||
|
||||
def subscribe_files_info(self, subscribe: Subscribe) -> Optional[SubscrbieInfo]:
|
||||
def subscribe_files_info(self, subscribe: Subscribe) -> Optional[schemas.SubscrbieInfo]:
|
||||
"""
|
||||
订阅相关的下载和文件信息
|
||||
"""
|
||||
@@ -1165,10 +1225,10 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
return
|
||||
|
||||
# 返回订阅数据
|
||||
subscribe_info = SubscrbieInfo()
|
||||
subscribe_info = schemas.SubscrbieInfo()
|
||||
|
||||
# 所有集的数据
|
||||
episodes: Dict[int, SubscribeEpisodeInfo] = {}
|
||||
episodes: Dict[int, schemas.SubscribeEpisodeInfo] = {}
|
||||
if subscribe.tmdbid and subscribe.type == MediaType.TV.value:
|
||||
# 查询TMDB中的集信息
|
||||
tmdb_episodes = self.tmdbchain.tmdb_episodes(
|
||||
@@ -1177,7 +1237,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
)
|
||||
if tmdb_episodes:
|
||||
for episode in tmdb_episodes:
|
||||
info = SubscribeEpisodeInfo()
|
||||
info = schemas.SubscribeEpisodeInfo()
|
||||
info.title = episode.name
|
||||
info.description = episode.overview
|
||||
info.backdrop = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/w500${episode.still_path}"
|
||||
@@ -1185,12 +1245,12 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
elif subscribe.type == MediaType.TV.value:
|
||||
# 根据开始结束集计算集信息
|
||||
for i in range(subscribe.start_episode or 1, subscribe.total_episode + 1):
|
||||
info = SubscribeEpisodeInfo()
|
||||
info = schemas.SubscribeEpisodeInfo()
|
||||
info.title = f'第 {i} 集'
|
||||
episodes[i] = info
|
||||
else:
|
||||
# 电影
|
||||
info = SubscribeEpisodeInfo()
|
||||
info = schemas.SubscribeEpisodeInfo()
|
||||
info.title = subscribe.name
|
||||
episodes[0] = info
|
||||
|
||||
@@ -1205,7 +1265,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
# 识别文件名
|
||||
file_meta = MetaInfo(file.filepath)
|
||||
# 下载文件信息
|
||||
file_info = SubscribeDownloadFileInfo(
|
||||
file_info = schemas.SubscribeDownloadFileInfo(
|
||||
torrent_title=his.torrent_name,
|
||||
site_name=his.torrent_site,
|
||||
downloader=file.downloader,
|
||||
@@ -1248,7 +1308,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
# 识别文件名
|
||||
file_meta = MetaInfo(fileitem.path)
|
||||
# 媒体库文件信息
|
||||
file_info = SubscribeLibraryFileInfo(
|
||||
file_info = schemas.SubscribeLibraryFileInfo(
|
||||
storage=fileitem.storage,
|
||||
file_path=fileitem.path,
|
||||
)
|
||||
@@ -1308,7 +1368,7 @@ class SubscribeChain(ChainBase, metaclass=Singleton):
|
||||
# 对于电视剧,构造缺失的媒体信息
|
||||
no_exists = {
|
||||
mediakey: {
|
||||
subscribe.season: NotExistMediaInfo(
|
||||
subscribe.season: schemas.NotExistMediaInfo(
|
||||
season=subscribe.season,
|
||||
episodes=[],
|
||||
total_episode=subscribe.total_episode,
|
||||
|
||||
@@ -14,19 +14,38 @@ class TmdbChain(ChainBase, metaclass=Singleton):
|
||||
TheMovieDB处理链,单例运行
|
||||
"""
|
||||
|
||||
def tmdb_discover(self, mtype: MediaType, sort_by: str, with_genres: str,
|
||||
with_original_language: str, page: int = 1) -> Optional[List[MediaInfo]]:
|
||||
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]]:
|
||||
"""
|
||||
:param mtype: 媒体类型
|
||||
:param sort_by: 排序方式
|
||||
:param with_genres: 类型
|
||||
:param with_original_language: 语言
|
||||
:param with_keywords: 关键字
|
||||
:param with_watch_providers: 提供商
|
||||
:param vote_average: 评分
|
||||
:param vote_count: 评分人数
|
||||
:param release_date: 上映日期
|
||||
:param page: 页码
|
||||
:return: 媒体信息列表
|
||||
"""
|
||||
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_keywords=with_keywords,
|
||||
with_watch_providers=with_watch_providers,
|
||||
vote_average=vote_average,
|
||||
vote_count=vote_count,
|
||||
release_date=release_date,
|
||||
page=page)
|
||||
|
||||
def tmdb_trending(self, page: int = 1) -> Optional[List[MediaInfo]]:
|
||||
|
||||
@@ -326,7 +326,7 @@ class JobManager:
|
||||
# 计算状态为完成的任务数
|
||||
if __mediaid__ not in self._job_view:
|
||||
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:
|
||||
"""
|
||||
@@ -453,9 +453,12 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
||||
if transferinfo.transfer_type in ["move"]:
|
||||
# 所有成功的业务
|
||||
tasks = self.jobview.success_tasks(task.mediainfo, task.meta.begin_season)
|
||||
# 记录已处理的种子hash
|
||||
processed_hashes = set()
|
||||
for t in tasks:
|
||||
# 下载器hash
|
||||
if t.download_hash:
|
||||
if t.download_hash and t.download_hash not in processed_hashes:
|
||||
processed_hashes.add(t.download_hash)
|
||||
if self.remove_torrents(t.download_hash, downloader=t.downloader):
|
||||
logger.info(f"移动模式删除种子成功:{t.download_hash} ")
|
||||
# 删除残留目录
|
||||
@@ -660,22 +663,22 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
||||
if transfer_history:
|
||||
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
|
||||
# 更新队列任务
|
||||
curr_task = self.jobview.remove_task(task.fileitem)
|
||||
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 task.target_path:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import threading
|
||||
import traceback
|
||||
from typing import Any, Union, Dict, Optional
|
||||
@@ -303,7 +304,7 @@ class Command(metaclass=Singleton):
|
||||
)
|
||||
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'])
|
||||
if args_num > 0:
|
||||
if cmd_data:
|
||||
|
||||
@@ -35,6 +35,17 @@ class CacheBackend(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def exists(self, key: str, region: str = DEFAULT_CACHE_REGION) -> bool:
|
||||
"""
|
||||
判断缓存键是否存在
|
||||
|
||||
:param key: 缓存的键
|
||||
:param region: 缓存的区
|
||||
:return: 存在返回 True,否则返回 False
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get(self, key: str, region: str = DEFAULT_CACHE_REGION) -> Any:
|
||||
"""
|
||||
@@ -79,6 +90,30 @@ class CacheBackend(ABC):
|
||||
"""
|
||||
return f"region:{region}" if region else "region:default"
|
||||
|
||||
@staticmethod
|
||||
def get_cache_key(func, args, kwargs):
|
||||
"""
|
||||
获取缓存的键,通过哈希函数对函数的参数进行处理
|
||||
:param func: 被装饰的函数
|
||||
:param args: 位置参数
|
||||
:param kwargs: 关键字参数
|
||||
:return: 缓存键
|
||||
"""
|
||||
signature = inspect.signature(func)
|
||||
# 绑定传入的参数并应用默认值
|
||||
bound = signature.bind(*args, **kwargs)
|
||||
bound.apply_defaults()
|
||||
# 忽略第一个参数,如果它是实例(self)或类(cls)
|
||||
parameters = list(signature.parameters.keys())
|
||||
if parameters and parameters[0] in ("self", "cls"):
|
||||
bound.arguments.pop(parameters[0], None)
|
||||
# 按照函数签名顺序提取参数值列表
|
||||
keys = [
|
||||
bound.arguments[param] for param in signature.parameters if param in bound.arguments
|
||||
]
|
||||
# 使用有序参数生成缓存键
|
||||
return f"{func.__name__}_{hashkey(*keys)}"
|
||||
|
||||
|
||||
class CacheToolsBackend(CacheBackend):
|
||||
"""
|
||||
@@ -130,6 +165,19 @@ class CacheToolsBackend(CacheBackend):
|
||||
# 设置缓存值
|
||||
region_cache[key] = value
|
||||
|
||||
def exists(self, key: str, region: str = DEFAULT_CACHE_REGION) -> bool:
|
||||
"""
|
||||
判断缓存键是否存在
|
||||
|
||||
:param key: 缓存的键
|
||||
:param region: 缓存的区
|
||||
:return: 存在返回 True,否则返回 False
|
||||
"""
|
||||
region_cache = self.__get_region_cache(region)
|
||||
if region_cache is None:
|
||||
return False
|
||||
return key in region_cache
|
||||
|
||||
def get(self, key: str, region: str = DEFAULT_CACHE_REGION) -> Any:
|
||||
"""
|
||||
获取缓存的值
|
||||
@@ -194,6 +242,10 @@ class RedisBackend(CacheBackend):
|
||||
- Pickle 反序列化可能存在安全风险,需进一步重构调用来源,避免复杂对象缓存
|
||||
"""
|
||||
|
||||
# 类型缓存集合,针对非容器简单类型
|
||||
_complex_serializable_types = set()
|
||||
_simple_serializable_types = set()
|
||||
|
||||
def __init__(self, redis_url: str = "redis://localhost", ttl: int = 1800):
|
||||
"""
|
||||
初始化 Redis 缓存实例
|
||||
@@ -234,19 +286,42 @@ class RedisBackend(CacheBackend):
|
||||
logger.error(f"Failed to set Redis maxmemory or policy: {e}")
|
||||
|
||||
@staticmethod
|
||||
def serialize(value: Any) -> bytes:
|
||||
def is_container_type(t):
|
||||
return t in (list, dict, tuple, set)
|
||||
|
||||
@classmethod
|
||||
def serialize(cls, value: Any) -> bytes:
|
||||
"""
|
||||
将值序列化为二进制数据,根据序列化方式标识格式
|
||||
"""
|
||||
try:
|
||||
# 尝试 JSON 序列化
|
||||
return b"JSON" + b"\x00" + json.dumps(value).encode("utf-8")
|
||||
except TypeError:
|
||||
# 如果 JSON 序列化失败,使用 Pickle 序列化
|
||||
return b"PICKLE" + b"\x00" + pickle.dumps(value)
|
||||
vt = type(value)
|
||||
# 针对非容器类型使用缓存策略
|
||||
if not cls.is_container_type(vt):
|
||||
# 如果已知需要复杂序列化
|
||||
if vt in cls._complex_serializable_types:
|
||||
return b"PICKLE" + b"\x00" + pickle.dumps(value)
|
||||
# 如果已知可以简单序列化
|
||||
if vt in cls._simple_serializable_types:
|
||||
json_data = json.dumps(value).encode("utf-8")
|
||||
return b"JSON" + b"\x00" + json_data
|
||||
# 对于未知的非容器类型,尝试简单序列化,如抛出异常,再使用复杂序列化
|
||||
try:
|
||||
json_data = json.dumps(value).encode("utf-8")
|
||||
cls._simple_serializable_types.add(vt)
|
||||
return b"JSON" + b"\x00" + json_data
|
||||
except TypeError:
|
||||
cls._complex_serializable_types.add(vt)
|
||||
return b"PICKLE" + b"\x00" + pickle.dumps(value)
|
||||
# 针对容器类型,每次尝试简单序列化,不使用缓存
|
||||
else:
|
||||
try:
|
||||
json_data = json.dumps(value).encode("utf-8")
|
||||
return b"JSON" + b"\x00" + json_data
|
||||
except TypeError:
|
||||
return b"PICKLE" + b"\x00" + pickle.dumps(value)
|
||||
|
||||
@staticmethod
|
||||
def deserialize(value: bytes) -> Any:
|
||||
@classmethod
|
||||
def deserialize(cls, value: bytes) -> Any:
|
||||
"""
|
||||
将二进制数据反序列化为原始值,根据格式标识区分序列化方式
|
||||
"""
|
||||
@@ -294,6 +369,21 @@ class RedisBackend(CacheBackend):
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to set key: {key} in region: {region}, error: {e}")
|
||||
|
||||
def exists(self, key: str, region: str = DEFAULT_CACHE_REGION) -> bool:
|
||||
"""
|
||||
判断缓存键是否存在
|
||||
|
||||
:param key: 缓存的键
|
||||
:param region: 缓存的区
|
||||
:return: 存在返回 True,否则返回 False
|
||||
"""
|
||||
try:
|
||||
redis_key = self.get_redis_key(region, key)
|
||||
return self.client.exists(redis_key) == 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to exists key: {key} region: {region}, error: {e}")
|
||||
return False
|
||||
|
||||
def get(self, key: str, region: str = DEFAULT_CACHE_REGION) -> Optional[Any]:
|
||||
"""
|
||||
获取缓存的值
|
||||
@@ -392,7 +482,7 @@ def cached(region: Optional[str] = None, maxsize: int = 1000, ttl: int = 1800,
|
||||
:param maxsize: 缓存的最大条目数,默认值为 1000
|
||||
:param ttl: 缓存的存活时间,单位秒,默认值为 1800
|
||||
:param skip_none: 跳过 None 缓存,默认为 True
|
||||
:param skip_empty: 跳过空值缓存(如 [], {}, "", set()),默认为 False
|
||||
:param skip_empty: 跳过空值缓存(如 None, [], {}, "", set()),默认为 False
|
||||
:return: 装饰器函数
|
||||
"""
|
||||
|
||||
@@ -405,34 +495,11 @@ def cached(region: Optional[str] = None, maxsize: int = 1000, ttl: int = 1800,
|
||||
"""
|
||||
if skip_none and value is None:
|
||||
return False
|
||||
# if disable_empty and value in [[], {}, "", set()]:
|
||||
# if skip_empty and value in [None, [], {}, "", set()]:
|
||||
if skip_empty and not value:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_cache_key(func, args, kwargs):
|
||||
"""
|
||||
获取缓存的键,通过哈希函数对函数的参数进行处理
|
||||
:param func: 被装饰的函数
|
||||
:param args: 位置参数
|
||||
:param kwargs: 关键字参数
|
||||
:return: 缓存键
|
||||
"""
|
||||
# 获取方法签名
|
||||
signature = inspect.signature(func)
|
||||
resolved_kwargs = {}
|
||||
# 获取默认值并结合传递的参数(如果有)
|
||||
for param, value in signature.parameters.items():
|
||||
if param in kwargs:
|
||||
# 使用显式传递的参数
|
||||
resolved_kwargs[param] = kwargs[param]
|
||||
elif value.default is not inspect.Parameter.empty:
|
||||
# 没有传递参数时使用默认值
|
||||
resolved_kwargs[param] = value.default
|
||||
# 构造缓存键,忽略实例(self 或 cls)
|
||||
params_to_hash = args[1:] if len(args) > 1 else []
|
||||
return f"{func.__name__}_{hashkey(*params_to_hash, **resolved_kwargs)}"
|
||||
|
||||
def decorator(func):
|
||||
|
||||
# 获取缓存区
|
||||
@@ -441,7 +508,7 @@ def cached(region: Optional[str] = None, maxsize: int = 1000, ttl: int = 1800,
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# 获取缓存键
|
||||
cache_key = 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)
|
||||
if should_cache(cached_value):
|
||||
|
||||
@@ -262,6 +262,8 @@ class MediaInfo:
|
||||
runtime: int = None
|
||||
# 下一集
|
||||
next_episode_to_air: dict = field(default_factory=dict)
|
||||
# 内容分级
|
||||
content_rating: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
# 设置媒体信息
|
||||
|
||||
@@ -259,9 +259,6 @@ class MetaBase(object):
|
||||
except Exception as err:
|
||||
logger.debug(f'识别集失败:{str(err)} - {traceback.format_exc()}')
|
||||
return
|
||||
if self.total_episode:
|
||||
self.begin_episode = 1
|
||||
self.end_episode = self.total_episode
|
||||
self.type = MediaType.TV
|
||||
self._subtitle_flag = True
|
||||
return
|
||||
|
||||
@@ -73,6 +73,18 @@ class SubscribeHistory(Base):
|
||||
result = db.query(SubscribeHistory).filter(
|
||||
SubscribeHistory.type == mtype
|
||||
).order_by(
|
||||
SubscribeHistory.date.desc()
|
||||
SubscribeHistory.date.desc()
|
||||
).offset((page - 1) * count).limit(count).all()
|
||||
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")
|
||||
subscribe = SubscribeHistory(**kwargs)
|
||||
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 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):
|
||||
"""
|
||||
新增用户
|
||||
@@ -90,3 +96,16 @@ class UserOper(DbOper):
|
||||
if settings:
|
||||
return settings.get(key)
|
||||
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
|
||||
|
||||
@@ -182,7 +182,7 @@ class SubscribeHelper(metaclass=Singleton):
|
||||
return False, res.json().get("message")
|
||||
|
||||
@cached(region=_shares_cache_region)
|
||||
def get_shares(self, name: str, page: int = 1, count: int = 30) -> List[dict]:
|
||||
def get_shares(self, name: str = None, page: int = 1, count: int = 30) -> List[dict]:
|
||||
"""
|
||||
获取订阅分享数据
|
||||
"""
|
||||
|
||||
@@ -165,3 +165,12 @@ class BangumiModule(_ModuleBase):
|
||||
if credits_info:
|
||||
return [MediaInfo(bangumi_info=credit) for credit in credits_info]
|
||||
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 = {
|
||||
"discover": "v0/subjects",
|
||||
"search": "search/subjects/%s?type=2",
|
||||
"calendar": "calendar",
|
||||
"detail": "v0/subjects/%s",
|
||||
@@ -30,14 +31,17 @@ class BangumiApi(object):
|
||||
|
||||
@classmethod
|
||||
@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
|
||||
params = {}
|
||||
if kwargs:
|
||||
params.update(kwargs)
|
||||
resp = cls._req.get_res(url=req_url, params=params)
|
||||
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:
|
||||
print(e)
|
||||
return None
|
||||
@@ -194,3 +198,11 @@ class BangumiApi(object):
|
||||
for item in result:
|
||||
ret_list.append(item)
|
||||
return ret_list
|
||||
|
||||
def discover(self, **kwargs):
|
||||
"""
|
||||
发现
|
||||
"""
|
||||
return self.__invoke(self._urls["discover"],
|
||||
key="data",
|
||||
_ts=datetime.strftime(datetime.now(), '%Y%m%d'), **kwargs)
|
||||
|
||||
@@ -420,7 +420,7 @@ class FanartModule(_ModuleBase):
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@cached(maxsize=settings.CACHE_CONF["fanart"], ttl=settings.CACHE_CONF["meta"])
|
||||
@cached(maxsize=settings.CACHE_CONF["fanart"], ttl=settings.CACHE_CONF["meta"], skip_none=False)
|
||||
def __request_fanart(cls, media_type: MediaType, queryid: Union[str, int]) -> Optional[dict]:
|
||||
if media_type == MediaType.MOVIE:
|
||||
image_url = cls._movie_url % queryid
|
||||
|
||||
@@ -16,7 +16,8 @@ from app.helper.module import ModuleHelper
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
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.utils.system import SystemUtils
|
||||
|
||||
@@ -368,7 +369,7 @@ class FileManagerModule(_ModuleBase):
|
||||
# 覆盖模式
|
||||
overwrite_mode = target_directory.overwrite_mode
|
||||
# 是否需要刮削
|
||||
need_scrape = scrape or target_directory.scraping
|
||||
need_scrape = target_directory.scraping if scrape is None else scrape
|
||||
# 目标存储类型
|
||||
if not target_storage:
|
||||
target_storage = target_directory.library_storage
|
||||
@@ -763,6 +764,21 @@ class FileManagerModule(_ModuleBase):
|
||||
target_item = target_oper.get_folder(target_path)
|
||||
if not target_item:
|
||||
return None, f"获取目标目录失败:{target_path}"
|
||||
event_data = TransferInterceptEventData(
|
||||
fileitem=fileitem,
|
||||
target_storage=target_storage,
|
||||
target_path=target_path,
|
||||
transfer_type=transfer_type
|
||||
)
|
||||
event = eventmanager.send_event(ChainEventType.TransferRename, 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,
|
||||
target_storage=target_storage,
|
||||
@@ -830,6 +846,24 @@ class FileManagerModule(_ModuleBase):
|
||||
target_file.unlink()
|
||||
logger.info(f"正在整理文件:【{fileitem.storage}】{fileitem.path} 到 【{target_storage}】{target_file},"
|
||||
f"操作类型:{transfer_type}")
|
||||
event_data = TransferInterceptEventData(
|
||||
fileitem=fileitem,
|
||||
target_storage=target_storage,
|
||||
target_path=target_file,
|
||||
transfer_type=transfer_type,
|
||||
options={
|
||||
"over_flag": over_flag
|
||||
}
|
||||
)
|
||||
event = eventmanager.send_event(ChainEventType.TransferRename, 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
|
||||
new_item, errmsg = self.__transfer_command(fileitem=fileitem,
|
||||
target_storage=target_storage,
|
||||
target_file=target_file,
|
||||
@@ -1127,7 +1161,7 @@ class FileManagerModule(_ModuleBase):
|
||||
if episode.episode_number == meta.begin_episode:
|
||||
episode_date = episode.air_date
|
||||
break
|
||||
|
||||
|
||||
return {
|
||||
# 标题
|
||||
"title": __convert_invalid_characters(mediainfo.title),
|
||||
|
||||
@@ -102,7 +102,7 @@ class StorageBase(metaclass=ABCMeta):
|
||||
"""
|
||||
获取父目录
|
||||
"""
|
||||
return self.get_folder(Path(fileitem.path).parent)
|
||||
return self.get_item(Path(fileitem.path).parent)
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, fileitem: schemas.FileItem) -> bool:
|
||||
|
||||
@@ -70,10 +70,13 @@ class Alist(StorageBase, metaclass=Singleton):
|
||||
@cached(maxsize=1, ttl=60 * 60 * 24 * 2 - 60 * 5)
|
||||
def __generate_token(self) -> str:
|
||||
"""
|
||||
使用账号密码生成一个临时token
|
||||
如果设置永久令牌则返回永久令牌,否则使用账号密码生成一个临时 token
|
||||
缓存2天,提前5分钟更新
|
||||
"""
|
||||
conf = self.get_conf()
|
||||
token = conf.get("token")
|
||||
if token:
|
||||
return str(token)
|
||||
resp: Response = RequestUtils(headers={
|
||||
'Content-Type': 'application/json'
|
||||
}).post_res(
|
||||
|
||||
@@ -208,9 +208,16 @@ class NexusPhpSiteUserInfo(SiteParserBase):
|
||||
# 是否存在下页数据
|
||||
next_page = None
|
||||
next_page_text = html.xpath('//a[contains(.//text(), "下一页") or contains(.//text(), "下一頁") or contains(.//text(), ">")]/@href')
|
||||
if next_page_text:
|
||||
next_page = next_page_text[-1].strip()
|
||||
# fix up page url
|
||||
|
||||
#防止识别到详情页
|
||||
while next_page_text:
|
||||
next_page = next_page_text.pop().strip()
|
||||
if not next_page.startswith('details.php'):
|
||||
break;
|
||||
next_page = None
|
||||
|
||||
# fix up page url
|
||||
if next_page:
|
||||
if self.userid not in next_page:
|
||||
next_page = f'{next_page}&userid={self.userid}&type=seeding'
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ class Slack:
|
||||
return ""
|
||||
conversation_id = ""
|
||||
try:
|
||||
for result in self._client.conversations_list():
|
||||
for result in self._client.conversations_list(types="public_channel,private_channel"):
|
||||
if conversation_id:
|
||||
break
|
||||
for channel in result["channels"]:
|
||||
|
||||
@@ -356,26 +356,52 @@ class TheMovieDbModule(_ModuleBase):
|
||||
return None
|
||||
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]]:
|
||||
"""
|
||||
:param mtype: 媒体类型
|
||||
:param sort_by: 排序方式
|
||||
:param with_genres: 类型
|
||||
:param with_original_language: 语言
|
||||
:param with_keywords: 关键字
|
||||
:param with_watch_providers: 提供商
|
||||
:param vote_average: 评分
|
||||
:param vote_count: 评分人数
|
||||
:param release_date: 发布日期
|
||||
:param page: 页码
|
||||
:return: 媒体信息列表
|
||||
"""
|
||||
if mtype == MediaType.MOVIE:
|
||||
infos = self.tmdb.discover_movies(sort_by=sort_by,
|
||||
with_genres=with_genres,
|
||||
with_original_language=with_original_language,
|
||||
page=page)
|
||||
infos = self.tmdb.discover_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.gte": vote_average,
|
||||
"vote_count.gte": vote_count,
|
||||
"release_date.gte": release_date,
|
||||
"page": page
|
||||
})
|
||||
elif mtype == MediaType.TV:
|
||||
infos = self.tmdb.discover_tvs(sort_by=sort_by,
|
||||
with_genres=with_genres,
|
||||
with_original_language=with_original_language,
|
||||
page=page)
|
||||
infos = self.tmdb.discover_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.gte": vote_average,
|
||||
"vote_count.gte": vote_count,
|
||||
"first_air_date.gte": release_date,
|
||||
"page": page
|
||||
})
|
||||
else:
|
||||
return []
|
||||
if infos:
|
||||
|
||||
@@ -170,6 +170,9 @@ class TmdbScraper:
|
||||
DomUtils.add_node(doc, root, "genre", genre.get("name") or "")
|
||||
# 评分
|
||||
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
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ from lxml import etree
|
||||
from app.core.cache import cached
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.schemas import APIRateLimitException
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.limit import rate_limit_exponential
|
||||
from app.utils.string import StringUtils
|
||||
from .tmdbv3api import TMDb, Search, Movie, TV, Season, Episode, Discover, Trending, Person, Collection
|
||||
from .tmdbv3api.exceptions import TMDbException
|
||||
@@ -492,6 +494,7 @@ class TmdbApi:
|
||||
return ret_info
|
||||
|
||||
@cached(maxsize=settings.CACHE_CONF["tmdb"], ttl=settings.CACHE_CONF["meta"])
|
||||
@rate_limit_exponential(source="match_tmdb_web", base_wait=5, max_wait=1800, enable_logging=True)
|
||||
def match_web(self, name: str, mtype: MediaType) -> Optional[dict]:
|
||||
"""
|
||||
搜索TMDB网站,直接抓取结果,结果只有一条时才返回
|
||||
@@ -504,51 +507,56 @@ class TmdbApi:
|
||||
return {}
|
||||
logger.info("正在从TheDbMovie网站查询:%s ..." % name)
|
||||
tmdb_url = "https://www.themoviedb.org/search?query=%s" % quote(name)
|
||||
res = RequestUtils(timeout=5, ua=settings.USER_AGENT).get_res(url=tmdb_url)
|
||||
if res and res.status_code == 200:
|
||||
html_text = res.text
|
||||
if not html_text:
|
||||
return None
|
||||
try:
|
||||
tmdb_links = []
|
||||
html = etree.HTML(html_text)
|
||||
if mtype == MediaType.TV:
|
||||
links = html.xpath("//a[@data-id and @data-media-type='tv']/@href")
|
||||
else:
|
||||
links = html.xpath("//a[@data-id]/@href")
|
||||
for link in links:
|
||||
if not link or (not link.startswith("/tv") and not link.startswith("/movie")):
|
||||
continue
|
||||
if link not in tmdb_links:
|
||||
tmdb_links.append(link)
|
||||
if len(tmdb_links) == 1:
|
||||
tmdbinfo = self.get_info(
|
||||
mtype=MediaType.TV if tmdb_links[0].startswith("/tv") else MediaType.MOVIE,
|
||||
tmdbid=tmdb_links[0].split("/")[-1])
|
||||
if tmdbinfo:
|
||||
if mtype == MediaType.TV and tmdbinfo.get('media_type') != MediaType.TV:
|
||||
return {}
|
||||
if tmdbinfo.get('media_type') == MediaType.MOVIE:
|
||||
logger.info("%s 从WEB识别到 电影:TMDBID=%s, 名称=%s, 上映日期=%s" % (
|
||||
name,
|
||||
tmdbinfo.get('id'),
|
||||
tmdbinfo.get('title'),
|
||||
tmdbinfo.get('release_date')))
|
||||
else:
|
||||
logger.info("%s 从WEB识别到 电视剧:TMDBID=%s, 名称=%s, 首播日期=%s" % (
|
||||
name,
|
||||
tmdbinfo.get('id'),
|
||||
tmdbinfo.get('name'),
|
||||
tmdbinfo.get('first_air_date')))
|
||||
return tmdbinfo
|
||||
elif len(tmdb_links) > 1:
|
||||
logger.info("%s TMDB网站返回数据过多:%s" % (name, len(tmdb_links)))
|
||||
else:
|
||||
logger.info("%s TMDB网站未查询到媒体信息!" % name)
|
||||
except Exception as err:
|
||||
logger.error(f"从TheDbMovie网站查询出错:{str(err)}")
|
||||
return None
|
||||
return None
|
||||
res = RequestUtils(timeout=5, ua=settings.USER_AGENT, proxies=settings.PROXY).get_res(url=tmdb_url)
|
||||
if res is None:
|
||||
return None
|
||||
if res.status_code == 429:
|
||||
raise APIRateLimitException("触发TheDbMovie网站限流,获取媒体信息失败")
|
||||
if res.status_code != 200:
|
||||
return {}
|
||||
html_text = res.text
|
||||
if not html_text:
|
||||
return {}
|
||||
try:
|
||||
tmdb_links = []
|
||||
html = etree.HTML(html_text)
|
||||
if mtype == MediaType.TV:
|
||||
links = html.xpath("//a[@data-id and @data-media-type='tv']/@href")
|
||||
else:
|
||||
links = html.xpath("//a[@data-id]/@href")
|
||||
for link in links:
|
||||
if not link or (not link.startswith("/tv") and not link.startswith("/movie")):
|
||||
continue
|
||||
if link not in tmdb_links:
|
||||
tmdb_links.append(link)
|
||||
if len(tmdb_links) == 1:
|
||||
tmdbinfo = self.get_info(
|
||||
mtype=MediaType.TV if tmdb_links[0].startswith("/tv") else MediaType.MOVIE,
|
||||
tmdbid=tmdb_links[0].split("/")[-1])
|
||||
if tmdbinfo:
|
||||
if mtype == MediaType.TV and tmdbinfo.get('media_type') != MediaType.TV:
|
||||
return {}
|
||||
if tmdbinfo.get('media_type') == MediaType.MOVIE:
|
||||
logger.info("%s 从WEB识别到 电影:TMDBID=%s, 名称=%s, 上映日期=%s" % (
|
||||
name,
|
||||
tmdbinfo.get('id'),
|
||||
tmdbinfo.get('title'),
|
||||
tmdbinfo.get('release_date')))
|
||||
else:
|
||||
logger.info("%s 从WEB识别到 电视剧:TMDBID=%s, 名称=%s, 首播日期=%s" % (
|
||||
name,
|
||||
tmdbinfo.get('id'),
|
||||
tmdbinfo.get('name'),
|
||||
tmdbinfo.get('first_air_date')))
|
||||
return tmdbinfo
|
||||
elif len(tmdb_links) > 1:
|
||||
logger.info("%s TMDB网站返回数据过多:%s" % (name, len(tmdb_links)))
|
||||
else:
|
||||
logger.info("%s TMDB网站未查询到媒体信息!" % name)
|
||||
except Exception as err:
|
||||
logger.error(f"从TheDbMovie网站查询出错:{str(err)}")
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def get_info(self,
|
||||
mtype: MediaType,
|
||||
@@ -593,6 +601,8 @@ class TmdbApi:
|
||||
tmdb_info['genre_ids'] = __get_genre_ids(tmdb_info.get('genres'))
|
||||
# 别名和译名
|
||||
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)
|
||||
# 转换中文标题
|
||||
@@ -600,6 +610,68 @@ class TmdbApi:
|
||||
|
||||
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
|
||||
def __update_tmdbinfo_cn_title(tmdb_info: dict):
|
||||
"""
|
||||
@@ -692,6 +764,7 @@ class TmdbApi:
|
||||
"credits,"
|
||||
"alternative_titles,"
|
||||
"translations,"
|
||||
"release_dates,"
|
||||
"external_ids") -> Optional[dict]:
|
||||
"""
|
||||
获取电影的详情
|
||||
@@ -804,6 +877,7 @@ class TmdbApi:
|
||||
"credits,"
|
||||
"alternative_titles,"
|
||||
"translations,"
|
||||
"content_ratings,"
|
||||
"external_ids") -> Optional[dict]:
|
||||
"""
|
||||
获取电视剧的详情
|
||||
@@ -1072,18 +1146,17 @@ class TmdbApi:
|
||||
logger.error(str(e))
|
||||
return {}
|
||||
|
||||
def discover_movies(self, **kwargs) -> List[dict]:
|
||||
def discover_movies(self, params: dict) -> List[dict]:
|
||||
"""
|
||||
发现电影
|
||||
:param kwargs:
|
||||
:param params: 参数
|
||||
:return:
|
||||
"""
|
||||
if not self.discover:
|
||||
return []
|
||||
try:
|
||||
logger.debug(f"正在发现电影:{kwargs}...")
|
||||
params_tuple = tuple(kwargs.items())
|
||||
tmdbinfo = self.discover.discover_movies(params_tuple)
|
||||
logger.debug(f"正在发现电影:{params}...")
|
||||
tmdbinfo = self.discover.discover_movies(tuple(params.items()))
|
||||
if tmdbinfo:
|
||||
for info in tmdbinfo:
|
||||
info['media_type'] = MediaType.MOVIE
|
||||
@@ -1092,18 +1165,17 @@ class TmdbApi:
|
||||
logger.error(str(e))
|
||||
return []
|
||||
|
||||
def discover_tvs(self, **kwargs) -> List[dict]:
|
||||
def discover_tvs(self, params: dict) -> List[dict]:
|
||||
"""
|
||||
发现电视剧
|
||||
:param kwargs:
|
||||
:param params: 参数
|
||||
:return:
|
||||
"""
|
||||
if not self.discover:
|
||||
return []
|
||||
try:
|
||||
logger.debug(f"正在发现电视剧:{kwargs}...")
|
||||
params_tuple = tuple(kwargs.items())
|
||||
tmdbinfo = self.discover.discover_tv_shows(params_tuple)
|
||||
logger.debug(f"正在发现电视剧:{params}...")
|
||||
tmdbinfo = self.discover.discover_tv_shows(tuple(params.items()))
|
||||
if tmdbinfo:
|
||||
for info in tmdbinfo:
|
||||
info['media_type'] = MediaType.TV
|
||||
|
||||
@@ -225,7 +225,8 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
return self.plugindata.del_data(plugin_id, key)
|
||||
|
||||
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__}")
|
||||
self.chain.post_message(Notification(
|
||||
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):
|
||||
|
||||
@@ -92,6 +92,11 @@ class Scheduler(metaclass=Singleton):
|
||||
"func": SubscribeChain().refresh,
|
||||
"running": False,
|
||||
},
|
||||
"subscribe_follow": {
|
||||
"name": "关注的订阅分享",
|
||||
"func": SubscribeChain().follow,
|
||||
"running": False,
|
||||
},
|
||||
"transfer": {
|
||||
"name": "下载文件整理",
|
||||
"func": TransferChain().process,
|
||||
@@ -241,6 +246,18 @@ class Scheduler(metaclass=Singleton):
|
||||
}
|
||||
)
|
||||
|
||||
# 关注订阅分享(每1小时)
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
"interval",
|
||||
id="subscribe_follow",
|
||||
name="关注的订阅分享",
|
||||
hours=1,
|
||||
kwargs={
|
||||
'job_id': 'subscribe_follow'
|
||||
}
|
||||
)
|
||||
|
||||
# 下载器文件转移(每5分钟)
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Optional, Dict, Any, List, Set
|
||||
|
||||
from pydantic import BaseModel, Field, root_validator
|
||||
|
||||
from app.schemas import MessageChannel
|
||||
from app.schemas import MessageChannel, FileItem
|
||||
|
||||
|
||||
class BaseEventData(BaseModel):
|
||||
@@ -50,7 +50,7 @@ class AuthCredentials(ChainEventData):
|
||||
service: Optional[str] = Field(default=None, description="服务名称")
|
||||
|
||||
@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")
|
||||
if not grant_type:
|
||||
values["grant_type"] = "password"
|
||||
@@ -202,3 +202,33 @@ class ResourceDownloadEventData(ChainEventData):
|
||||
cancel: bool = Field(default=False, description="是否取消下载")
|
||||
source: 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="源文件")
|
||||
target_storage: str = Field(..., description="目标存储")
|
||||
target_path: Path = Field(..., description="目标路径")
|
||||
transfer_type: str = Field(..., description="整理方式")
|
||||
options: Optional[dict] = Field(None, description="其他参数")
|
||||
|
||||
# 输出参数
|
||||
cancel: bool = Field(default=False, description="是否取消整理")
|
||||
source: str = Field(default="未知拦截源", description="拦截源")
|
||||
reason: str = Field(default="", description="拦截原因")
|
||||
|
||||
@@ -75,6 +75,8 @@ class ChainEventType(Enum):
|
||||
CommandRegister = "command.register"
|
||||
# 整理重命名
|
||||
TransferRename = "transfer.rename"
|
||||
# 整理拦截
|
||||
TransferIntercept = "transfer.intercept"
|
||||
# 资源选择
|
||||
ResourceSelection = "resource.selection"
|
||||
# 资源下载
|
||||
@@ -135,6 +137,8 @@ class SystemConfigKey(Enum):
|
||||
DefaultTvSubscribeConfig = "DefaultTvSubscribeConfig"
|
||||
# 用户站点认证参数
|
||||
UserSiteAuthParams = "UserSiteAuthParams"
|
||||
# Follow订阅分享者
|
||||
FollowSubscribers = "FollowSubscribers"
|
||||
|
||||
|
||||
# 处理进度Key字典
|
||||
|
||||
@@ -156,7 +156,8 @@ class ExponentialBackoffRateLimiter(BaseRateLimiter):
|
||||
with self.lock:
|
||||
self.next_allowed_time = current_time + self.current_wait
|
||||
self.current_wait = min(self.current_wait * self.backoff_factor, self.max_wait)
|
||||
self.log_warning(f"触发限流,将在 {self.current_wait} 秒后允许继续调用")
|
||||
wait_time = self.next_allowed_time - current_time
|
||||
self.log_warning(f"触发限流,将在 {wait_time:.2f} 秒后允许继续调用")
|
||||
|
||||
|
||||
# 时间窗口限流器
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = 'v2.2.3'
|
||||
FRONTEND_VERSION = 'v2.2.3'
|
||||
APP_VERSION = 'v2.2.6'
|
||||
FRONTEND_VERSION = 'v2.2.6'
|
||||
|
||||
Reference in New Issue
Block a user