Compare commits

...

89 Commits

Author SHA1 Message Date
jxxghp
8e309e8658 更新 version.py 2024-12-17 22:19:32 +08:00
jxxghp
3400a9f87a fix #3548 2024-12-17 12:44:37 +08:00
jxxghp
c6830059b2 Merge pull request #3548 from 0honus0/v2 2024-12-17 11:54:36 +08:00
honus
7e4a18b365 fix rclone __get_fileitem err 2024-12-17 00:18:52 +08:00
honus
9ecc8c14d8 fix rclone bug 2024-12-16 23:20:49 +08:00
jxxghp
91ba71ad23 Merge pull request #3546 from InfinityPacer/feature/subscribe 2024-12-16 19:47:30 +08:00
jxxghp
5ae8914060 Merge pull request #3545 from xianghuawe/v2 2024-12-16 19:46:18 +08:00
InfinityPacer
77c8f1244f Merge branch 'v2' of https://github.com/jxxghp/MoviePilot into feature/subscribe 2024-12-16 19:09:14 +08:00
InfinityPacer
5d5c8a0af7 feat(event): add SubscribeDeleted event 2024-12-16 19:09:00 +08:00
coder_wen
dcaf3e6678 fix: change alist.py upload api to put, fix big file upload over memory limit #3265 2024-12-16 15:14:16 +08:00
jxxghp
c0170a173c Merge pull request #3542 from DDS-Derek/dev 2024-12-16 12:56:19 +08:00
DDSRem
d182a7079d chore(deps): update dependency python-115 to v0.0.9.8.8.3 2024-12-16 12:28:50 +08:00
jxxghp
b5cc5653b2 Merge pull request #3536 from InfinityPacer/feature/subscribe 2024-12-15 07:56:06 +08:00
jxxghp
bdbd908b3a Merge pull request #3535 from InfinityPacer/feature/event 2024-12-15 07:55:15 +08:00
InfinityPacer
11fedb1ffc fix(download): optimize performance by checking binary content 2024-12-15 01:27:30 +08:00
InfinityPacer
7de82f6c0d fix(event): remove unnecessary code 2024-12-15 00:17:53 +08:00
jxxghp
782829c992 Merge pull request #3531 from InfinityPacer/feature/subscribe 2024-12-13 20:18:58 +08:00
InfinityPacer
6ab76453d4 feat(events): update episodes field to Download event 2024-12-13 20:05:40 +08:00
jxxghp
56767b92d7 Merge pull request #3524 from InfinityPacer/feature/subscribe 2024-12-12 17:29:17 +08:00
InfinityPacer
621df40c66 feat(event): add support for priority in event registration 2024-12-12 15:38:28 +08:00
jxxghp
ba7cb76640 Merge pull request #3519 from InfinityPacer/feature/subscribe 2024-12-11 22:27:24 +08:00
InfinityPacer
d353853472 feat(subscribe): add support for update movie downloaded note 2024-12-11 20:19:47 +08:00
InfinityPacer
1fcf5f4709 feat(subscribe): add state reset to 'R' on subscription reset 2024-12-11 20:01:10 +08:00
InfinityPacer
0ec4630461 fix(subscribe): avoid redundant updates for remaining episodes 2024-12-11 16:31:11 +08:00
InfinityPacer
fa45dea1aa fix(subscribe): prioritize update state when fininsh subscribe 2024-12-11 16:18:03 +08:00
InfinityPacer
2217583052 fix(subscribe): update missing episode logic and return status 2024-12-11 15:51:04 +08:00
InfinityPacer
f4dc7a133e fix(subscribe): update subscription state after download 2024-12-11 15:47:45 +08:00
jxxghp
26b1e64bad Merge pull request #3518 from InfinityPacer/feature/subscribe 2024-12-11 13:32:17 +08:00
InfinityPacer
a1d8af6521 fix(subscribe): update remove_site to set sites as an empty list 2024-12-11 12:39:13 +08:00
jxxghp
9fb3d093ff Merge pull request #3517 from wikrin/match_rule 2024-12-11 06:54:58 +08:00
jxxghp
8c9b37a12f Merge pull request #3516 from InfinityPacer/feature/subscribe 2024-12-11 06:53:42 +08:00
Attente
73e4596d1a feat(filter): add publish time filter for torrents
- 在 `TorrentInfo` 类中添加 `pub_minutes` 方法以计算自发布以来的`分钟`数
- 在 FilterModule 中实现发布时间过滤
- 支持发布时间的单值和范围比较
2024-12-10 23:36:54 +08:00
InfinityPacer
83798e6823 feat(event): add multiple IDs to source with json 2024-12-10 21:23:52 +08:00
InfinityPacer
6d9595b643 feat(event): add source tracking in download event 2024-12-10 18:50:50 +08:00
jxxghp
dc047d949d Merge pull request #3511 from wikrin/offset 2024-12-10 07:13:10 +08:00
Attente
a31b4bc0a1 refactor(app): improve episode offset calculation
- Remove unnecessary try-except block
2024-12-10 00:37:50 +08:00
Attente
94b8633803 手动整理中集数偏移可不使用集数定位 2024-12-10 00:32:01 +08:00
jxxghp
107e85033f Merge pull request #3507 from InfinityPacer/feature/subscribe 2024-12-09 19:38:48 +08:00
InfinityPacer
eea8060182 feat(plugin): add username support for post_message 2024-12-09 19:27:25 +08:00
jxxghp
83f7869de4 Merge pull request #3506 from thsrite/v2 2024-12-09 17:32:49 +08:00
thsrite
4f0eff8b88 fix site vip level ignores ratio warning 2024-12-09 16:43:05 +08:00
jxxghp
58b438c345 fix #3343 2024-12-08 08:51:58 +08:00
jxxghp
bc57bb1a78 更新 version.py 2024-12-07 07:41:14 +08:00
jxxghp
e08ab0dd33 Merge pull request #3341 from InfinityPacer/feature/subscribe 2024-12-07 07:39:28 +08:00
InfinityPacer
64bfa246ae fix: replace is None with is_(None) for proper SQLAlchemy filter 2024-12-07 01:09:03 +08:00
jxxghp
cde4db1a56 v2.1.2 2024-12-06 15:55:56 +08:00
jxxghp
29ae910953 fix build 2024-12-06 12:31:29 +08:00
jxxghp
314f90cc40 upgrade python-115 2024-12-06 12:30:13 +08:00
jxxghp
1c22e3d024 Merge pull request #3337 from InfinityPacer/feature/subscribe
feat(event): add ResourceDownload event for cancel download
2024-12-06 11:17:34 +08:00
InfinityPacer
233d62479f feat(event): add options to ResourceDownloadEventData 2024-12-06 10:47:56 +08:00
jxxghp
6974f2ebd7 Merge pull request #3335 from mackerel-12138/fix_scraper 2024-12-06 06:53:24 +08:00
InfinityPacer
c030166cf5 feat(event): send events for resource download based on source 2024-12-06 02:08:36 +08:00
InfinityPacer
4c511eaea6 chore(event): update ResourceDownloadEventData comment 2024-12-06 02:06:00 +08:00
InfinityPacer
6e443a1127 feat(event): add ResourceDownload event for cancel download 2024-12-06 01:55:44 +08:00
InfinityPacer
896e473c41 fix(event): filter and handle only enabled event handlers 2024-12-06 01:54:51 +08:00
zhanglijun
12f10ebedf fix: 音轨文件重命名整理 2024-12-06 00:40:38 +08:00
jxxghp
ba9f85747c Merge pull request #3330 from InfinityPacer/feature/subscribe 2024-12-05 17:10:47 +08:00
InfinityPacer
2954c02a7c feat(subscribe): add subscription status update API 2024-12-05 16:24:05 +08:00
InfinityPacer
312e602f12 feat(subscribe): add Pending and Suspended subscription states 2024-12-05 16:22:09 +08:00
InfinityPacer
ed37fcbb07 feat(subscribe): update get_by_state to handle multiple states 2024-12-05 16:20:14 +08:00
jxxghp
6acf8fbf00 Merge pull request #3324 from InfinityPacer/feature/subscribe 2024-12-05 06:54:45 +08:00
InfinityPacer
a1e178c805 feat(event): add ResourceSelection event for update resource contexts 2024-12-04 20:21:57 +08:00
jxxghp
922e2fc446 Merge pull request #3323 from Aqr-K/feat-module 2024-12-04 18:19:15 +08:00
jxxghp
db4c8cb3f2 Merge pull request #3322 from InfinityPacer/feature/subscribe 2024-12-04 18:18:32 +08:00
Aqr-K
1c578746fe fix(module): 补全 indexer 缺少 get_subtype 方法
- 补全 `indexer` 缺少 `get_subtype` 方法。
- 增加 `get_running_subtype_module` 方法,可结合 `types` 快速获取单个运行中的 `module` 。
2024-12-04 18:14:56 +08:00
InfinityPacer
68f88117b6 feat(events): add episodes field to DownloadAdded event for unpack 2024-12-04 16:11:35 +08:00
jxxghp
108c0a89f6 Merge pull request #3320 from InfinityPacer/feature/subscribe 2024-12-04 12:18:19 +08:00
InfinityPacer
92dacdf6a2 fix(subscribe): add RLock to prevent duplicate subscription downloads 2024-12-04 11:07:45 +08:00
InfinityPacer
6aa684d6a5 fix(subscribe): handle case when no subscriptions are found 2024-12-04 11:03:32 +08:00
InfinityPacer
efece8cc56 fix(subscribe): add check for None before updating subscription 2024-12-04 10:27:33 +08:00
jxxghp
383c8ca19a Merge pull request #3313 from Aqr-K/feat-module 2024-12-03 18:09:49 +08:00
jxxghp
0a73681280 Merge pull request #3315 from InfinityPacer/feature/scheduler 2024-12-03 18:09:23 +08:00
InfinityPacer
c1ecda280c fix #3312 2024-12-03 17:33:00 +08:00
Aqr-K
825fc35134 feat(modules): 增加子级 type 分类。
- 在 `types` 里,针对各个模块的类型进行子级分类。
- 为每个模块统一添加 `get_subtype` 方法,这样一来,能更精准快速地区分与调用子类的每个模块,又能获取 ModuleType 所规定的分类以及对应存在的子模块类型支持列表,从而有效解决当下调用时需繁琐遍历每个 module 以获取 get_name 或 _channel 的问题。
- 解决因消息渠道前端返回所保存的 type 与后端规定值不一致,而需要频繁调用 _channel 私有方法才能获取分类所可能产生的问题。
2024-12-03 14:57:19 +08:00
jxxghp
8f543ca602 Merge pull request #3309 from yxlimo/tmdbid-for-downloader 2024-12-03 06:55:36 +08:00
yxlimo
f0ecc1a497 fix: return last record when get downloadhistory by hash 2024-12-02 22:55:57 +08:00
jxxghp
71f170a1ad Merge pull request #3293 from wikrin/v2 2024-12-01 10:23:51 +08:00
Attente
3709b65b0e fix(api): correct variable reference in media scraping logic
- Change incorrect reference from media_info to mediainfo
2024-12-01 03:40:30 +08:00
jxxghp
9d6eb0f1e1 Merge pull request #3291 from mackerel-12138/fix_scraper 2024-11-30 16:06:04 +08:00
jxxghp
c93306147b Merge pull request #3290 from mackerel-12138/fix_poster 2024-11-30 16:05:11 +08:00
zhanglijun
5e8f924a2f fix: 修复指定tmdbid刮削时tmdbid丢失问题 2024-11-30 15:57:47 +08:00
zhanglijun
54988d6397 fix: 修复fanart季图片下载缺失/错误的问题 2024-11-30 13:51:30 +08:00
jxxghp
112761dc4c Merge pull request #3287 from InfinityPacer/feature/security 2024-11-30 07:15:52 +08:00
InfinityPacer
ef20508840 feat(auth): handle service instance retrieval with proper null check 2024-11-30 01:14:36 +08:00
InfinityPacer
589a1765ed feat(auth): support specifying service for authentication 2024-11-30 01:04:48 +08:00
jxxghp
2c666e24f3 Merge pull request #3283 from InfinityPacer/feature/subscribe 2024-11-29 21:12:25 +08:00
InfinityPacer
168e3c5533 fix(subscribe): move state update to finally to prevent duplicates 2024-11-29 18:56:19 +08:00
jxxghp
cda8b2573a Merge pull request #3282 from InfinityPacer/feature/subscribe 2024-11-29 16:47:56 +08:00
InfinityPacer
4cb4eb23b8 fix(subscribe): prevent fallback to search rules if not defined 2024-11-29 16:15:37 +08:00
55 changed files with 1189 additions and 642 deletions

View File

@@ -5,10 +5,7 @@ on:
branches: branches:
- v2 - v2
paths: paths:
- '.github/workflows/build.yml'
- 'Dockerfile'
- 'version.py' - 'version.py'
- 'requirements.in'
jobs: jobs:
Docker-build: Docker-build:

View File

@@ -51,7 +51,7 @@ def download(
torrent_info=torrentinfo torrent_info=torrentinfo
) )
did = DownloadChain().download_single(context=context, username=current_user.name, did = DownloadChain().download_single(context=context, username=current_user.name,
downloader=downloader, save_path=save_path) downloader=downloader, save_path=save_path, source="Manual")
if not did: if not did:
return schemas.Response(success=False, message="任务添加失败") return schemas.Response(success=False, message="任务添加失败")
return schemas.Response(success=True, data={ return schemas.Response(success=True, data={
@@ -84,7 +84,7 @@ def add(
torrent_info=torrentinfo torrent_info=torrentinfo
) )
did = DownloadChain().download_single(context=context, username=current_user.name, did = DownloadChain().download_single(context=context, username=current_user.name,
downloader=downloader, save_path=save_path) downloader=downloader, save_path=save_path, source="Manual")
if not did: if not did:
return schemas.Response(success=False, message="任务添加失败") return schemas.Response(success=False, message="任务添加失败")
return schemas.Response(success=True, data={ return schemas.Response(success=True, data={

View File

@@ -111,7 +111,7 @@ def scrape(fileitem: schemas.FileItem,
scrape_path = Path(fileitem.path) scrape_path = Path(fileitem.path)
meta = MetaInfoPath(scrape_path) meta = MetaInfoPath(scrape_path)
mediainfo = chain.recognize_by_meta(meta) mediainfo = chain.recognize_by_meta(meta)
if not media_info: if not mediainfo:
return schemas.Response(success=False, message="刮削失败,无法识别媒体信息") return schemas.Response(success=False, message="刮削失败,无法识别媒体信息")
if storage == "local": if storage == "local":
if not scrape_path.exists(): if not scrape_path.exists():

View File

@@ -8,6 +8,7 @@ from app import schemas
from app.chain.subscribe import SubscribeChain from app.chain.subscribe import SubscribeChain
from app.core.config import settings from app.core.config import settings
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.core.event import eventmanager
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.core.security import verify_token, verify_apitoken from app.core.security import verify_token, verify_apitoken
from app.db import get_db from app.db import get_db
@@ -17,7 +18,7 @@ from app.db.models.user import User
from app.db.user_oper import get_current_active_user from app.db.user_oper import get_current_active_user
from app.helper.subscribe import SubscribeHelper from app.helper.subscribe import SubscribeHelper
from app.scheduler import Scheduler from app.scheduler import Scheduler
from app.schemas.types import MediaType from app.schemas.types import MediaType, EventType
router = APIRouter() router = APIRouter()
@@ -124,6 +125,27 @@ def update_subscribe(
return schemas.Response(success=True) return schemas.Response(success=True)
@router.put("/status/{subid}", summary="更新订阅状态", response_model=schemas.Response)
def update_subscribe_status(
subid: int,
state: str,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
"""
更新订阅状态
"""
subscribe = Subscribe.get(db, subid)
if not subscribe:
return schemas.Response(success=False, message="订阅不存在")
valid_states = ["R", "P", "S"]
if state not in valid_states:
return schemas.Response(success=False, message="无效的订阅状态")
subscribe.update(db, {
"state": state
})
return schemas.Response(success=True)
@router.get("/media/{mediaid}", summary="查询订阅", response_model=schemas.Subscribe) @router.get("/media/{mediaid}", summary="查询订阅", response_model=schemas.Subscribe)
def subscribe_mediaid( def subscribe_mediaid(
mediaid: str, mediaid: str,
@@ -186,8 +208,9 @@ def reset_subscribes(
subscribe = Subscribe.get(db, subid) subscribe = Subscribe.get(db, subid)
if subscribe: if subscribe:
subscribe.update(db, { subscribe.update(db, {
"note": "", "note": [],
"lack_episode": subscribe.total_episode "lack_episode": subscribe.total_episode,
"state": "R"
}) })
return schemas.Response(success=True) return schemas.Response(success=True)
return schemas.Response(success=False, message="订阅不存在") return schemas.Response(success=False, message="订阅不存在")
@@ -252,17 +275,27 @@ def delete_subscribe_by_mediaid(
""" """
根据TMDBID或豆瓣ID删除订阅 tmdb:/douban: 根据TMDBID或豆瓣ID删除订阅 tmdb:/douban:
""" """
delete_subscribes = []
if mediaid.startswith("tmdb:"): if mediaid.startswith("tmdb:"):
tmdbid = mediaid[5:] tmdbid = mediaid[5:]
if not tmdbid or not str(tmdbid).isdigit(): if not tmdbid or not str(tmdbid).isdigit():
return schemas.Response(success=False) return schemas.Response(success=False)
Subscribe().delete_by_tmdbid(db, int(tmdbid), season) subscribes = Subscribe().get_by_tmdbid(db, int(tmdbid), season)
delete_subscribes.extend(subscribes)
elif mediaid.startswith("douban:"): elif mediaid.startswith("douban:"):
doubanid = mediaid[7:] doubanid = mediaid[7:]
if not doubanid: if not doubanid:
return schemas.Response(success=False) return schemas.Response(success=False)
Subscribe().delete_by_doubanid(db, doubanid) subscribe = Subscribe().get_by_doubanid(db, doubanid)
if subscribe:
delete_subscribes.append(subscribe)
for subscribe in delete_subscribes:
Subscribe().delete(db, subscribe.id)
# 发送事件
eventmanager.send_event(EventType.SubscribeDeleted, {
"subscribe_id": subscribe.id,
"subscribe": subscribe.to_dict()
})
return schemas.Response(success=True) return schemas.Response(success=True)
@@ -485,6 +518,11 @@ def delete_subscribe(
subscribe = Subscribe.get(db, subscribe_id) subscribe = Subscribe.get(db, subscribe_id)
if subscribe: if subscribe:
subscribe.delete(db, subscribe_id) subscribe.delete(db, subscribe_id)
# 发送事件
eventmanager.send_event(EventType.SubscribeDeleted, {
"subscribe_id": subscribe_id,
"subscribe": subscribe.to_dict()
})
# 统计订阅 # 统计订阅
SubscribeHelper().sub_done_async({ SubscribeHelper().sub_done_async({
"tmdbid": subscribe.tmdbid, "tmdbid": subscribe.tmdbid,

View File

@@ -388,6 +388,8 @@ def ruletest(title: str,
# 根据标题查询媒体信息 # 根据标题查询媒体信息
media_info = SearchChain().recognize_media(MetaInfo(title=title, subtitle=subtitle)) media_info = SearchChain().recognize_media(MetaInfo(title=title, subtitle=subtitle))
if not media_info:
return schemas.Response(success=False, message="未识别到媒体信息!")
# 过滤 # 过滤
result = SearchChain().filter_torrents(rule_groups=[rulegroup.name], result = SearchChain().filter_torrents(rule_groups=[rulegroup.name],

View File

@@ -1,8 +1,7 @@
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app import schemas from app import schemas
@@ -14,50 +13,11 @@ from app.core.security import verify_token, verify_apitoken
from app.db import get_db from app.db import get_db
from app.db.models.transferhistory import TransferHistory from app.db.models.transferhistory import TransferHistory
from app.db.user_oper import get_current_active_superuser from app.db.user_oper import get_current_active_superuser
from app.schemas import MediaType, FileItem from app.schemas import MediaType, FileItem, ManualTransferItem
router = APIRouter() router = APIRouter()
class ManualTransferItem(BaseModel):
# 文件项
fileitem: FileItem = None
# 日志ID
logid: Optional[int] = None
# 目标存储
target_storage: Optional[str] = None
# 目标路径
target_path: Optional[str] = None
# TMDB ID
tmdbid: Optional[int] = None
# 豆瓣ID
doubanid: Optional[str] = None
# 类型
type_name: Optional[str] = None
# 季号
season: Optional[int] = None
# 整理方式
transfer_type: Optional[str] = None
# 自定义格式
episode_format: Optional[str] = None
# 指定集数
episode_detail: Optional[str] = None
# 指定PART
episode_part: Optional[str] = None
# 集数偏移
episode_offset: Optional[str] = None
# 最小文件大小
min_filesize: Optional[int] = 0
# 刮削
scrape: bool = False
# 媒体库类型子目录
library_type_folder: Optional[bool] = None
# 媒体库类别子目录
library_category_folder: Optional[bool] = None
# 复用历史识别信息
from_history: Optional[bool] = False
@router.get("/name", summary="查询整理后的名称", response_model=schemas.Response) @router.get("/name", summary="查询整理后的名称", response_model=schemas.Response)
def query_name(path: str, filetype: str, def query_name(path: str, filetype: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:

View File

@@ -20,7 +20,8 @@ from app.helper.message import MessageHelper
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 import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification from app.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType from app.schemas.event import ResourceSelectionEventData, ResourceDownloadEventData
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType, ChainEventType
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from app.utils.string import StringUtils from app.utils.string import StringUtils
@@ -191,7 +192,7 @@ class DownloadChain(ChainBase):
logger.error(f"下载种子文件失败:{torrent.title} - {torrent_url}") logger.error(f"下载种子文件失败:{torrent.title} - {torrent_url}")
self.post_message(Notification( self.post_message(Notification(
channel=channel, channel=channel,
source=source, source=source if channel else None,
mtype=NotificationType.Manual, mtype=NotificationType.Manual,
title=f"{torrent.title} 种子下载失败!", title=f"{torrent.title} 种子下载失败!",
text=f"错误信息:{error_msg}\n站点:{torrent.site_name}", text=f"错误信息:{error_msg}\n站点:{torrent.site_name}",
@@ -203,7 +204,8 @@ class DownloadChain(ChainBase):
def download_single(self, context: Context, torrent_file: Path = None, def download_single(self, context: Context, torrent_file: Path = None,
episodes: Set[int] = None, episodes: Set[int] = None,
channel: MessageChannel = None, source: str = None, channel: MessageChannel = None,
source: str = None,
downloader: str = None, downloader: str = None,
save_path: str = None, save_path: str = None,
userid: Union[str, int] = None, userid: Union[str, int] = None,
@@ -215,13 +217,38 @@ class DownloadChain(ChainBase):
:param torrent_file: 种子文件路径 :param torrent_file: 种子文件路径
:param episodes: 需要下载的集数 :param episodes: 需要下载的集数
:param channel: 通知渠道 :param channel: 通知渠道
:param source: 通知来源 :param source: 来源消息通知、Subscribe、Manual等
:param downloader: 下载器 :param downloader: 下载器
:param save_path: 保存路径 :param save_path: 保存路径
:param userid: 用户ID :param userid: 用户ID
:param username: 调用下载的用户名/插件名 :param username: 调用下载的用户名/插件名
:param media_category: 自定义媒体类别 :param media_category: 自定义媒体类别
""" """
# 发送资源下载事件,允许外部拦截下载
event_data = ResourceDownloadEventData(
context=context,
episodes=episodes or context.meta_info.episode_list,
channel=channel,
origin=source,
downloader=downloader,
options={
"save_path": save_path,
"userid": userid,
"username": username,
"media_category": media_category
}
)
# 触发资源下载事件
event = eventmanager.send_event(ChainEventType.ResourceDownload, event_data)
if event and event.event_data:
event_data: ResourceDownloadEventData = event.event_data
# 如果事件被取消,跳过资源下载
if event_data.cancel:
logger.debug(
f"Resource download canceled by event: {event_data.source},"
f"Reason: {event_data.reason}")
return None
_torrent = context.torrent_info _torrent = context.torrent_info
_media = context.media_info _media = context.media_info
_meta = context.meta_info _meta = context.meta_info
@@ -318,7 +345,8 @@ class DownloadChain(ChainBase):
username=username, username=username,
channel=channel.value if channel else None, channel=channel.value if channel else None,
date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), date=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
media_category=media_category media_category=media_category,
note={"source": source}
) )
# 登记下载文件 # 登记下载文件
@@ -355,7 +383,9 @@ class DownloadChain(ChainBase):
"hash": _hash, "hash": _hash,
"context": context, "context": context,
"username": username, "username": username,
"downloader": _downloader "downloader": _downloader,
"episodes": episodes or _meta.episode_list,
"source": source
}) })
else: else:
# 下载失败 # 下载失败
@@ -364,7 +394,7 @@ class DownloadChain(ChainBase):
# 只发送给对应渠道和用户 # 只发送给对应渠道和用户
self.post_message(Notification( self.post_message(Notification(
channel=channel, channel=channel,
source=source, source=source if channel else None,
mtype=NotificationType.Manual, mtype=NotificationType.Manual,
title="添加下载任务失败:%s %s" title="添加下载任务失败:%s %s"
% (_media.title_year, _meta.season_episode), % (_media.title_year, _meta.season_episode),
@@ -392,7 +422,7 @@ class DownloadChain(ChainBase):
:param no_exists: 缺失的剧集信息 :param no_exists: 缺失的剧集信息
:param save_path: 保存路径 :param save_path: 保存路径
:param channel: 通知渠道 :param channel: 通知渠道
:param source: 通知来源 :param source: 来源(消息通知、订阅、手工下载等)
:param userid: 用户ID :param userid: 用户ID
:param username: 调用下载的用户名/插件名 :param username: 调用下载的用户名/插件名
:param media_category: 自定义媒体类别 :param media_category: 自定义媒体类别
@@ -457,6 +487,22 @@ class DownloadChain(ChainBase):
return 9999 return 9999
return no_exist[season].total_episode return no_exist[season].total_episode
# 发送资源选择事件,允许外部修改上下文数据
logger.debug(f"Initial contexts: {len(contexts)} items, Downloader: {downloader}")
event_data = ResourceSelectionEventData(
contexts=contexts,
downloader=downloader,
origin=source
)
event = eventmanager.send_event(ChainEventType.ResourceSelection, event_data)
# 如果事件修改了上下文数据,使用更新后的数据
if event and event.event_data:
event_data: ResourceSelectionEventData = event.event_data
if event_data.updated and event_data.updated_contexts is not None:
logger.debug(f"Contexts updated by event: "
f"{len(event_data.updated_contexts)} items (source: {event_data.source})")
contexts = event_data.updated_contexts
# 分组排序 # 分组排序
contexts = TorrentHelper().sort_group_torrents(contexts) contexts = TorrentHelper().sort_group_torrents(contexts)

View File

@@ -477,7 +477,7 @@ class MediaChain(ChainBase, metaclass=Singleton):
if not file_meta.begin_episode: if not file_meta.begin_episode:
logger.warn(f"{filepath.name} 无法识别文件集数!") logger.warn(f"{filepath.name} 无法识别文件集数!")
return return
file_mediainfo = self.recognize_media(meta=file_meta) file_mediainfo = self.recognize_media(meta=file_meta, tmdbid=mediainfo.tmdb_id)
if not file_mediainfo: if not file_mediainfo:
logger.warn(f"{filepath.name} 无法识别文件媒体信息!") logger.warn(f"{filepath.name} 无法识别文件媒体信息!")
return return
@@ -552,6 +552,29 @@ class MediaChain(ChainBase, metaclass=Singleton):
if not parent: if not parent:
parent = self.storagechain.get_parent_item(fileitem) parent = self.storagechain.get_parent_item(fileitem)
__save_file(_fileitem=parent, _path=image_path, _content=content) __save_file(_fileitem=parent, _path=image_path, _content=content)
# 额外fanart季图片poster thumb banner
image_dict = self.metadata_img(mediainfo=mediainfo)
if image_dict:
for image_name, image_url in image_dict.items():
if image_name.startswith("season"):
image_path = filepath.with_name(image_name)
# 只下载当前刮削季的图片
image_season = "00" if "specials" in image_name else image_name[6:8]
if image_season != str(season_meta.begin_season).rjust(2, '0'):
logger.info(f"当前刮削季为:{season_meta.begin_season},跳过文件:{image_path}")
continue
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage,
path=image_path):
logger.info(f"已存在图片文件:{image_path}")
continue
# 下载图片
content = __download_image(image_url)
# 保存图片文件到当前目录
if content:
if not parent:
parent = self.storagechain.get_parent_item(fileitem)
__save_file(_fileitem=parent, _path=image_path, _content=content)
# 判断当前目录是不是剧集根目录 # 判断当前目录是不是剧集根目录
if not season_meta.season: if not season_meta.season:
# 是否已存在 # 是否已存在
@@ -570,6 +593,9 @@ class MediaChain(ChainBase, metaclass=Singleton):
image_dict = self.metadata_img(mediainfo=mediainfo) image_dict = self.metadata_img(mediainfo=mediainfo)
if image_dict: if image_dict:
for image_name, image_url in image_dict.items(): for image_name, image_url in image_dict.items():
# 不下载季图片
if image_name.startswith("season"):
continue
image_path = filepath / image_name image_path = filepath / image_name
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage,
path=image_path): path=image_path):

View File

@@ -111,6 +111,8 @@ class MessageChain(ChainBase):
info = self.message_parser(source=source, body=body, form=form, args=args) info = self.message_parser(source=source, body=body, form=form, args=args)
if not info: if not info:
return return
# 更新消息来源
source = info.source
# 渠道 # 渠道
channel = info.channel channel = info.channel
# 用户ID # 用户ID

View File

@@ -87,7 +87,8 @@ class SiteChain(ChainBase):
link=site.get("url") link=site.get("url")
)) ))
# 低分享率警告 # 低分享率警告
if userdata.ratio and float(userdata.ratio) < 1: if userdata.ratio and float(userdata.ratio) < 1 and not bool(
re.search(r"(贵宾|VIP?)", userdata.user_level, re.IGNORECASE)):
self.post_message(Notification( self.post_message(Notification(
mtype=NotificationType.SiteMessage, mtype=NotificationType.SiteMessage,
title=f"【站点分享率低预警】", title=f"【站点分享率低预警】",

View File

@@ -1,5 +1,7 @@
import copy import copy
import json
import random import random
import threading
import time import time
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional, Union, Tuple from typing import Dict, List, Optional, Union, Tuple
@@ -28,15 +30,17 @@ from app.log import logger
from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo, SubscribeDownloadFileInfo, \ from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo, SubscribeDownloadFileInfo, \
SubscribeLibraryFileInfo SubscribeLibraryFileInfo
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType
from app.utils.singleton import Singleton
class SubscribeChain(ChainBase): class SubscribeChain(ChainBase, metaclass=Singleton):
""" """
订阅管理处理链 订阅管理处理链
""" """
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._rlock = threading.RLock()
self.downloadchain = DownloadChain() self.downloadchain = DownloadChain()
self.downloadhis = DownloadHistoryOper() self.downloadhis = DownloadHistoryOper()
self.searchchain = SearchChain() self.searchchain = SearchChain()
@@ -234,14 +238,17 @@ class SubscribeChain(ChainBase):
""" """
订阅搜索 订阅搜索
:param sid: 订阅ID有值时只处理该订阅 :param sid: 订阅ID有值时只处理该订阅
:param state: 订阅状态 N:未搜索 R:已搜索 :param state: 订阅状态 N:新建, R:订阅中, P:待定, S:暂停
:param manual: 是否手动搜索 :param manual: 是否手动搜索
:return: 更新订阅状态为R或删除订阅 :return: 更新订阅状态为R或删除订阅
""" """
with self._rlock:
logger.debug(f"search lock acquired at {datetime.now()}")
if sid: if sid:
subscribes = [self.subscribeoper.get(sid)] subscribe = self.subscribeoper.get(sid)
subscribes = [subscribe] if subscribe else []
else: else:
subscribes = self.subscribeoper.list(state) subscribes = self.subscribeoper.list(self.get_states_for_search(state))
# 遍历订阅 # 遍历订阅
for subscribe in subscribes: for subscribe in subscribes:
if global_vars.is_system_stopped: if global_vars.is_system_stopped:
@@ -256,14 +263,12 @@ class SubscribeChain(ChainBase):
logger.debug(f"订阅标题:{subscribe.name} 新增小于1分钟暂不搜索...") logger.debug(f"订阅标题:{subscribe.name} 新增小于1分钟暂不搜索...")
continue continue
# 随机休眠1-5分钟 # 随机休眠1-5分钟
if not sid and state == 'R': if not sid and state in ['R', 'P']:
sleep_time = random.randint(60, 300) sleep_time = random.randint(60, 300)
logger.info(f'订阅搜索随机休眠 {sleep_time} 秒 ...') logger.info(f'订阅搜索随机休眠 {sleep_time} 秒 ...')
time.sleep(sleep_time) time.sleep(sleep_time)
try:
logger.info(f'开始搜索订阅,标题:{subscribe.name} ...') logger.info(f'开始搜索订阅,标题:{subscribe.name} ...')
# 如果状态为N则更新为R
if subscribe.state == 'N':
self.subscribeoper.update(subscribe.id, {'state': 'R'})
# 生成元数据 # 生成元数据
meta = MetaInfo(subscribe.name) meta = MetaInfo(subscribe.name)
meta.year = subscribe.year meta.year = subscribe.year
@@ -283,65 +288,23 @@ class SubscribeChain(ChainBase):
f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}doubanid{subscribe.doubanid}') f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}doubanid{subscribe.doubanid}')
continue continue
# 非洗版状态 # 如果媒体已存在或已下载完毕,跳过当前订阅处理
if not subscribe.best_version: exist_flag, no_exists = self.check_and_handle_existing_media(subscribe=subscribe, meta=meta,
# 每季总集数
totals = {}
if subscribe.season and subscribe.total_episode:
totals = {
subscribe.season: subscribe.total_episode
}
# 查询媒体库缺失的媒体信息
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
meta=meta,
mediainfo=mediainfo, mediainfo=mediainfo,
totals=totals mediakey=mediakey)
)
else:
# 洗版状态
exist_flag = False
if meta.type == MediaType.TV:
no_exists = {
mediakey: {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
}
}
else:
no_exists = {}
# 已存在
if exist_flag: if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
continue continue
# 电视剧订阅处理缺失集
if meta.type == MediaType.TV:
# 实际缺失集与订阅开始结束集范围进行整合,同时剔除已下载的集数
no_exists = self.__get_subscribe_no_exits(
subscribe_name=f'{subscribe.name} {meta.season}',
no_exists=no_exists,
mediakey=mediakey,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
downloaded_episodes=self.__get_downloaded_episodes(subscribe)
)
# 站点范围 # 站点范围
sites = self.get_sub_sites(subscribe) sites = self.get_sub_sites(subscribe)
# 优先级过滤规则 # 优先级过滤规则
if subscribe.best_version: if subscribe.best_version:
rule_groups = subscribe.filter_groups \ rule_groups = subscribe.filter_groups \
or self.systemconfig.get(SystemConfigKey.BestVersionFilterRuleGroups) or self.systemconfig.get(SystemConfigKey.BestVersionFilterRuleGroups) or []
else: else:
rule_groups = subscribe.filter_groups \ rule_groups = subscribe.filter_groups \
or self.systemconfig.get(SystemConfigKey.SubscribeFilterRuleGroups) or self.systemconfig.get(SystemConfigKey.SubscribeFilterRuleGroups) or []
# 搜索,同时电视剧会过滤掉不需要的剧集 # 搜索,同时电视剧会过滤掉不需要的剧集
contexts = self.searchchain.process(mediainfo=mediainfo, contexts = self.searchchain.process(mediainfo=mediainfo,
@@ -375,7 +338,8 @@ class SubscribeChain(ChainBase):
# 洗版时,优先级小于等于已下载优先级的不要 # 洗版时,优先级小于等于已下载优先级的不要
if subscribe.current_priority \ if subscribe.current_priority \
and torrent_info.pri_order <= subscribe.current_priority: and torrent_info.pri_order <= subscribe.current_priority:
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级') logger.info(
f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级')
continue continue
matched_contexts.append(context) matched_contexts.append(context)
@@ -394,18 +358,31 @@ class SubscribeChain(ChainBase):
save_path=subscribe.save_path, save_path=subscribe.save_path,
media_category=subscribe.media_category, media_category=subscribe.media_category,
downloader=subscribe.downloader, downloader=subscribe.downloader,
source=self.get_subscribe_source_keyword(subscribe)
) )
# 同步外部修改,更新订阅信息
subscribe = self.subscribeoper.get(subscribe.id)
# 判断是否应完成订阅 # 判断是否应完成订阅
if subscribe:
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo,
downloads=downloads, lefts=lefts) downloads=downloads, lefts=lefts)
finally:
# 如果状态为N则更新为R
if subscribe and subscribe.state == 'N':
self.subscribeoper.update(subscribe.id, {'state': 'R'})
# 手动触发时发送系统消息 # 手动触发时发送系统消息
if manual: if manual:
if subscribes:
if sid: if sid:
self.message.put(f'{subscribes[0].name} 搜索完成!', title="订阅搜索", role="system") self.message.put(f'{subscribes[0].name} 搜索完成!', title="订阅搜索", role="system")
else: else:
self.message.put('所有订阅搜索完成!', title="订阅搜索", role="system") self.message.put('所有订阅搜索完成!', title="订阅搜索", role="system")
else:
self.message.put('没有找到订阅!', title="订阅搜索", role="system")
logger.debug(f"search Lock released at {datetime.now()}")
def update_subscribe_priority(self, subscribe: Subscribe, meta: MetaInfo, def update_subscribe_priority(self, subscribe: Subscribe, meta: MetaInfo,
mediainfo: MediaInfo, downloads: List[Context]): mediainfo: MediaInfo, downloads: List[Context]):
@@ -440,25 +417,20 @@ class SubscribeChain(ChainBase):
no_lefts = not lefts or not lefts.get(mediakey) no_lefts = not lefts or not lefts.get(mediakey)
# 是否完成订阅 # 是否完成订阅
if not subscribe.best_version: if not subscribe.best_version:
# 非洗板 # 订阅存在待定策略,不管是否已完成,均需更新订阅信息
# 更新订阅已下载信息
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
# 更新订阅剩余集数和时间
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe, mediainfo=mediainfo,
update_date=bool(downloads))
# 判断是否需要完成订阅
if ((no_lefts and meta.type == MediaType.TV) if ((no_lefts and meta.type == MediaType.TV)
or (downloads and meta.type == MediaType.MOVIE) or (downloads and meta.type == MediaType.MOVIE)
or force): or force):
# 完成订阅
self.__finish_subscribe(subscribe=subscribe, meta=meta, mediainfo=mediainfo) self.__finish_subscribe(subscribe=subscribe, meta=meta, mediainfo=mediainfo)
elif downloads and meta.type == MediaType.TV:
# 电视剧更新已下载集数
self.__update_subscribe_note(subscribe=subscribe, downloads=downloads)
# 更新订阅剩余集数和时间
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
mediainfo=mediainfo, update_date=True)
else: else:
# 未下载到内容且不完整 # 未下载到内容且不完整
logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...') logger.info(f'{mediainfo.title_year} 未下载完整,继续订阅 ...')
if meta.type == MediaType.TV:
# 更新订阅剩余集数
self.__update_lack_episodes(lefts=lefts, subscribe=subscribe,
mediainfo=mediainfo, update_date=False)
elif downloads: elif downloads:
# 洗板,下载到了内容,更新资源优先级 # 洗板,下载到了内容,更新资源优先级
self.update_subscribe_priority(subscribe=subscribe, meta=meta, self.update_subscribe_priority(subscribe=subscribe, meta=meta,
@@ -508,7 +480,7 @@ class SubscribeChain(ChainBase):
:return: 返回[]代表所有站点命中返回None代表没有订阅 :return: 返回[]代表所有站点命中返回None代表没有订阅
""" """
# 查询所有订阅 # 查询所有订阅
subscribes = self.subscribeoper.list('R') subscribes = self.subscribeoper.list(self.get_states_for_search('R'))
if not subscribes: if not subscribes:
return None return None
ret_sites = [] ret_sites = []
@@ -533,8 +505,10 @@ class SubscribeChain(ChainBase):
# 记录重新识别过的种子 # 记录重新识别过的种子
_recognize_cached = [] _recognize_cached = []
with self._rlock:
logger.debug(f"match lock acquired at {datetime.now()}")
# 所有订阅 # 所有订阅
subscribes = self.subscribeoper.list('R') subscribes = self.subscribeoper.list(self.get_states_for_search('R'))
# 遍历订阅 # 遍历订阅
for subscribe in subscribes: for subscribe in subscribes:
if global_vars.is_system_stopped: if global_vars.is_system_stopped:
@@ -563,55 +537,14 @@ class SubscribeChain(ChainBase):
logger.warn( logger.warn(
f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}doubanid{subscribe.doubanid}') f'未识别到媒体信息,标题:{subscribe.name}tmdbid{subscribe.tmdbid}doubanid{subscribe.doubanid}')
continue continue
# 非洗版
if not subscribe.best_version: # 如果媒体已存在或已下载完毕,跳过当前订阅处理
# 每季总集数 exist_flag, no_exists = self.check_and_handle_existing_media(subscribe=subscribe, meta=meta,
totals = {}
if subscribe.season and subscribe.total_episode:
totals = {
subscribe.season: subscribe.total_episode
}
# 查询缺失的媒体信息
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
meta=meta,
mediainfo=mediainfo, mediainfo=mediainfo,
totals=totals mediakey=mediakey)
)
else:
# 洗版
exist_flag = False
if meta.type == MediaType.TV:
no_exists = {
mediakey: {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
}
}
else:
no_exists = {}
# 已存在
if exist_flag: if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
continue continue
# 电视剧订阅
if meta.type == MediaType.TV:
# 整合实际缺失集与订阅开始集结束集,同时剔除已下载的集数
no_exists = self.__get_subscribe_no_exits(
subscribe_name=f'{subscribe.name} {meta.season}',
no_exists=no_exists,
mediakey=mediakey,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
downloaded_episodes=self.__get_downloaded_episodes(subscribe)
)
# 遍历缓存种子 # 遍历缓存种子
_match_context = [] _match_context = []
for domain, contexts in torrents.items(): for domain, contexts in torrents.items():
@@ -751,7 +684,8 @@ class SubscribeChain(ChainBase):
if subscribe.best_version: if subscribe.best_version:
if subscribe.current_priority \ if subscribe.current_priority \
and torrent_info.pri_order <= subscribe.current_priority: and torrent_info.pri_order <= subscribe.current_priority:
logger.info(f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级') logger.info(
f'{subscribe.name} 正在洗版,{torrent_info.title} 优先级低于或等于已下载优先级')
continue continue
# 匹配成功 # 匹配成功
@@ -773,10 +707,18 @@ class SubscribeChain(ChainBase):
username=subscribe.username, username=subscribe.username,
save_path=subscribe.save_path, save_path=subscribe.save_path,
media_category=subscribe.media_category, media_category=subscribe.media_category,
downloader=subscribe.downloader) downloader=subscribe.downloader,
source=self.get_subscribe_source_keyword(subscribe)
)
# 同步外部修改,更新订阅信息
subscribe = self.subscribeoper.get(subscribe.id)
# 判断是否要完成订阅 # 判断是否要完成订阅
if subscribe:
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo,
downloads=downloads, lefts=lefts) downloads=downloads, lefts=lefts)
logger.debug(f"match Lock released at {datetime.now()}")
def check(self): def check(self):
""" """
@@ -837,7 +779,7 @@ class SubscribeChain(ChainBase):
def __update_subscribe_note(self, subscribe: Subscribe, downloads: List[Context]): def __update_subscribe_note(self, subscribe: Subscribe, downloads: List[Context]):
""" """
更新已下载集数到note字段 更新已下载信息到note字段
""" """
# 查询现有Note # 查询现有Note
if not downloads: if not downloads:
@@ -848,48 +790,66 @@ class SubscribeChain(ChainBase):
for context in downloads: for context in downloads:
meta = context.meta_info meta = context.meta_info
mediainfo = context.media_info mediainfo = context.media_info
if mediainfo.type != MediaType.TV:
continue
if subscribe.tmdbid and mediainfo.tmdb_id \ if subscribe.tmdbid and mediainfo.tmdb_id \
and mediainfo.tmdb_id != subscribe.tmdbid: and mediainfo.tmdb_id != subscribe.tmdbid:
continue continue
if subscribe.doubanid and mediainfo.douban_id \ if subscribe.doubanid and mediainfo.douban_id \
and mediainfo.douban_id != subscribe.doubanid: and mediainfo.douban_id != subscribe.doubanid:
continue continue
episodes = meta.episode_list items = []
if not episodes: if mediainfo.type == MediaType.TV:
# 电视剧有集数,使用 episode_list
items = meta.episode_list
elif mediainfo.type == MediaType.MOVIE:
# 电影只有一个条目,设置为 [1]
items = [1]
if not items:
continue continue
# 合并已下载 # 合并已下载的集数或电影项(去重)
note = list(set(note).union(set(episodes))) note = list(set(note).union(set(items)))
# 更新订阅 # 更新订阅
if note:
self.subscribeoper.update(subscribe.id, { self.subscribeoper.update(subscribe.id, {
"note": note "note": note
}) })
@staticmethod @staticmethod
def __get_downloaded_episodes(subscribe: Subscribe) -> List[int]: def __get_downloaded(subscribe: Subscribe) -> List[int]:
""" """
获取已下载过的集数 获取已下载过的集数或电影
""" """
if not subscribe.note: note = subscribe.note or []
if not note:
return [] return []
if subscribe.type != MediaType.TV.value: # 针对 TV 类型,返回已下载的集数
if subscribe.type == MediaType.TV.value:
logger.info(f'订阅 {subscribe.name}{subscribe.season}季 已下载集数:{note}')
return note
# 针对 Movie 类型,直接返回已下载的电影
if subscribe.type == MediaType.MOVIE.value:
logger.info(f'订阅 {subscribe.name} 已下载内容:{note}')
return note
return [] return []
episodes = subscribe.note or []
logger.info(f'订阅 {subscribe.name}{subscribe.season}季 已下载集数:{episodes}')
return episodes
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, NotExistMediaInfo]],
subscribe: Subscribe, subscribe: Subscribe,
mediainfo: MediaInfo, mediainfo: MediaInfo,
update_date: bool = False): update_date: bool = False):
""" """
更新订阅剩余集数 更新订阅剩余集数及时间
""" """
update_data = {}
if update_date:
update_data["last_update"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if subscribe.type == MediaType.TV.value:
if not lefts: if not lefts:
return # 如果 lefts 为空,表示没有缺失集数,直接设置 lack_episode 为 0
lack_episode = 0
logger.info(f'{mediainfo.title_year} 没有缺失集数,直接更新为 0 ...')
else:
mediakey = subscribe.tmdbid or subscribe.doubanid mediakey = subscribe.tmdbid or subscribe.doubanid
left_seasons = lefts.get(mediakey) left_seasons = lefts.get(mediakey)
lack_episode = 0
if left_seasons: if left_seasons:
for season_info in left_seasons.values(): for season_info in left_seasons.values():
season = season_info.season season = season_info.season
@@ -899,23 +859,21 @@ class SubscribeChain(ChainBase):
lack_episode = season_info.total_episode lack_episode = season_info.total_episode
else: else:
lack_episode = len(left_episodes) lack_episode = len(left_episodes)
logger.info(f'{mediainfo.title_year}{season} 更新缺失集数为{lack_episode} ...') logger.info(f"{mediainfo.title_year}{season} 更新缺失集数为{lack_episode} ...")
if update_date: break
# 同时更新最后时间 update_data = {"lack_episode": lack_episode}
self.subscribeoper.update(subscribe.id, { # 更新数据库
"lack_episode": lack_episode, if update_data:
"last_update": datetime.now().strftime('%Y-%m-%d %H:%M:%S') self.subscribeoper.update(subscribe.id, update_data)
})
else:
self.subscribeoper.update(subscribe.id, {
"lack_episode": lack_episode
})
def __finish_subscribe(self, subscribe: Subscribe, mediainfo: MediaInfo, def __finish_subscribe(self, subscribe: Subscribe, mediainfo: MediaInfo,
meta: MetaBase, bestversion: bool = False): meta: MetaBase, bestversion: bool = False):
""" """
完成订阅 完成订阅
""" """
# 如果订阅状态为待定P说明订阅信息尚未完全更新无法完成订阅
if subscribe.state == "P":
return
# 完成订阅 # 完成订阅
msgstr = "订阅" msgstr = "订阅"
if bestversion: if bestversion:
@@ -1015,7 +973,7 @@ class SubscribeChain(ChainBase):
total_episode: int, total_episode: int,
start_episode: int, start_episode: int,
downloaded_episodes: List[int] = None downloaded_episodes: List[int] = None
) -> Dict[Union[int, str], Dict[int, NotExistMediaInfo]]: ) -> Tuple[bool, Dict[Union[int, str], Dict[int, NotExistMediaInfo]]]:
""" """
根据订阅开始集数和总集数结合TMDB信息计算当前订阅的缺失集数 根据订阅开始集数和总集数结合TMDB信息计算当前订阅的缺失集数
:param subscribe_name: 订阅名称 :param subscribe_name: 订阅名称
@@ -1028,7 +986,7 @@ class SubscribeChain(ChainBase):
""" """
# 使用订阅的总集数和开始集数替换no_exists # 使用订阅的总集数和开始集数替换no_exists
if not no_exists or not no_exists.get(mediakey): if not no_exists or not no_exists.get(mediakey):
return no_exists return False, no_exists
no_exists_item = no_exists.get(mediakey) no_exists_item = no_exists.get(mediakey)
if total_episode or start_episode: if total_episode or start_episode:
logger.info(f'订阅 {subscribe_name} 设定的开始集数:{start_episode}、总集数:{total_episode}') logger.info(f'订阅 {subscribe_name} 设定的开始集数:{start_episode}、总集数:{total_episode}')
@@ -1053,7 +1011,7 @@ class SubscribeChain(ChainBase):
if not start_episode \ if not start_episode \
and not total_episode: and not total_episode:
# 无需调整 # 无需调整
return no_exists return False, no_exists
if not start_episode: if not start_episode:
# 没有自定义开始集 # 没有自定义开始集
start_episode = start start_episode = start
@@ -1088,25 +1046,32 @@ class SubscribeChain(ChainBase):
episode_list = list(range(start, total + 1)) episode_list = list(range(start, total + 1))
# 更新剧集列表 # 更新剧集列表
episodes = list(set(episode_list).difference(set(downloaded_episodes))) episodes = list(set(episode_list).difference(set(downloaded_episodes)))
# 如果存在已下载剧集,则差集为空时,说明所有均已存在
if not episodes:
return True, {}
# 更新集合 # 更新集合
no_exists[mediakey][begin_season] = NotExistMediaInfo( no_exists[mediakey][begin_season] = NotExistMediaInfo(
season=begin_season, season=begin_season,
episodes=episodes, episodes=episodes,
total_episode=total, total_episode=total,
start_episode=start start_episode=start,
) )
else: else:
# 开始集数 # 开始集数
start = start_episode or 1 start = start_episode or 1
# 不存在的季 # 更新剧集列表
episodes = list(set(range(start, total_episode + 1)).difference(set(downloaded_episodes)))
# 如果存在已下载剧集,则差集为空时,说明所有均已存在
if not episodes:
return True, {}
no_exists[mediakey][begin_season] = NotExistMediaInfo( no_exists[mediakey][begin_season] = NotExistMediaInfo(
season=begin_season, season=begin_season,
episodes=list(set(range(start, total_episode + 1)).difference(set(downloaded_episodes))), episodes=episodes,
total_episode=total_episode, total_episode=total_episode,
start_episode=start start_episode=start,
) )
logger.info(f'订阅 {subscribe_name} 缺失剧集数更新为:{no_exists}') logger.info(f'订阅 {subscribe_name} 缺失剧集数更新为:{no_exists}')
return no_exists return False, no_exists
@eventmanager.register(EventType.SiteDeleted) @eventmanager.register(EventType.SiteDeleted)
def remove_site(self, event: Event): def remove_site(self, event: Event):
@@ -1126,7 +1091,7 @@ class SubscribeChain(ChainBase):
if not subscribe.sites: if not subscribe.sites:
continue continue
self.subscribeoper.update(subscribe.id, { self.subscribeoper.update(subscribe.id, {
"sites": "" "sites": []
}) })
return return
# 从选中的rss站点中移除 # 从选中的rss站点中移除
@@ -1294,3 +1259,119 @@ class SubscribeChain(ChainBase):
subscribe_info.subscribe = Subscribe(**subscribe.to_dict()) subscribe_info.subscribe = Subscribe(**subscribe.to_dict())
subscribe_info.episodes = episodes subscribe_info.episodes = episodes
return subscribe_info return subscribe_info
def check_and_handle_existing_media(self, subscribe: Subscribe, meta: MetaInfo,
mediainfo: MediaInfo, mediakey: str):
"""
检查媒体是否已经存在,并根据情况执行相应的操作
1. 查询缺失的媒体信息
2. 判断是否已经下载完毕
3. 根据媒体类型(电视剧或电影)执行不同的处理
:param subscribe: 订阅信息对象
:param meta: 媒体元数据
:param mediainfo: 媒体信息
:param mediakey: 媒体标识符
:return:
- exist_flag (bool): 布尔值,表示媒体是否已经完全下载或已存在
- no_exists (dict): 缺失的媒体信息,包含缺失的集数或其他相关信息
"""
# 非洗版
if not subscribe.best_version:
# 每季总集数
totals = {}
if subscribe.season and subscribe.total_episode:
totals = {
subscribe.season: subscribe.total_episode
}
# 查询媒体库缺失的媒体信息
exist_flag, no_exists = self.downloadchain.get_no_exists_info(
meta=meta,
mediainfo=mediainfo,
totals=totals
)
else:
# 洗版
exist_flag = False
if meta.type == MediaType.TV:
# 对于电视剧,构造缺失的媒体信息
no_exists = {
mediakey: {
subscribe.season: NotExistMediaInfo(
season=subscribe.season,
episodes=[],
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode or 1)
}
}
else:
no_exists = {}
# 如果媒体已存在,执行订阅完成操作
if exist_flag:
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
return True, no_exists
# 获取已下载的集数或电影
downloaded = self.__get_downloaded(subscribe)
if meta.type == MediaType.TV:
# 对于电视剧类型,整合缺失集数并剔除已下载的集数
exist_flag, no_exists = self.__get_subscribe_no_exits(
subscribe_name=f'{subscribe.name} {meta.season}',
no_exists=no_exists,
mediakey=mediakey,
begin_season=meta.begin_season,
total_episode=subscribe.total_episode,
start_episode=subscribe.start_episode,
downloaded_episodes=downloaded
)
elif meta.type == MediaType.MOVIE:
# 对于电影类型,直接根据是否已下载判断
exist_flag = bool(downloaded)
# 如果已下载完毕,执行订阅完成操作
if exist_flag:
logger.info(f'{mediainfo.title_year} 已全部下载')
self.finish_subscribe_or_not(subscribe=subscribe, meta=meta, mediainfo=mediainfo, force=True)
return True, no_exists
# 返回结果,表示媒体未完全下载或存在
return False, no_exists
@staticmethod
def get_states_for_search(state: str) -> str:
"""
根据给定的状态返回实际需要搜索的状态列表,支持多个状态用逗号分隔
:param state: 订阅状态
N: New新建未处理
R: Resolved订阅中
P: Pending待定信息待进一步更新允许搜索不允许完成
S: Suspended暂停订阅不参与任何动作暂时停止处理
:return: 需要查询的状态列表(多个状态用逗号分隔)
"""
# 如果状态是 R 或 P则视为一起搜索返回 R,P 作为查询条件
if state in ["R", "P"]:
return "R,P"
return state
@staticmethod
def get_subscribe_source_keyword(subscribe: Subscribe) -> str:
"""
构造用于订阅来源的关键字字符串
:param subscribe: Subscribe 对象
:return: 格式化的订阅来源关键字字符串,格式为 "Subscribe|{...}"
"""
source_keyword = {
'id': subscribe.id,
'name': subscribe.name,
'year': subscribe.year,
'type': subscribe.type,
'season': subscribe.season,
'tmdbid': subscribe.tmdbid,
'imdbid': subscribe.imdbid,
'tvdbid': subscribe.tvdbid,
'doubanid': subscribe.doubanid,
'bangumiid': subscribe.bangumiid
}
return f"Subscribe|{json.dumps(source_keyword, ensure_ascii=False)}"

View File

@@ -330,7 +330,7 @@ class TransferChain(ChainBase):
# 自定义识别 # 自定义识别
if formaterHandler: if formaterHandler:
# 开始集、结束集、PART # 开始集、结束集、PART
begin_ep, end_ep, part = formaterHandler.split_episode(file_path.name) begin_ep, end_ep, part = formaterHandler.split_episode(file_name=file_path.name, file_meta=file_meta)
if begin_ep is not None: if begin_ep is not None:
file_meta.begin_episode = begin_ep file_meta.begin_episode = begin_ep
file_meta.part = part file_meta.part = part
@@ -392,6 +392,7 @@ class TransferChain(ChainBase):
download_hash = download_file.download_hash download_hash = download_file.download_hash
# 查询整理目标目录 # 查询整理目标目录
dir_info = None
if not target_directory: if not target_directory:
if src_match: if src_match:
# 按源目录匹配,以便找到更合适的目录配置 # 按源目录匹配,以便找到更合适的目录配置

View File

@@ -1,5 +1,6 @@
import re import re
from dataclasses import dataclass, field, asdict from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import List, Dict, Any, Tuple from typing import List, Dict, Any, Tuple
from app.core.config import settings from app.core.config import settings
@@ -123,6 +124,20 @@ class TorrentInfo:
return "" return ""
return StringUtils.diff_time_str(self.freedate) return StringUtils.diff_time_str(self.freedate)
def pub_minutes(self) -> float:
"""
返回发布时间距离当前时间的分钟数
"""
if not self.pubdate:
return 0
try:
pub_date = datetime.strptime(self.pubdate, "%Y-%m-%d %H:%M:%S")
now_datetime = datetime.now()
return (now_datetime - pub_date).total_seconds() // 60
except Exception as e:
print(f"种子发布时间获取失败: {e}")
return 0
def to_dict(self): def to_dict(self):
""" """
返回字典 返回字典

View File

@@ -347,8 +347,17 @@ class EventManager(metaclass=Singleton):
if not handlers: if not handlers:
logger.debug(f"No handlers found for chain event: {event}") logger.debug(f"No handlers found for chain event: {event}")
return False return False
# 过滤出启用的处理器
enabled_handlers = {handler_id: (priority, handler) for handler_id, (priority, handler) in handlers.items()
if self.__is_handler_enabled(handler)}
if not enabled_handlers:
logger.debug(f"No enabled handlers found for chain event: {event}. Skipping execution.")
return False
self.__log_event_lifecycle(event, "Started") self.__log_event_lifecycle(event, "Started")
for handler_id, (priority, handler) in handlers.items(): for handler_id, (priority, handler) in enabled_handlers.items():
start_time = time.time() start_time = time.time()
self.__safe_invoke_handler(handler, event) self.__safe_invoke_handler(handler, event)
logger.debug( logger.debug(
@@ -493,13 +502,15 @@ class EventManager(metaclass=Singleton):
} }
) )
def register(self, etype: Union[EventType, ChainEventType, List[Union[EventType, ChainEventType]], type]): def register(self, etype: Union[EventType, ChainEventType, List[Union[EventType, ChainEventType]], type],
priority: int = DEFAULT_EVENT_PRIORITY):
""" """
事件注册装饰器,用于将函数注册为事件的处理器 事件注册装饰器,用于将函数注册为事件的处理器
:param etype: :param etype:
- 单个事件类型成员 (如 EventType.MetadataScrape, ChainEventType.PluginAction) - 单个事件类型成员 (如 EventType.MetadataScrape, ChainEventType.PluginAction)
- 事件类型类 (EventType, ChainEventType) - 事件类型类 (EventType, ChainEventType)
- 或事件类型成员的列表 - 或事件类型成员的列表
:param priority: 可选,链式事件的优先级,默认为 DEFAULT_EVENT_PRIORITY
""" """
def decorator(f: Callable): def decorator(f: Callable):
@@ -507,11 +518,6 @@ class EventManager(metaclass=Singleton):
if isinstance(etype, list): if isinstance(etype, list):
# 传入的已经是列表,直接使用 # 传入的已经是列表,直接使用
event_list = etype event_list = etype
elif etype is EventType:
# 订阅所有事件
event_list = []
for et in etype:
event_list.append(et)
else: else:
# 不是列表则包裹成单一元素的列表 # 不是列表则包裹成单一元素的列表
event_list = [etype] event_list = [etype]
@@ -519,11 +525,11 @@ class EventManager(metaclass=Singleton):
# 遍历列表,处理每个事件类型 # 遍历列表,处理每个事件类型
for event in event_list: for event in event_list:
if isinstance(event, (EventType, ChainEventType)): if isinstance(event, (EventType, ChainEventType)):
self.add_event_listener(event, f) self.add_event_listener(event, f, priority)
elif isinstance(event, type) and issubclass(event, (EventType, ChainEventType)): elif isinstance(event, type) and issubclass(event, (EventType, ChainEventType)):
# 如果是 EventType 或 ChainEventType 类,提取该类中的所有成员 # 如果是 EventType 或 ChainEventType 类,提取该类中的所有成员
for et in event.__members__.values(): for et in event.__members__.values():
self.add_event_listener(et, f) self.add_event_listener(et, f, priority)
else: else:
raise ValueError(f"无效的事件类型: {event}") raise ValueError(f"无效的事件类型: {event}")

View File

@@ -1,11 +1,12 @@
import traceback import traceback
from typing import Generator, Optional, Tuple, Any from typing import Generator, Optional, Tuple, Any, Union
from app.core.config import settings from app.core.config import settings
from app.core.event import eventmanager from app.core.event import eventmanager
from app.helper.module import ModuleHelper from app.helper.module import ModuleHelper
from app.log import logger from app.log import logger
from app.schemas.types import EventType, ModuleType from app.schemas.types import EventType, ModuleType, DownloaderType, MediaServerType, MessageChannel, StorageSchema, \
OtherModulesType
from app.utils.object import ObjectUtils from app.utils.object import ObjectUtils
from app.utils.singleton import Singleton from app.utils.singleton import Singleton
@@ -19,6 +20,8 @@ class ModuleManager(metaclass=Singleton):
_modules: dict = {} _modules: dict = {}
# 运行态模块列表 # 运行态模块列表
_running_modules: dict = {} _running_modules: dict = {}
# 子模块类型集合
SubType = Union[DownloaderType, MediaServerType, MessageChannel, StorageSchema, OtherModulesType]
def __init__(self): def __init__(self):
self.load_modules() self.load_modules()
@@ -135,6 +138,17 @@ class ModuleManager(metaclass=Singleton):
and module.get_type() == module_type: and module.get_type() == module_type:
yield module yield module
def get_running_subtype_module(self, module_subtype: SubType) -> Generator:
"""
获取指定子类型的模块
"""
if not self._running_modules:
return []
for _, module in self._running_modules.items():
if hasattr(module, 'get_subtype') \
and module.get_subtype() == module_subtype:
yield module
def get_module(self, module_id: str) -> Any: def get_module(self, module_id: str) -> Any:
""" """
根据模块id获取模块 根据模块id获取模块

View File

@@ -53,7 +53,9 @@ class DownloadHistory(Base):
@staticmethod @staticmethod
@db_query @db_query
def get_by_hash(db: Session, download_hash: str): def get_by_hash(db: Session, download_hash: str):
return db.query(DownloadHistory).filter(DownloadHistory.download_hash == download_hash).first() return db.query(DownloadHistory).filter(DownloadHistory.download_hash == download_hash).order_by(
DownloadHistory.date.desc()
).first()
@staticmethod @staticmethod
@db_query @db_query

View File

@@ -81,7 +81,7 @@ class SiteUserData(Base):
func.max(SiteUserData.updated_day).label('latest_update_day') func.max(SiteUserData.updated_day).label('latest_update_day')
) )
.group_by(SiteUserData.domain) .group_by(SiteUserData.domain)
.filter(SiteUserData.err_msg == None) .filter(SiteUserData.err_msg.is_(None))
.subquery() .subquery()
) )

View File

@@ -54,7 +54,7 @@ class Subscribe(Base):
lack_episode = Column(Integer) lack_episode = Column(Integer)
# 附加信息 # 附加信息
note = Column(JSON) note = Column(JSON)
# 状态N-新建 R-订阅中 # 状态N-新建 R-订阅中 P-待定 S-暂停
state = Column(String, nullable=False, index=True, default='N') state = Column(String, nullable=False, index=True, default='N')
# 最后更新时间 # 最后更新时间
last_update = Column(String) last_update = Column(String)
@@ -98,7 +98,13 @@ class Subscribe(Base):
@staticmethod @staticmethod
@db_query @db_query
def get_by_state(db: Session, state: str): def get_by_state(db: Session, state: str):
result = db.query(Subscribe).filter(Subscribe.state == state).all() # 如果 state 为空或 None返回所有订阅
if not state:
result = db.query(Subscribe).all()
else:
# 如果传入的状态不为空,拆分成多个状态
states = state.split(',')
result = db.query(Subscribe).filter(Subscribe.state.in_(states)).all()
return list(result) return list(result)
@staticmethod @staticmethod

View File

@@ -83,6 +83,7 @@ class SubscribeOper(DbOper):
更新订阅 更新订阅
""" """
subscribe = self.get(sid) subscribe = self.get(sid)
if subscribe:
subscribe.update(self._db, payload) subscribe.update(self._db, payload)
return subscribe return subscribe

View File

@@ -16,7 +16,7 @@ class PlaywrightHelper:
""" """
sync_stealth(page, pure=True) sync_stealth(page, pure=True)
page.goto(url) page.goto(url)
return sync_cf_retry(page) return sync_cf_retry(page)[0]
def action(self, url: str, def action(self, url: str,
callback: Callable, callback: Callable,

View File

@@ -59,7 +59,6 @@ class DirectoryHelper:
:param include_unsorted: 包含不整理目录 :param include_unsorted: 包含不整理目录
:param storage: 源存储类型 :param storage: 源存储类型
:param target_storage: 目标存储类型 :param target_storage: 目标存储类型
:param fileitem: 文件项,使用文件路径匹配
:param src_path: 源目录,有值时直接匹配 :param src_path: 源目录,有值时直接匹配
:param dest_path: 目标目录,有值时直接匹配 :param dest_path: 目标目录,有值时直接匹配
""" """

View File

@@ -3,6 +3,8 @@ from typing import Tuple, Optional
import parse import parse
from app.core.meta.metabase import MetaBase
class FormatParser(object): class FormatParser(object):
_key = "" _key = ""
@@ -77,7 +79,7 @@ class FormatParser(object):
return True return True
return False return False
def split_episode(self, file_name: str) -> Tuple[Optional[int], Optional[int], Optional[str]]: def split_episode(self, file_name: str, file_meta: MetaBase) -> Tuple[Optional[int], Optional[int], Optional[str]]:
""" """
拆分集数返回开始集数结束集数Part信息 拆分集数返回开始集数结束集数Part信息
""" """
@@ -94,7 +96,9 @@ class FormatParser(object):
start_ep = self.__offset.replace("EP", str(self._start_ep)) start_ep = self.__offset.replace("EP", str(self._start_ep))
return int(eval(start_ep)), None, self.part return int(eval(start_ep)), None, self.part
if not self._format: if not self._format:
return self._start_ep, self._end_ep, self.part start_ep = eval(self.__offset.replace("EP", str(file_meta.begin_episode))) if file_meta.begin_episode else None
end_ep = eval(self.__offset.replace("EP", str(file_meta.end_episode))) if file_meta.end_episode else None
return int(start_ep) if start_ep else None, int(end_ep) if end_ep else None, self.part
else: else:
s, e = self.__handle_single(file_name) s, e = self.__handle_single(file_name)
start_ep = self.__offset.replace("EP", str(s)) if s else None start_ep = self.__offset.replace("EP", str(s)) if s else None

View File

@@ -64,10 +64,10 @@ class TorrentHelper(metaclass=Singleton):
if not req.content: if not req.content:
return None, None, "", [], "未下载到种子数据" return None, None, "", [], "未下载到种子数据"
# 解析内容格式 # 解析内容格式
if req.text and str(req.text).startswith("magnet:"): if req.content.startswith(b"magnet:"):
# 磁力链接 # 磁力链接
return None, req.text, "", [], f"获取到磁力链接" return None, req.text, "", [], f"获取到磁力链接"
elif req.text and "下载种子文件" in req.text: if "下载种子文件".encode("utf-8") in req.content:
# 首次下载提示页面 # 首次下载提示页面
skip_flag = False skip_flag = False
try: try:

View File

@@ -2,8 +2,9 @@ from abc import abstractmethod, ABCMeta
from typing import Generic, Tuple, Union, TypeVar, Type, Dict, Optional, Callable from typing import Generic, Tuple, Union, TypeVar, Type, Dict, Optional, Callable
from app.helper.service import ServiceConfigHelper from app.helper.service import ServiceConfigHelper
from app.schemas import Notification, MessageChannel, NotificationConf, MediaServerConf, DownloaderConf from app.schemas import Notification, NotificationConf, MediaServerConf, DownloaderConf
from app.schemas.types import ModuleType from app.schemas.types import ModuleType, DownloaderType, MediaServerType, MessageChannel, StorageSchema, \
OtherModulesType
class _ModuleBase(metaclass=ABCMeta): class _ModuleBase(metaclass=ABCMeta):
@@ -43,6 +44,14 @@ class _ModuleBase(metaclass=ABCMeta):
""" """
pass pass
@staticmethod
@abstractmethod
def get_subtype() -> Union[DownloaderType, MediaServerType, MessageChannel, StorageSchema, OtherModulesType]:
"""
获取模块子类型(下载器、媒体服务器、消息通道、存储类型、其他杂项模块类型)
"""
pass
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def get_priority() -> int: def get_priority() -> int:

View File

@@ -7,7 +7,7 @@ from app.core.meta import MetaBase
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.bangumi.bangumi import BangumiApi from app.modules.bangumi.bangumi import BangumiApi
from app.schemas.types import ModuleType from app.schemas.types import ModuleType, MediaRecognizeType
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
@@ -45,6 +45,13 @@ class BangumiModule(_ModuleBase):
""" """
return ModuleType.MediaRecognize return ModuleType.MediaRecognize
@staticmethod
def get_subtype() -> MediaRecognizeType:
"""
获取模块子类型
"""
return MediaRecognizeType.Bangumi
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -15,7 +15,7 @@ from app.modules.douban.apiv2 import DoubanApi
from app.modules.douban.douban_cache import DoubanCache from app.modules.douban.douban_cache import DoubanCache
from app.modules.douban.scraper import DoubanScraper from app.modules.douban.scraper import DoubanScraper
from app.schemas import MediaPerson, APIRateLimitException from app.schemas import MediaPerson, APIRateLimitException
from app.schemas.types import MediaType, ModuleType from app.schemas.types import MediaType, ModuleType, MediaRecognizeType
from app.utils.common import retry from app.utils.common import retry
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from app.utils.limit import rate_limit_exponential from app.utils.limit import rate_limit_exponential
@@ -59,6 +59,13 @@ class DoubanModule(_ModuleBase):
""" """
return ModuleType.MediaRecognize return ModuleType.MediaRecognize
@staticmethod
def get_subtype() -> MediaRecognizeType:
"""
获取模块子类型
"""
return MediaRecognizeType.Douban
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -7,7 +7,7 @@ from app.log import logger
from app.modules import _MediaServerBase, _ModuleBase from app.modules import _MediaServerBase, _ModuleBase
from app.modules.emby.emby import Emby from app.modules.emby.emby import Emby
from app.schemas.event import AuthCredentials, AuthInterceptCredentials from app.schemas.event import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import MediaType, ModuleType, ChainEventType from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType
class EmbyModule(_ModuleBase, _MediaServerBase[Emby]): class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
@@ -30,6 +30,13 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
""" """
return ModuleType.MediaServer return ModuleType.MediaServer
@staticmethod
def get_subtype() -> MediaServerType:
"""
获取模块子类型
"""
return MediaServerType.Emby
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """
@@ -66,16 +73,26 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
logger.info(f"Emby服务器 {name} 连接断开,尝试重连 ...") logger.info(f"Emby服务器 {name} 连接断开,尝试重连 ...")
server.reconnect() server.reconnect()
def user_authenticate(self, credentials: AuthCredentials) -> Optional[AuthCredentials]: def user_authenticate(self, credentials: AuthCredentials, service_name: Optional[str] = None) \
-> Optional[AuthCredentials]:
""" """
使用Emby用户辅助完成用户认证 使用Emby用户辅助完成用户认证
:param credentials: 认证数据 :param credentials: 认证数据
:param service_name: 指定要认证的媒体服务器名称,若为 None 则认证所有服务
:return: 认证数据 :return: 认证数据
""" """
# Emby认证 # Emby认证
if not credentials or credentials.grant_type != "password": if not credentials or credentials.grant_type != "password":
return None return None
for name, server in self.get_instances().items(): # 确定要认证的服务器列表
if service_name:
# 如果指定了服务名,获取该服务实例
servers = [(service_name, server)] if (server := self.get_instance(service_name)) else []
else:
# 如果没有指定服务名,遍历所有服务
servers = self.get_instances().items()
# 遍历要认证的服务器
for name, server in servers:
# 触发认证拦截事件 # 触发认证拦截事件
intercept_event = eventmanager.send_event( intercept_event = eventmanager.send_event(
etype=ChainEventType.AuthIntercept, etype=ChainEventType.AuthIntercept,

View File

@@ -6,7 +6,7 @@ from cachetools import TTLCache, cached
from app.core.context import MediaInfo, settings from app.core.context import MediaInfo, settings
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.schemas.types import MediaType, ModuleType from app.schemas.types import MediaType, ModuleType, OtherModulesType
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
@@ -343,6 +343,13 @@ class FanartModule(_ModuleBase):
""" """
return ModuleType.Other return ModuleType.Other
@staticmethod
def get_subtype() -> OtherModulesType:
"""
获取模块子类型
"""
return OtherModulesType.Fanart
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """
@@ -377,19 +384,29 @@ class FanartModule(_ModuleBase):
continue continue
if not isinstance(images, list): if not isinstance(images, list):
continue continue
# 按欢迎程度倒排
# 图片属性xx_path
image_name = self.__name(name)
if image_name.startswith("season"):
# 季图片图片格式seasonxx-xxxx/season-specials-xxxx
for image_obj in images:
image_season = image_obj.get('season')
if image_season is not None:
# 包括poster,thumb,banner
if image_season == '0':
season_image = f"season-specials-{image_name[6:]}"
else:
season_image = f"season{str(image_season).rjust(2, '0')}-{image_name[6:]}"
# 设置图片,没有图片才设置
if not mediainfo.get_image(season_image):
mediainfo.set_image(season_image, image_obj.get('url'))
else:
# 其他图片,按欢迎程度倒排
images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True) images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
# 取第一张图片 # 取第一张图片
image_obj = images[0] image_obj = images[0]
# 图片属性xx_path # 设置图片,没有图片才设置
image_name = self.__name(name)
image_season = image_obj.get('season')
# 设置图片
if image_name.startswith("season") and image_season:
# 季图片格式 seasonxx-poster
image_name = f"season{str(image_season).rjust(2, '0')}-{image_name[6:]}"
if not mediainfo.get_image(image_name): if not mediainfo.get_image(image_name):
# 没有图片才设置
mediainfo.set_image(image_name, image_obj.get('url')) mediainfo.set_image(image_name, image_obj.get('url'))
return mediainfo return mediainfo

View File

@@ -18,7 +18,7 @@ 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 from app.schemas import TransferInfo, ExistMediaInfo, TmdbEpisode, TransferDirectoryConf, FileItem, StorageUsage
from app.schemas.event import TransferRenameEventData from app.schemas.event import TransferRenameEventData
from app.schemas.types import MediaType, ModuleType, ChainEventType from app.schemas.types import MediaType, ModuleType, ChainEventType, OtherModulesType
from app.utils.system import SystemUtils from app.utils.system import SystemUtils
lock = Lock() lock = Lock()
@@ -52,6 +52,13 @@ class FileManagerModule(_ModuleBase):
""" """
return ModuleType.Other return ModuleType.Other
@staticmethod
def get_subtype() -> OtherModulesType:
"""
获取模块子类型
"""
return OtherModulesType.FileManager
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """
@@ -695,7 +702,7 @@ class FileManagerModule(_ModuleBase):
return False, errmsg return False, errmsg
except Exception as error: except Exception as error:
logger.info(f"字幕 {new_file} 出错了,原因: {str(error)}") logger.info(f"字幕 {new_file} 出错了,原因: {str(error)}")
return False, "" return True, ""
def __transfer_audio_track_files(self, fileitem: FileItem, target_storage: str, target_file: Path, def __transfer_audio_track_files(self, fileitem: FileItem, target_storage: str, target_file: Path,
transfer_type: str) -> Tuple[bool, str]: transfer_type: str) -> Tuple[bool, str]:
@@ -719,7 +726,7 @@ class FileManagerModule(_ModuleBase):
file_list: List[FileItem] = storage_oper.list(parent_item) file_list: List[FileItem] = storage_oper.list(parent_item)
# 匹配音轨文件 # 匹配音轨文件
pending_file_list: List[FileItem] = [file for file in file_list pending_file_list: List[FileItem] = [file for file in file_list
if Path(file.name).stem == org_path.name if Path(file.name).stem == org_path.stem
and file.type == "file" and file.extension and file.type == "file" and file.extension
and f".{file.extension.lower()}" in settings.RMT_AUDIOEXT] and f".{file.extension.lower()}" in settings.RMT_AUDIOEXT]
if len(pending_file_list) == 0: if len(pending_file_list) == 0:

View File

@@ -553,15 +553,15 @@ class Alist(StorageBase, metaclass=Singleton):
:param new_name: 上传后文件名 :param new_name: 上传后文件名
:param task: 是否为任务默认为False避免未完成上传时对文件进行操作 :param task: 是否为任务默认为False避免未完成上传时对文件进行操作
""" """
encoded_path = UrlUtils.quote(fileitem.path) encoded_path = UrlUtils.quote(fileitem.path + path.name)
headers = self.__get_header_with_token() headers = self.__get_header_with_token()
headers.setdefault("Content-Type", "multipart/form-data") headers.setdefault("Content-Type", "application/octet-stream")
headers.setdefault("As-Task", str(task).lower()) headers.setdefault("As-Task", str(task).lower())
headers.setdefault("File-Path", encoded_path) headers.setdefault("File-Path", encoded_path)
with open(path, "rb") as f: with open(path, "rb") as f:
resp: Response = RequestUtils(headers=headers).put_res( resp: Response = RequestUtils(headers=headers).put_res(
self.__get_api_url("/api/fs/form"), self.__get_api_url("/api/fs/put"),
data={"file": f}, data=f,
) )
if resp.status_code != 200: if resp.status_code != 200:

View File

@@ -1,4 +1,3 @@
import copy
import json import json
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -57,21 +56,6 @@ class Rclone(StorageBase):
else: else:
return None return None
def __get_fileitem(self, path: Path):
"""
获取文件项
"""
return schemas.FileItem(
storage=self.schema.value,
type="file",
path=str(path).replace("\\", "/"),
name=path.name,
basename=path.stem,
extension=path.suffix[1:],
size=path.stat().st_size,
modify_time=path.stat().st_mtime,
)
def __get_rcloneitem(self, item: dict, parent: str = "/") -> schemas.FileItem: def __get_rcloneitem(self, item: dict, parent: str = "/") -> schemas.FileItem:
""" """
获取rclone文件项 获取rclone文件项
@@ -146,12 +130,12 @@ class Rclone(StorageBase):
retcode = subprocess.run( retcode = subprocess.run(
[ [
'rclone', 'mkdir', 'rclone', 'mkdir',
f'MP:{fileitem.path}/{name}' f'MP:{Path(fileitem.path) / name}'
], ],
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
).returncode ).returncode
if retcode == 0: if retcode == 0:
return self.get_item(Path(f"{fileitem.path}/{name}")) return self.get_item(Path(fileitem.path) / name)
except Exception as err: except Exception as err:
logger.error(f"rclone创建目录失败{err}") logger.error(f"rclone创建目录失败{err}")
return None return None
@@ -200,16 +184,19 @@ class Rclone(StorageBase):
ret = subprocess.run( ret = subprocess.run(
[ [
'rclone', 'lsjson', 'rclone', 'lsjson',
f'MP:{path}' f'MP:{path.parent}'
], ],
capture_output=True, capture_output=True,
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
) )
if ret.returncode == 0: if ret.returncode == 0:
items = json.loads(ret.stdout) items = json.loads(ret.stdout)
return self.__get_rcloneitem(items[0]) for item in items:
if item.get("Name") == path.name:
return self.__get_rcloneitem(item, parent=str(path.parent) + "/")
return None
except Exception as err: except Exception as err:
logger.error(f"rclone获取文件失败{err}") logger.debug(f"rclone获取文件失败:{err}")
return None return None
def delete(self, fileitem: schemas.FileItem) -> bool: def delete(self, fileitem: schemas.FileItem) -> bool:
@@ -239,7 +226,7 @@ class Rclone(StorageBase):
[ [
'rclone', 'moveto', 'rclone', 'moveto',
f'MP:{fileitem.path}', f'MP:{fileitem.path}',
f'MP:{Path(fileitem.path).parent}/{name}' f'MP:{Path(fileitem.path).parent / name}'
], ],
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
).returncode ).returncode
@@ -287,7 +274,7 @@ class Rclone(StorageBase):
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
).returncode ).returncode
if retcode == 0: if retcode == 0:
return self.__get_fileitem(new_path) return self.get_item(new_path)
except Exception as err: except Exception as err:
logger.error(f"rclone上传文件失败{err}") logger.error(f"rclone上传文件失败{err}")
return None return None

View File

@@ -7,7 +7,7 @@ from app.helper.rule import RuleHelper
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.filter.RuleParser import RuleParser from app.modules.filter.RuleParser import RuleParser
from app.schemas.types import ModuleType from app.schemas.types import ModuleType, OtherModulesType
from app.utils.string import StringUtils from app.utils.string import StringUtils
@@ -167,6 +167,13 @@ class FilterModule(_ModuleBase):
""" """
return ModuleType.Other return ModuleType.Other
@staticmethod
def get_subtype() -> OtherModulesType:
"""
获取模块子类型
"""
return OtherModulesType.Filter
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """
@@ -359,6 +366,8 @@ class FilterModule(_ModuleBase):
seeders = self.rule_set[rule_name].get("seeders") seeders = self.rule_set[rule_name].get("seeders")
# FREE规则 # FREE规则
downloadvolumefactor = self.rule_set[rule_name].get("downloadvolumefactor") downloadvolumefactor = self.rule_set[rule_name].get("downloadvolumefactor")
# 发布时间规则
pubdate: str = self.rule_set[rule_name].get("publish_time")
if includes and not any(re.search(r"%s" % include, content, re.IGNORECASE) for include in includes): if includes and not any(re.search(r"%s" % include, content, re.IGNORECASE) for include in includes):
# 未发现任何包含项 # 未发现任何包含项
logger.debug(f"种子 {torrent.site_name} - {torrent.title} 不包含任何项 {includes}") logger.debug(f"种子 {torrent.site_name} - {torrent.title} 不包含任何项 {includes}")
@@ -385,6 +394,22 @@ class FilterModule(_ModuleBase):
logger.debug( logger.debug(
f"种子 {torrent.site_name} - {torrent.title} FREE值 {torrent.downloadvolumefactor} 不是 {downloadvolumefactor}") f"种子 {torrent.site_name} - {torrent.title} FREE值 {torrent.downloadvolumefactor} 不是 {downloadvolumefactor}")
return False return False
if pubdate:
# 种子发布时间
pub_minutes = torrent.pub_minutes()
# 发布时间规则
pub_times = [float(t) for t in pubdate.split("-")]
if len(pub_times) == 1:
# 发布时间小于规则
if pub_minutes < pub_times[0]:
logger.debug(f"种子 {torrent.site_name} - {torrent.title} 发布时间 {pub_minutes} 小于 {pub_times[0]}")
return False
else:
# 区间
if not (pub_times[0] <= pub_minutes <= pub_times[1]):
logger.debug(f"种子 {torrent.site_name} - {torrent.title} 发布时间 {pub_minutes} 不在 {pub_times[0]}-{pub_times[1]} 时间区间")
return False
return True return True
def __match_tmdb(self, tmdb: dict) -> bool: def __match_tmdb(self, tmdb: dict) -> bool:

View File

@@ -18,7 +18,7 @@ from app.modules.indexer.spider.tnode import TNodeSpider
from app.modules.indexer.spider.torrentleech import TorrentLeech from app.modules.indexer.spider.torrentleech import TorrentLeech
from app.modules.indexer.spider.yema import YemaSpider from app.modules.indexer.spider.yema import YemaSpider
from app.schemas import SiteUserData from app.schemas import SiteUserData
from app.schemas.types import MediaType, ModuleType from app.schemas.types import MediaType, ModuleType, OtherModulesType
from app.utils.string import StringUtils from app.utils.string import StringUtils
@@ -47,6 +47,13 @@ class IndexerModule(_ModuleBase):
""" """
return ModuleType.Indexer return ModuleType.Indexer
@staticmethod
def get_subtype() -> OtherModulesType:
"""
获取模块子类型
"""
return OtherModulesType.Indexer
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -7,7 +7,7 @@ from app.log import logger
from app.modules import _MediaServerBase, _ModuleBase from app.modules import _MediaServerBase, _ModuleBase
from app.modules.jellyfin.jellyfin import Jellyfin from app.modules.jellyfin.jellyfin import Jellyfin
from app.schemas.event import AuthCredentials, AuthInterceptCredentials from app.schemas.event import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import MediaType, ModuleType, ChainEventType from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType
class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]): class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
@@ -30,6 +30,13 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
""" """
return ModuleType.MediaServer return ModuleType.MediaServer
@staticmethod
def get_subtype() -> MediaServerType:
"""
获取模块子类型
"""
return MediaServerType.Jellyfin
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """
@@ -66,16 +73,26 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
return False, f"无法连接Jellyfin服务器{name}" return False, f"无法连接Jellyfin服务器{name}"
return True, "" return True, ""
def user_authenticate(self, credentials: AuthCredentials) -> Optional[AuthCredentials]: def user_authenticate(self, credentials: AuthCredentials, service_name: Optional[str] = None) \
-> Optional[AuthCredentials]:
""" """
使用Jellyfin用户辅助完成用户认证 使用Jellyfin用户辅助完成用户认证
:param credentials: 认证数据 :param credentials: 认证数据
:param service_name: 指定要认证的媒体服务器名称,若为 None 则认证所有服务
:return: 认证数据 :return: 认证数据
""" """
# Jellyfin认证 # Jellyfin认证
if not credentials or credentials.grant_type != "password": if not credentials or credentials.grant_type != "password":
return None return None
for name, server in self.get_instances().items(): # 确定要认证的服务器列表
if service_name:
# 如果指定了服务名,获取该服务实例
servers = [(service_name, server)] if (server := self.get_instance(service_name)) else []
else:
# 如果没有指定服务名,遍历所有服务
servers = self.get_instances().items()
# 遍历要认证的服务器
for name, server in servers:
# 触发认证拦截事件 # 触发认证拦截事件
intercept_event = eventmanager.send_event( intercept_event = eventmanager.send_event(
etype=ChainEventType.AuthIntercept, etype=ChainEventType.AuthIntercept,

View File

@@ -7,7 +7,7 @@ from app.log import logger
from app.modules import _ModuleBase, _MediaServerBase from app.modules import _ModuleBase, _MediaServerBase
from app.modules.plex.plex import Plex from app.modules.plex.plex import Plex
from app.schemas.event import AuthCredentials, AuthInterceptCredentials from app.schemas.event import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import MediaType, ModuleType, ChainEventType from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType
class PlexModule(_ModuleBase, _MediaServerBase[Plex]): class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
@@ -30,6 +30,13 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
""" """
return ModuleType.MediaServer return ModuleType.MediaServer
@staticmethod
def get_subtype() -> MediaServerType:
"""
获取模块子类型
"""
return MediaServerType.Plex
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """
@@ -66,16 +73,26 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
logger.info(f"Plex {name} 服务器连接断开,尝试重连 ...") logger.info(f"Plex {name} 服务器连接断开,尝试重连 ...")
server.reconnect() server.reconnect()
def user_authenticate(self, credentials: AuthCredentials) -> Optional[AuthCredentials]: def user_authenticate(self, credentials: AuthCredentials, service_name: Optional[str] = None) \
-> Optional[AuthCredentials]:
""" """
使用Plex用户辅助完成用户认证 使用Plex用户辅助完成用户认证
:param credentials: 认证数据 :param credentials: 认证数据
:param service_name: 指定要认证的媒体服务器名称,若为 None 则认证所有服务
:return: 认证数据 :return: 认证数据
""" """
# Plex认证 # Plex认证
if not credentials or credentials.grant_type != "password": if not credentials or credentials.grant_type != "password":
return None return None
for name, server in self.get_instances().items(): # 确定要认证的服务器列表
if service_name:
# 如果指定了服务名,获取该服务实例
servers = [(service_name, server)] if (server := self.get_instance(service_name)) else []
else:
# 如果没有指定服务名,遍历所有服务
servers = self.get_instances().items()
# 遍历要认证的服务器
for name, server in servers:
# 触发认证拦截事件 # 触发认证拦截事件
intercept_event = eventmanager.send_event( intercept_event = eventmanager.send_event(
etype=ChainEventType.AuthIntercept, etype=ChainEventType.AuthIntercept,

View File

@@ -11,7 +11,7 @@ from app.log import logger
from app.modules import _ModuleBase, _DownloaderBase from app.modules import _ModuleBase, _DownloaderBase
from app.modules.qbittorrent.qbittorrent import Qbittorrent from app.modules.qbittorrent.qbittorrent import Qbittorrent
from app.schemas import TransferTorrent, DownloadingTorrent from app.schemas import TransferTorrent, DownloadingTorrent
from app.schemas.types import TorrentStatus, ModuleType from app.schemas.types import TorrentStatus, ModuleType, DownloaderType
from app.utils.string import StringUtils from app.utils.string import StringUtils
@@ -35,6 +35,13 @@ class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
""" """
return ModuleType.Downloader return ModuleType.Downloader
@staticmethod
def get_subtype() -> DownloaderType:
"""
获取模块子类型
"""
return DownloaderType.Qbittorrent
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -31,6 +31,13 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
""" """
return ModuleType.Notification return ModuleType.Notification
@staticmethod
def get_subtype() -> MessageChannel:
"""
获取模块子类型
"""
return MessageChannel.Slack
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -10,7 +10,7 @@ from app.core.context import Context
from app.helper.torrent import TorrentHelper from app.helper.torrent import TorrentHelper
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.schemas.types import ModuleType from app.schemas.types import ModuleType, OtherModulesType
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from app.utils.string import StringUtils from app.utils.string import StringUtils
from app.utils.system import SystemUtils from app.utils.system import SystemUtils
@@ -40,6 +40,13 @@ class SubtitleModule(_ModuleBase):
""" """
return ModuleType.Other return ModuleType.Other
@staticmethod
def get_subtype() -> OtherModulesType:
"""
获取模块子类型
"""
return OtherModulesType.Subtitle
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -29,6 +29,13 @@ class SynologyChatModule(_ModuleBase, _MessageBase[SynologyChat]):
""" """
return ModuleType.Notification return ModuleType.Notification
@staticmethod
def get_subtype() -> MessageChannel:
"""
获取模块子类型
"""
return MessageChannel.SynologyChat
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -34,6 +34,13 @@ class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
""" """
return ModuleType.Notification return ModuleType.Notification
@staticmethod
def get_subtype() -> MessageChannel:
"""
获取模块子类型
"""
return MessageChannel.Telegram
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -14,7 +14,7 @@ from app.modules.themoviedb.scraper import TmdbScraper
from app.modules.themoviedb.tmdb_cache import TmdbCache from app.modules.themoviedb.tmdb_cache import TmdbCache
from app.modules.themoviedb.tmdbapi import TmdbApi from app.modules.themoviedb.tmdbapi import TmdbApi
from app.schemas import MediaPerson from app.schemas import MediaPerson
from app.schemas.types import MediaType, MediaImageType, ModuleType from app.schemas.types import MediaType, MediaImageType, ModuleType, MediaRecognizeType
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
@@ -49,6 +49,13 @@ class TheMovieDbModule(_ModuleBase):
""" """
return ModuleType.MediaRecognize return ModuleType.MediaRecognize
@staticmethod
def get_subtype() -> MediaRecognizeType:
"""
获取模块子类型
"""
return MediaRecognizeType.TMDB
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -161,7 +161,7 @@ class TmdbApi:
season_number: int = None) -> Optional[dict]: season_number: int = None) -> Optional[dict]:
""" """
搜索tmdb中的媒体信息匹配返回一条尽可能正确的信息 搜索tmdb中的媒体信息匹配返回一条尽可能正确的信息
:param name: 索的名称 :param name: 索的名称
:param mtype: 类型:电影、电视剧 :param mtype: 类型:电影、电视剧
:param year: 年份,如要是季集需要是首播年份(first_air_date) :param year: 年份,如要是季集需要是首播年份(first_air_date)
:param season_year: 当前季集年份 :param season_year: 当前季集年份

View File

@@ -4,7 +4,7 @@ from app.core.config import settings
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.thetvdb import tvdbapi from app.modules.thetvdb import tvdbapi
from app.schemas.types import ModuleType from app.schemas.types import ModuleType, MediaRecognizeType
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
@@ -28,6 +28,13 @@ class TheTvDbModule(_ModuleBase):
""" """
return ModuleType.MediaRecognize return ModuleType.MediaRecognize
@staticmethod
def get_subtype() -> MediaRecognizeType:
"""
获取模块子类型
"""
return MediaRecognizeType.TVDB
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -11,7 +11,7 @@ from app.log import logger
from app.modules import _ModuleBase, _DownloaderBase from app.modules import _ModuleBase, _DownloaderBase
from app.modules.transmission.transmission import Transmission from app.modules.transmission.transmission import Transmission
from app.schemas import TransferTorrent, DownloadingTorrent from app.schemas import TransferTorrent, DownloadingTorrent
from app.schemas.types import TorrentStatus, ModuleType from app.schemas.types import TorrentStatus, ModuleType, DownloaderType
from app.utils.string import StringUtils from app.utils.string import StringUtils
@@ -35,6 +35,13 @@ class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]):
""" """
return ModuleType.Downloader return ModuleType.Downloader
@staticmethod
def get_subtype() -> DownloaderType:
"""
获取模块子类型
"""
return DownloaderType.Transmission
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -30,6 +30,13 @@ class VoceChatModule(_ModuleBase, _MessageBase[VoceChat]):
""" """
return ModuleType.Notification return ModuleType.Notification
@staticmethod
def get_subtype() -> MessageChannel:
"""
获取模块子类型
"""
return MessageChannel.VoceChat
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -30,6 +30,13 @@ class WebPushModule(_ModuleBase, _MessageBase):
""" """
return ModuleType.Notification return ModuleType.Notification
@staticmethod
def get_subtype() -> MessageChannel:
"""
获取模块子类型
"""
return MessageChannel.WebPush
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -36,6 +36,13 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
""" """
return ModuleType.Notification return ModuleType.Notification
@staticmethod
def get_subtype() -> MessageChannel:
"""
获取模块的子类型
"""
return MessageChannel.Wechat
@staticmethod @staticmethod
def get_priority() -> int: def get_priority() -> int:
""" """

View File

@@ -225,7 +225,7 @@ 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): text: str = None, image: str = None, link: str = None, userid: str = None, username: str = None):
""" """
发送消息 发送消息
""" """
@@ -233,7 +233,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 image=image, link=link, userid=userid, username=username
)) ))
def close(self): def close(self):

View File

@@ -297,7 +297,8 @@ class Scheduler(metaclass=Singleton):
} }
) )
# 站点数据刷新每隔30分钟 # 站点数据刷新
if settings.SITEDATA_REFRESH_INTERVAL:
self._scheduler.add_job( self._scheduler.add_job(
self.start, self.start,
"interval", "interval",

View File

@@ -1,8 +1,11 @@
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any from typing import Optional, Dict, Any, List, Set
from pydantic import BaseModel, Field, root_validator from pydantic import BaseModel, Field, root_validator
from app.core.context import Context
from app.schemas import MessageChannel
class BaseEventData(BaseModel): class BaseEventData(BaseModel):
""" """
@@ -143,3 +146,60 @@ class TransferRenameEventData(ChainEventData):
updated: bool = Field(False, description="是否已更新") updated: bool = Field(False, description="是否已更新")
updated_str: Optional[str] = Field(None, description="更新后的字符串") updated_str: Optional[str] = Field(None, description="更新后的字符串")
source: Optional[str] = Field("未知拦截源", description="拦截源") source: Optional[str] = Field("未知拦截源", description="拦截源")
class ResourceSelectionEventData(BaseModel):
"""
ResourceSelection 事件的数据模型
Attributes:
# 输入参数
contexts (List[Context]): 当前待选择的资源上下文列表
source (str): 事件源,指示事件的触发来源
# 输出参数
updated (bool): 是否已更新,默认值为 False
updated_contexts (Optional[List[Context]]): 已更新的资源上下文列表,默认值为 None
source (str): 更新源,默认值为 "未知更新源"
"""
# 输入参数
contexts: Any = Field(None, description="待选择的资源上下文列表")
downloader: Optional[str] = Field(None, description="下载器")
origin: Optional[str] = Field(None, description="来源")
# 输出参数
updated: bool = Field(False, description="是否已更新")
updated_contexts: Optional[List[Context]] = Field(None, description="已更新的资源上下文列表")
source: Optional[str] = Field("未知拦截源", description="拦截源")
class ResourceDownloadEventData(ChainEventData):
"""
ResourceDownload 事件的数据模型
Attributes:
# 输入参数
context (Context): 当前资源上下文
episodes (Set[int]): 需要下载的集数
channel (MessageChannel): 通知渠道
origin (str): 来源消息通知、Subscribe、Manual等
downloader (str): 下载器
options (dict): 其他参数
# 输出参数
cancel (bool): 是否取消下载,默认值为 False
source (str): 拦截源,默认值为 "未知拦截源"
reason (str): 拦截原因,描述拦截的具体原因
"""
# 输入参数
context: Any = Field(None, description="当前资源上下文")
episodes: Optional[Set[int]] = Field(None, description="需要下载的集数")
channel: Optional[MessageChannel] = Field(None, description="通知渠道")
origin: Optional[str] = Field(None, description="来源")
downloader: Optional[str] = Field(None, description="下载器")
options: Optional[dict] = Field(None, description="其他参数")
# 输出参数
cancel: bool = Field(False, description="是否取消下载")
source: str = Field("未知拦截源", description="拦截源")
reason: str = Field("", description="拦截原因")

View File

@@ -89,3 +89,42 @@ class EpisodeFormat(BaseModel):
detail: Optional[str] = None detail: Optional[str] = None
part: Optional[str] = None part: Optional[str] = None
offset: Optional[str] = None offset: Optional[str] = None
class ManualTransferItem(BaseModel):
# 文件项
fileitem: FileItem = None
# 日志ID
logid: Optional[int] = None
# 目标存储
target_storage: Optional[str] = None
# 目标路径
target_path: Optional[str] = None
# TMDB ID
tmdbid: Optional[int] = None
# 豆瓣ID
doubanid: Optional[str] = None
# 类型
type_name: Optional[str] = None
# 季号
season: Optional[int] = None
# 整理方式
transfer_type: Optional[str] = None
# 自定义格式
episode_format: Optional[str] = None
# 指定集数
episode_detail: Optional[str] = None
# 指定PART
episode_part: Optional[str] = None
# 集数偏移
episode_offset: Optional[str] = None
# 最小文件大小
min_filesize: Optional[int] = 0
# 刮削
scrape: bool = False
# 媒体库类型子目录
library_type_folder: Optional[bool] = None
# 媒体库类别子目录
library_category_folder: Optional[bool] = None
# 复用历史识别信息
from_history: Optional[bool] = False

View File

@@ -48,6 +48,8 @@ class EventType(Enum):
NoticeMessage = "notice.message" NoticeMessage = "notice.message"
# 订阅已添加 # 订阅已添加
SubscribeAdded = "subscribe.added" SubscribeAdded = "subscribe.added"
# 订阅已删除
SubscribeDeleted = "subscribe.deleted"
# 订阅已完成 # 订阅已完成
SubscribeComplete = "subscribe.complete" SubscribeComplete = "subscribe.complete"
# 系统错误 # 系统错误
@@ -70,6 +72,10 @@ class ChainEventType(Enum):
CommandRegister = "command.register" CommandRegister = "command.register"
# 整理重命名 # 整理重命名
TransferRename = "transfer.rename" TransferRename = "transfer.rename"
# 资源选择
ResourceSelection = "resource.selection"
# 资源下载
ResourceDownload = "resource.download"
# 系统配置Key字典 # 系统配置Key字典
@@ -178,6 +184,52 @@ class MessageChannel(Enum):
WebPush = "WebPush" WebPush = "WebPush"
# 下载器类型
class DownloaderType(Enum):
# Qbittorrent
Qbittorrent = "Qbittorrent"
# Transmission
Transmission = "Transmission"
# Aria2
# Aria2 = "Aria2"
# 媒体服务器类型
class MediaServerType(Enum):
# Emby
Emby = "Emby"
# Jellyfin
Jellyfin = "Jellyfin"
# Plex
Plex = "Plex"
# 识别器类型
class MediaRecognizeType(Enum):
# 豆瓣
Douban = "豆瓣"
# TMDB
TMDB = "TheMovieDb"
# TVDB
TVDB = "TheTvDb"
# bangumi
Bangumi = "Bangumi"
# 其他杂项模块类型
class OtherModulesType(Enum):
# 字幕
Subtitle = "站点字幕"
# Fanart
Fanart = "Fanart"
# 文件整理
FileManager = "文件整理"
# 过滤器
Filter = "过滤器"
# 站点索引
Indexer = "站点索引"
# 用户配置Key字典 # 用户配置Key字典
class UserConfigKey(Enum): class UserConfigKey(Enum):
# 监控面板 # 监控面板

View File

@@ -58,6 +58,6 @@ pystray~=0.19.5
pyotp~=2.9.0 pyotp~=2.9.0
Pinyin2Hanzi~=0.1.1 Pinyin2Hanzi~=0.1.1
pywebpush~=2.0.0 pywebpush~=2.0.0
python-115~=0.0.9.8.7 python-115~=0.0.9.8.8.3
aligo~=6.2.4 aligo~=6.2.4
aiofiles~=24.1.0 aiofiles~=24.1.0

View File

@@ -1,2 +1,2 @@
APP_VERSION = 'v2.1.1' APP_VERSION = 'v2.1.3'
FRONTEND_VERSION = 'v2.1.1' FRONTEND_VERSION = 'v2.1.4'