Compare commits

...

22 Commits

Author SHA1 Message Date
jxxghp
da4ff99570 fix #655 2023-09-25 08:40:19 +08:00
jxxghp
b3c0dc813b fix #662 2023-09-25 07:12:36 +08:00
jxxghp
a7b51d9fcc fix bug 2023-09-24 19:48:03 +08:00
jxxghp
76f1de42a8 v1.2.5 2023-09-24 19:33:25 +08:00
jxxghp
bad016b2b4 rollback mteam 2023-09-24 19:29:24 +08:00
jxxghp
5cd48d5447 fix 优化定时服务调度 2023-09-24 12:41:59 +08:00
jxxghp
41ff5363ea Merge remote-tracking branch 'origin/main' 2023-09-24 11:14:00 +08:00
jxxghp
85014f4acb feat 服务手动触发 2023-09-24 11:13:49 +08:00
jxxghp
d9a68daddd Merge pull request #658 from WithdewHua/fix-torrentremover 2023-09-24 08:02:45 +08:00
WithdewHua
141e78f274 fix: 种子分类为空时被删除 2023-09-24 02:58:24 +08:00
jxxghp
de98ccd33c fix mteam、zhuque登录判定 2023-09-23 21:42:21 +08:00
jxxghp
d490dadfdd fix mteam 2023-09-23 16:35:27 +08:00
jxxghp
f46bbf73ba Merge pull request #654 from DDS-Derek/main
fix: container id retrieval error
2023-09-23 16:21:05 +08:00
jxxghp
17eba86f7a fix mteam 2023-09-23 16:20:08 +08:00
DDSRem
fdf25b8c66 fix: container id retrieval error 2023-09-23 16:04:25 +08:00
jxxghp
516cb443b9 fix mteam 2023-09-23 15:58:42 +08:00
jxxghp
7c4c3b3f9a feat 支持新版本mteam 2023-09-23 12:30:19 +08:00
jxxghp
e298a1a8a0 feat 支持新版本mteam 2023-09-23 12:02:04 +08:00
jxxghp
fd9eef2089 feat 支持多媒体服务器同时使用 2023-09-23 09:20:51 +08:00
jxxghp
78dab04c96 fix #650 2023-09-23 08:33:49 +08:00
jxxghp
c34475653f Merge pull request #652 from WithdewHua/fix-torrentremover 2023-09-22 22:45:17 +08:00
WithdewHua
eb6a6eee0a fix: 种子分类为空时被删除 2023-09-22 21:27:48 +08:00
41 changed files with 1151 additions and 629 deletions

View File

@@ -132,7 +132,7 @@ docker pull jxxghp/moviepilot:latest
- **DOWNLOADER_MONITOR** 下载器监控,`true`/`false`,默认为`true`,开启后下载完成时才会自动整理入库 - **DOWNLOADER_MONITOR** 下载器监控,`true`/`false`,默认为`true`,开启后下载完成时才会自动整理入库
- **MEDIASERVER** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby` - **MEDIASERVER** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时开启多个使用`,`分隔。还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
- `emby`设置项: - `emby`设置项:

View File

@@ -27,4 +27,4 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
pass pass

View File

@@ -0,0 +1,29 @@
"""1.0.7
Revision ID: 30329639c12b
Revises: 232dfa044617
Create Date: 2023-09-23 08:25:59.776488
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '30329639c12b'
down_revision = '232dfa044617'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.execute("delete from systemconfig where key = 'DefaultFilterRules';")
op.execute(
"insert into systemconfig(key, value) VALUES('DefaultFilterRules', (select value from systemconfig where key= 'DefaultIncludeExcludeFilter'));")
op.execute("delete from systemconfig where key = 'DefaultIncludeExcludeFilter';")
# ### end Alembic commands ###
def downgrade() -> None:
pass

View File

@@ -1,5 +1,5 @@
from pathlib import Path from pathlib import Path
from typing import Any, List from typing import Any, List, Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from requests import Session from requests import Session
@@ -11,9 +11,7 @@ from app.core.security import verify_token
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.scheduler import Scheduler from app.scheduler import Scheduler
from app.utils.string import StringUtils
from app.utils.system import SystemUtils from app.utils.system import SystemUtils
from app.utils.timer import TimerUtils
router = APIRouter() router = APIRouter()
@@ -24,14 +22,16 @@ def statistic(db: Session = Depends(get_db),
""" """
查询媒体数量统计信息 查询媒体数量统计信息
""" """
media_statistic = DashboardChain(db).media_statistic() media_statistics: Optional[List[schemas.Statistic]] = DashboardChain(db).media_statistic()
if media_statistic: if media_statistics:
return schemas.Statistic( # 汇总各媒体库统计信息
movie_count=media_statistic.movie_count, ret_statistic = schemas.Statistic()
tv_count=media_statistic.tv_count, for media_statistic in media_statistics:
episode_count=media_statistic.episode_count, ret_statistic.movie_count += media_statistic.movie_count
user_count=media_statistic.user_count ret_statistic.tv_count += media_statistic.tv_count
) ret_statistic.episode_count += media_statistic.episode_count
ret_statistic.user_count += media_statistic.user_count
return ret_statistic
else: else:
return schemas.Statistic() return schemas.Statistic()
@@ -81,37 +81,7 @@ def schedule(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
查询后台服务信息 查询后台服务信息
""" """
# 返回计时任务 return Scheduler().list()
schedulers = []
# 去重
added = []
jobs = Scheduler().list()
# 按照下次运行时间排序
jobs.sort(key=lambda x: x.next_run_time)
for job in jobs:
if job.name not in added:
added.append(job.name)
else:
continue
if not StringUtils.is_chinese(job.name):
continue
if not job.next_run_time:
status = "已停止"
next_run = ""
else:
next_run = TimerUtils.time_difference(job.next_run_time)
if not next_run:
status = "正在运行"
else:
status = "阻塞" if job.pending else "等待"
schedulers.append(schemas.ScheduleInfo(
id=job.id,
name=job.name,
status=status,
next_run=next_run
))
return schedulers
@router.get("/transfer", summary="文件整理统计", response_model=List[int]) @router.get("/transfer", summary="文件整理统计", response_model=List[int])

View File

@@ -1,12 +1,10 @@
from typing import Any, List from typing import Any, List
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app import schemas from app import schemas
from app.core.plugin import PluginManager from app.core.plugin import PluginManager
from app.core.security import verify_token from app.core.security import verify_token
from app.db import get_db
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey

View File

@@ -5,7 +5,6 @@ from sqlalchemy.orm import Session
from starlette.background import BackgroundTasks from starlette.background import BackgroundTasks
from app import schemas from app import schemas
from app.chain.cookiecloud import CookieCloudChain
from app.chain.site import SiteChain from app.chain.site import SiteChain
from app.chain.torrents import TorrentsChain from app.chain.torrents import TorrentsChain
from app.core.event import EventManager from app.core.event import EventManager
@@ -15,19 +14,13 @@ from app.db.models.site import Site
from app.db.models.siteicon import SiteIcon from app.db.models.siteicon import SiteIcon
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.helper.sites import SitesHelper from app.helper.sites import SitesHelper
from app.scheduler import Scheduler
from app.schemas.types import SystemConfigKey, EventType from app.schemas.types import SystemConfigKey, EventType
from app.utils.string import StringUtils from app.utils.string import StringUtils
router = APIRouter() router = APIRouter()
def start_cookiecloud_sync(db: Session):
"""
后台启动CookieCloud站点同步
"""
CookieCloudChain(db).process(manual=True)
@router.get("/", summary="所有站点", response_model=List[schemas.Site]) @router.get("/", summary="所有站点", response_model=List[schemas.Site])
def read_sites(db: Session = Depends(get_db), def read_sites(db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> List[dict]: _: schemas.TokenPayload = Depends(verify_token)) -> List[dict]:
@@ -101,12 +94,11 @@ def delete_site(
@router.get("/cookiecloud", summary="CookieCloud同步", response_model=schemas.Response) @router.get("/cookiecloud", summary="CookieCloud同步", response_model=schemas.Response)
def cookie_cloud_sync(background_tasks: BackgroundTasks, def cookie_cloud_sync(background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
运行CookieCloud同步站点信息 运行CookieCloud同步站点信息
""" """
background_tasks.add_task(start_cookiecloud_sync, db) background_tasks.add_task(Scheduler().start, job_id="cookiecloud")
return schemas.Response(success=True, message="CookieCloud同步任务已启动") return schemas.Response(success=True, message="CookieCloud同步任务已启动")
@@ -119,7 +111,8 @@ def cookie_cloud_sync(db: Session = Depends(get_db),
Site.reset(db) Site.reset(db)
SystemConfigOper().set(SystemConfigKey.IndexerSites, []) SystemConfigOper().set(SystemConfigKey.IndexerSites, [])
SystemConfigOper().set(SystemConfigKey.RssSites, []) SystemConfigOper().set(SystemConfigKey.RssSites, [])
CookieCloudChain().process(manual=True) # 启动定时服务
Scheduler().start("cookiecloud", manual=True)
# 插件站点删除 # 插件站点删除
EventManager().send_event(EventType.SiteDeleted, EventManager().send_event(EventType.SiteDeleted,
{ {

View File

@@ -1,5 +1,5 @@
import json import json
from typing import List, Any, Optional from typing import List, Any
from fastapi import APIRouter, Request, BackgroundTasks, Depends, HTTPException, Header from fastapi import APIRouter, Request, BackgroundTasks, Depends, HTTPException, Header
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -12,6 +12,7 @@ from app.db import get_db
from app.db.models.subscribe import Subscribe from app.db.models.subscribe import Subscribe
from app.db.models.user import User from app.db.models.user import User
from app.db.userauth import get_current_active_user from app.db.userauth import get_current_active_user
from app.scheduler import Scheduler
from app.schemas.types import MediaType from app.schemas.types import MediaType
router = APIRouter() router = APIRouter()
@@ -26,13 +27,6 @@ def start_subscribe_add(db: Session, title: str, year: str,
mtype=mtype, tmdbid=tmdbid, season=season, username=username) mtype=mtype, tmdbid=tmdbid, season=season, username=username)
def start_subscribe_search(db: Session, sid: Optional[int], state: Optional[str]):
"""
启动订阅搜索任务
"""
SubscribeChain(db).search(sid=sid, state=state, manual=True)
@router.get("/", summary="所有订阅", response_model=List[schemas.Subscribe]) @router.get("/", summary="所有订阅", response_model=List[schemas.Subscribe])
def read_subscribes( def read_subscribes(
db: Session = Depends(get_db), db: Session = Depends(get_db),
@@ -140,35 +134,36 @@ def subscribe_mediaid(
@router.get("/refresh", summary="刷新订阅", response_model=schemas.Response) @router.get("/refresh", summary="刷新订阅", response_model=schemas.Response)
def refresh_subscribes( def refresh_subscribes(
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
刷新所有订阅 刷新所有订阅
""" """
SubscribeChain(db).refresh() Scheduler().start("subscribe_refresh")
return schemas.Response(success=True) return schemas.Response(success=True)
@router.get("/check", summary="刷新订阅 TMDB 信息", response_model=schemas.Response) @router.get("/check", summary="刷新订阅 TMDB 信息", response_model=schemas.Response)
def check_subscribes( def check_subscribes(
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
刷新所有订阅 刷新订阅 TMDB 信息
""" """
SubscribeChain(db).check() Scheduler().start("subscribe_tmdb")
return schemas.Response(success=True) return schemas.Response(success=True)
@router.get("/search", summary="搜索所有订阅", response_model=schemas.Response) @router.get("/search", summary="搜索所有订阅", response_model=schemas.Response)
def search_subscribes( def search_subscribes(
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
搜索所有订阅 搜索所有订阅
""" """
background_tasks.add_task(start_subscribe_search, db=db, sid=None, state='R') background_tasks.add_task(
Scheduler().start,
job_id="subscribe_search",
sid=None, state='R'
)
return schemas.Response(success=True) return schemas.Response(success=True)
@@ -176,12 +171,15 @@ def search_subscribes(
def search_subscribe( def search_subscribe(
subscribe_id: int, subscribe_id: int,
background_tasks: BackgroundTasks, background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:
""" """
根据订阅编号搜索订阅 根据订阅编号搜索订阅
""" """
background_tasks.add_task(start_subscribe_search, db=db, sid=subscribe_id, state=None) background_tasks.add_task(
Scheduler().start,
job_id="subscribe_search",
sid=subscribe_id, state=None
)
return schemas.Response(success=True) return schemas.Response(success=True)

View File

@@ -1,9 +1,9 @@
import json import json
import time import time
import tailer
from datetime import datetime from datetime import datetime
from typing import Union from typing import Union
import tailer
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -16,6 +16,7 @@ from app.db import get_db
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.helper.message import MessageHelper from app.helper.message import MessageHelper
from app.helper.progress import ProgressHelper from app.helper.progress import ProgressHelper
from app.scheduler import Scheduler
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from app.utils.system import SystemUtils from app.utils.system import SystemUtils
@@ -211,3 +212,15 @@ def restart_system(_: schemas.TokenPayload = Depends(verify_token)):
# 执行重启 # 执行重启
ret, msg = SystemUtils.restart() ret, msg = SystemUtils.restart()
return schemas.Response(success=ret, message=msg) return schemas.Response(success=ret, message=msg)
@router.get("/runscheduler", summary="运行服务", response_model=schemas.Response)
def execute_command(jobid: str,
_: schemas.TokenPayload = Depends(verify_token)):
"""
执行命令
"""
if not jobid:
return schemas.Response(success=False, message="命令不能为空!")
Scheduler().start(jobid)
return schemas.Response(success=True)

View File

@@ -684,7 +684,7 @@ def arr_serie(apikey: str, tid: int, db: Session = Depends(get_db)) -> Any:
"monitored": True, "monitored": True,
}], }],
year=subscribe.year, year=subscribe.year,
remotePoster=subscribe.image, remotePoster=subscribe.poster,
tmdbId=subscribe.tmdbid, tmdbId=subscribe.tmdbid,
tvdbId=subscribe.tvdbid, tvdbId=subscribe.tvdbid,
imdbId=subscribe.imdbid, imdbId=subscribe.imdbid,

View File

@@ -336,14 +336,14 @@ class ChainBase(metaclass=ABCMeta):
""" """
return self.run_module("media_exists", mediainfo=mediainfo, itemid=itemid) return self.run_module("media_exists", mediainfo=mediainfo, itemid=itemid)
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]: def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
""" """
刷新媒体库 刷新媒体库
:param mediainfo: 识别的媒体信息 :param mediainfo: 识别的媒体信息
:param file_path: 文件路径 :param file_path: 文件路径
:return: 成功或失败 :return: 成功或失败
""" """
return self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path) self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path)
def post_message(self, message: Notification) -> None: def post_message(self, message: Notification) -> None:
""" """
@@ -391,22 +391,22 @@ class ChainBase(metaclass=ABCMeta):
:param mediainfo: 识别的媒体信息 :param mediainfo: 识别的媒体信息
:return: 成功或失败 :return: 成功或失败
""" """
return self.run_module("scrape_metadata", path=path, mediainfo=mediainfo) self.run_module("scrape_metadata", path=path, mediainfo=mediainfo)
def register_commands(self, commands: Dict[str, dict]) -> None: def register_commands(self, commands: Dict[str, dict]) -> None:
""" """
注册菜单命令 注册菜单命令
""" """
return self.run_module("register_commands", commands=commands) self.run_module("register_commands", commands=commands)
def scheduler_job(self) -> None: def scheduler_job(self) -> None:
""" """
定时任务每10分钟调用一次模块实现该接口以实现定时服务 定时任务每10分钟调用一次模块实现该接口以实现定时服务
""" """
return self.run_module("scheduler_job") self.run_module("scheduler_job")
def clear_cache(self) -> None: def clear_cache(self) -> None:
""" """
清理缓存,模块实现该接口响应清理缓存事件 清理缓存,模块实现该接口响应清理缓存事件
""" """
return self.run_module("clear_cache") self.run_module("clear_cache")

View File

@@ -1,5 +1,5 @@
import base64 import base64
from typing import Tuple, Optional, Union from typing import Tuple, Optional
from urllib.parse import urljoin from urllib.parse import urljoin
from lxml import etree from lxml import etree
@@ -16,7 +16,6 @@ from app.helper.message import MessageHelper
from app.helper.rss import RssHelper from app.helper.rss import RssHelper
from app.helper.sites import SitesHelper from app.helper.sites import SitesHelper
from app.log import logger from app.log import logger
from app.schemas import Notification, NotificationType, MessageChannel
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from app.utils.site import SiteUtils from app.utils.site import SiteUtils
@@ -40,21 +39,6 @@ class CookieCloudChain(ChainBase):
password=settings.COOKIECLOUD_PASSWORD password=settings.COOKIECLOUD_PASSWORD
) )
def remote_sync(self, channel: MessageChannel, userid: Union[int, str]):
"""
远程触发同步站点,发送消息
"""
self.post_message(Notification(channel=channel, mtype=NotificationType.SiteMessage,
title="开始同步CookieCloud站点 ...", userid=userid))
# 开始同步
success, msg = self.process()
if success:
self.post_message(Notification(channel=channel, mtype=NotificationType.SiteMessage,
title=f"同步站点成功,{msg}", userid=userid))
else:
self.post_message(Notification(channel=channel, mtype=NotificationType.SiteMessage,
title=f"同步站点失败:{msg}", userid=userid))
def process(self, manual=False) -> Tuple[bool, str]: def process(self, manual=False) -> Tuple[bool, str]:
""" """
通过CookieCloud同步站点Cookie 通过CookieCloud同步站点Cookie

View File

@@ -1,3 +1,5 @@
from typing import Optional, List
from app import schemas from app import schemas
from app.chain import ChainBase from app.chain import ChainBase
@@ -6,7 +8,7 @@ class DashboardChain(ChainBase):
""" """
各类仪表板统计处理链 各类仪表板统计处理链
""" """
def media_statistic(self) -> schemas.Statistic: def media_statistic(self) -> Optional[List[schemas.Statistic]]:
""" """
媒体数量统计 媒体数量统计
""" """

View File

@@ -1,3 +1,5 @@
import base64
import json
import re import re
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple, Set, Dict, Union from typing import List, Optional, Tuple, Set, Dict, Union
@@ -15,6 +17,7 @@ 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.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType
from app.utils.http import RequestUtils
from app.utils.string import StringUtils from app.utils.string import StringUtils
@@ -79,8 +82,68 @@ class DownloadChain(ChainBase):
下载种子文件,如果是磁力链,会返回磁力链接本身 下载种子文件,如果是磁力链,会返回磁力链接本身
:return: 种子路径,种子目录名,种子文件清单 :return: 种子路径,种子目录名,种子文件清单
""" """
def __get_redict_url(url: str, ua: str = None, cookie: str = None) -> Optional[str]:
"""
获取下载链接, url格式[base64]url
"""
# 获取[]中的内容
m = re.search(r"\[(.*)](.*)", url)
if m:
# 参数
base64_str = m.group(1)
# URL
url = m.group(2)
if not base64_str:
return url
# 解码参数
req_str = base64.b64decode(base64_str.encode('utf-8')).decode('utf-8')
req_params: Dict[str, dict] = json.loads(req_str)
if req_params.get('method') == 'get':
# GET请求
res = RequestUtils(
ua=ua,
cookies=cookie
).get_res(url, params=req_params.get('params'))
else:
# POST请求
res = RequestUtils(
ua=ua,
cookies=cookie
).post_res(url, params=req_params.get('params'))
if not res:
return None
if not req_params.get('result'):
return res.text
else:
data = res.json()
for key in str(req_params.get('result')).split("."):
data = data.get(key)
if not data:
return None
logger.info(f"获取到下载地址:{data}")
return data
return None
# 获取下载链接
if not torrent.enclosure:
return None, "", []
if torrent.enclosure.startswith("magnet:"):
return torrent.enclosure, "", []
if torrent.enclosure.startswith("["):
# 需要解码获取下载地址
torrent_url = __get_redict_url(url=torrent.enclosure,
ua=torrent.site_ua,
cookie=torrent.site_cookie)
else:
torrent_url = torrent.enclosure
if not torrent_url:
logger.error(f"{torrent.title} 无法获取下载地址:{torrent.enclosure}")
return None, "", []
# 下载种子文件
torrent_file, content, download_folder, files, error_msg = self.torrent.download_torrent( torrent_file, content, download_folder, files, error_msg = self.torrent.download_torrent(
url=torrent.enclosure, url=torrent_url,
cookie=torrent.site_cookie, cookie=torrent.site_cookie,
ua=torrent.site_ua, ua=torrent.site_ua,
proxy=torrent.site_proxy) proxy=torrent.site_proxy)
@@ -90,7 +153,7 @@ class DownloadChain(ChainBase):
return content, "", [] return content, "", []
if not torrent_file: if not torrent_file:
logger.error(f"下载种子文件失败:{torrent.title} - {torrent.enclosure}") logger.error(f"下载种子文件失败:{torrent.title} - {torrent_url}")
self.post_message(Notification( self.post_message(Notification(
channel=channel, channel=channel,
mtype=NotificationType.Manual, mtype=NotificationType.Manual,
@@ -122,7 +185,9 @@ class DownloadChain(ChainBase):
_folder_name = "" _folder_name = ""
if not torrent_file: if not torrent_file:
# 下载种子文件,得到的可能是文件也可能是磁力链 # 下载种子文件,得到的可能是文件也可能是磁力链
content, _folder_name, _file_list = self.download_torrent(_torrent, userid=userid) content, _folder_name, _file_list = self.download_torrent(_torrent,
channel=channel,
userid=userid)
if not content: if not content:
return return
else: else:
@@ -253,12 +318,14 @@ class DownloadChain(ChainBase):
contexts: List[Context], contexts: List[Context],
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None, no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
save_path: str = None, save_path: str = None,
channel: str = None,
userid: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]: userid: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
""" """
根据缺失数据,自动种子列表中组合择优下载 根据缺失数据,自动种子列表中组合择优下载
:param contexts: 资源上下文列表 :param contexts: 资源上下文列表
:param no_exists: 缺失的剧集信息 :param no_exists: 缺失的剧集信息
:param save_path: 保存路径 :param save_path: 保存路径
:param channel: 通知渠道
:param userid: 用户ID :param userid: 用户ID
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id] = {season: NotExistMediaInfo} :return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id] = {season: NotExistMediaInfo}
""" """
@@ -323,7 +390,8 @@ class DownloadChain(ChainBase):
# 如果是电影,直接下载 # 如果是电影,直接下载
for context in contexts: for context in contexts:
if context.media_info.type == MediaType.MOVIE: if context.media_info.type == MediaType.MOVIE:
if self.download_single(context, save_path=save_path, userid=userid): if self.download_single(context, save_path=save_path,
channel=channel, userid=userid):
# 下载成功 # 下载成功
downloaded_list.append(context) downloaded_list.append(context)
@@ -390,11 +458,13 @@ class DownloadChain(ChainBase):
context=context, context=context,
torrent_file=content if isinstance(content, Path) else None, torrent_file=content if isinstance(content, Path) else None,
save_path=save_path, save_path=save_path,
channel=channel,
userid=userid userid=userid
) )
else: else:
# 下载 # 下载
download_id = self.download_single(context, save_path=save_path, userid=userid) download_id = self.download_single(context, save_path=save_path,
channel=channel, userid=userid)
if download_id: if download_id:
# 下载成功 # 下载成功
@@ -452,7 +522,8 @@ class DownloadChain(ChainBase):
# 为需要集的子集则下载 # 为需要集的子集则下载
if torrent_episodes.issubset(set(need_episodes)): if torrent_episodes.issubset(set(need_episodes)):
# 下载 # 下载
download_id = self.download_single(context, save_path=save_path, userid=userid) download_id = self.download_single(context, save_path=save_path,
channel=channel, userid=userid)
if download_id: if download_id:
# 下载成功 # 下载成功
downloaded_list.append(context) downloaded_list.append(context)
@@ -508,7 +579,7 @@ class DownloadChain(ChainBase):
and len(meta.season_list) == 1 \ and len(meta.season_list) == 1 \
and meta.season_list[0] == need_season: and meta.season_list[0] == need_season:
# 检查种子看是否有需要的集 # 检查种子看是否有需要的集
content, _, torrent_files = self.download_torrent(torrent, userid=userid) content, _, torrent_files = self.download_torrent(torrent)
if not content: if not content:
continue continue
if isinstance(content, str): if isinstance(content, str):
@@ -529,6 +600,7 @@ class DownloadChain(ChainBase):
torrent_file=content if isinstance(content, Path) else None, torrent_file=content if isinstance(content, Path) else None,
episodes=selected_episodes, episodes=selected_episodes,
save_path=save_path, save_path=save_path,
channel=channel,
userid=userid userid=userid
) )
if not download_id: if not download_id:

View File

@@ -10,7 +10,6 @@ from app.core.config import settings
from app.db import SessionFactory from app.db import SessionFactory
from app.db.mediaserver_oper import MediaServerOper from app.db.mediaserver_oper import MediaServerOper
from app.log import logger from app.log import logger
from app.schemas import MessageChannel, Notification
lock = threading.Lock() lock = threading.Lock()
@@ -23,33 +22,23 @@ class MediaServerChain(ChainBase):
def __init__(self, db: Session = None): def __init__(self, db: Session = None):
super().__init__(db) super().__init__(db)
def librarys(self) -> List[schemas.MediaServerLibrary]: def librarys(self, server: str) -> List[schemas.MediaServerLibrary]:
""" """
获取媒体服务器所有媒体库 获取媒体服务器所有媒体库
""" """
return self.run_module("mediaserver_librarys") return self.run_module("mediaserver_librarys", server=server)
def items(self, library_id: Union[str, int]) -> Generator: def items(self, server: str, library_id: Union[str, int]) -> Generator:
""" """
获取媒体服务器所有项目 获取媒体服务器所有项目
""" """
return self.run_module("mediaserver_items", library_id=library_id) return self.run_module("mediaserver_items", server=server, library_id=library_id)
def episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]: def episodes(self, server: str, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
""" """
获取媒体服务器剧集信息 获取媒体服务器剧集信息
""" """
return self.run_module("mediaserver_tv_episodes", item_id=item_id) return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id)
def remote_sync(self, channel: MessageChannel, userid: Union[int, str]):
"""
同步豆瓣想看数据,发送消息
"""
self.post_message(Notification(channel=channel,
title="开始媒体服务器 ...", userid=userid))
self.sync()
self.post_message(Notification(channel=channel,
title="同步媒体服务器完成!", userid=userid))
def sync(self): def sync(self):
""" """
@@ -59,7 +48,6 @@ class MediaServerChain(ChainBase):
# 媒体服务器同步使用独立的会话 # 媒体服务器同步使用独立的会话
_db = SessionFactory() _db = SessionFactory()
_dbOper = MediaServerOper(_db) _dbOper = MediaServerOper(_db)
logger.info("开始同步媒体库数据 ...")
# 汇总统计 # 汇总统计
total_count = 0 total_count = 0
# 清空登记薄 # 清空登记薄
@@ -67,35 +55,42 @@ class MediaServerChain(ChainBase):
# 同步黑名单 # 同步黑名单
sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split( sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split(
",") if settings.MEDIASERVER_SYNC_BLACKLIST else [] ",") if settings.MEDIASERVER_SYNC_BLACKLIST else []
for library in self.librarys(): # 设置的媒体服务器
if library.name in sync_blacklist: if not settings.MEDIASERVER:
return
mediaservers = settings.MEDIASERVER.split(",")
# 遍历媒体服务器
for mediaserver in mediaservers:
logger.info(f"开始同步媒体库 {mediaserver} 的数据 ...")
for library in self.librarys(mediaserver):
# 同步黑名单 跳过 # 同步黑名单 跳过
continue if library.name in sync_blacklist:
logger.info(f"正在同步媒体库 {library.name} ...")
library_count = 0
for item in self.items(library.id):
if not item:
continue continue
if not item.item_id: logger.info(f"正在同步 {mediaserver} 媒体库 {library.name} ...")
continue library_count = 0
# 计数 for item in self.items(mediaserver, library.id):
library_count += 1 if not item:
seasoninfo = {} continue
# 类型 if not item.item_id:
item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影" continue
if item_type == "电视剧": # 计数
# 查询剧集信息 library_count += 1
espisodes_info = self.episodes(item.item_id) or [] seasoninfo = {}
for episode in espisodes_info: # 类型
seasoninfo[episode.season] = episode.episodes item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影"
# 插入数据 if item_type == "电视剧":
item_dict = item.dict() # 查询剧集信息
item_dict['seasoninfo'] = json.dumps(seasoninfo) espisodes_info = self.episodes(mediaserver, item.item_id) or []
item_dict['item_type'] = item_type for episode in espisodes_info:
_dbOper.add(**item_dict) seasoninfo[episode.season] = episode.episodes
logger.info(f"媒体库 {library.name} 同步完成,共同步数量:{library_count}") # 插入数据
# 总数累加 item_dict = item.dict()
total_count += library_count item_dict['seasoninfo'] = json.dumps(seasoninfo)
item_dict['item_type'] = item_type
_dbOper.add(**item_dict)
logger.info(f"{mediaserver} 媒体库 {library.name} 同步完成,共同步数量:{library_count}")
# 总数累加
total_count += library_count
# 关闭数据库连接 # 关闭数据库连接
if _db: if _db:
_db.close() _db.close()

View File

@@ -348,6 +348,7 @@ class MessageChain(ChainBase):
# 批量下载 # 批量下载
downloads, lefts = self.downloadchain.batch_download(contexts=cache_list, downloads, lefts = self.downloadchain.batch_download(contexts=cache_list,
no_exists=no_exists, no_exists=no_exists,
channel=channel,
userid=userid) userid=userid)
if downloads and not lefts: if downloads and not lefts:
# 全部下载完成 # 全部下载完成

View File

@@ -81,7 +81,8 @@ class SearchChain(ChainBase):
keyword: str = None, keyword: str = None,
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None, no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
sites: List[int] = None, sites: List[int] = None,
filter_rule: str = None, priority_rule: str = None,
filter_rule: Dict[str, str] = None,
area: str = "title") -> List[Context]: area: str = "title") -> List[Context]:
""" """
根据媒体信息搜索种子资源精确匹配应用过滤规则同时根据no_exists过滤本地已存在的资源 根据媒体信息搜索种子资源精确匹配应用过滤规则同时根据no_exists过滤本地已存在的资源
@@ -89,7 +90,8 @@ class SearchChain(ChainBase):
:param keyword: 搜索关键词 :param keyword: 搜索关键词
:param no_exists: 缺失的媒体信息 :param no_exists: 缺失的媒体信息
:param sites: 站点ID列表为空时搜索所有站点 :param sites: 站点ID列表为空时搜索所有站点
:param filter_rule: 过滤规则,为空使用默认搜索过滤规则 :param priority_rule: 优先级规则,为空使用搜索优先级规则
:param filter_rule: 过滤规则,为空是使用默认过滤规则
:param area: 搜索范围title or imdbid :param area: 搜索范围title or imdbid
""" """
logger.info(f'开始搜索资源,关键词:{keyword or mediainfo.title} ...') logger.info(f'开始搜索资源,关键词:{keyword or mediainfo.title} ...')
@@ -129,12 +131,12 @@ class SearchChain(ChainBase):
logger.warn(f'{keyword or mediainfo.title} 未搜索到资源') logger.warn(f'{keyword or mediainfo.title} 未搜索到资源')
return [] return []
# 过滤种子 # 过滤种子
if filter_rule is None: if priority_rule is None:
# 取搜索优先级规则 # 取搜索优先级规则
filter_rule = self.systemconfig.get(SystemConfigKey.SearchFilterRules) priority_rule = self.systemconfig.get(SystemConfigKey.SearchFilterRules)
if filter_rule: if priority_rule:
logger.info(f'开始过滤资源,当前规则:{filter_rule} ...') logger.info(f'开始过滤资源,当前规则:{priority_rule} ...')
result: List[TorrentInfo] = self.filter_torrents(rule_string=filter_rule, result: List[TorrentInfo] = self.filter_torrents(rule_string=priority_rule,
torrent_list=torrents, torrent_list=torrents,
season_episodes=season_episodes, season_episodes=season_episodes,
mediainfo=mediainfo) mediainfo=mediainfo)
@@ -144,9 +146,10 @@ class SearchChain(ChainBase):
logger.warn(f'{keyword or mediainfo.title} 没有符合优先级规则的资源') logger.warn(f'{keyword or mediainfo.title} 没有符合优先级规则的资源')
return [] return []
# 使用默认过滤规则再次过滤 # 使用默认过滤规则再次过滤
torrents = self.filter_torrents_by_default_rule(torrents) torrents = self.filter_torrents_by_rule(torrents=torrents,
filter_rule=filter_rule)
if not torrents: if not torrents:
logger.warn(f'{keyword or mediainfo.title} 没有符合默认过滤规则的资源') logger.warn(f'{keyword or mediainfo.title} 没有符合过滤规则的资源')
return [] return []
# 匹配的资源 # 匹配的资源
_match_torrents = [] _match_torrents = []
@@ -314,28 +317,43 @@ class SearchChain(ChainBase):
# 返回 # 返回
return results return results
def filter_torrents_by_default_rule(self, def filter_torrents_by_rule(self,
torrents: List[TorrentInfo]) -> List[TorrentInfo]: torrents: List[TorrentInfo],
filter_rule: Dict[str, str] = None
) -> List[TorrentInfo]:
"""
使用过滤规则过滤种子
:param torrents: 种子列表
:param filter_rule: 过滤规则
"""
# 取默认过滤规则 # 取默认过滤规则
default_include_exclude = self.systemconfig.get(SystemConfigKey.DefaultIncludeExcludeFilter) or {} if not filter_rule:
if not default_include_exclude: filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
if not filter_rule:
return torrents return torrents
include = default_include_exclude.get("include") # 包含
exclude = default_include_exclude.get("exclude") include = filter_rule.get("include")
new_torrents = [] # 排除
for torrent in torrents: exclude = filter_rule.get("exclude")
def __filter_torrent(t: TorrentInfo) -> bool:
"""
过滤种子
"""
# 包含 # 包含
if include: if include:
if not re.search(r"%s" % include, if not re.search(r"%s" % include,
f"{torrent.title} {torrent.description}", re.I): f"{t.title} {t.description}", re.I):
logger.info(f"{torrent.title} 不匹配包含规则 {include}") logger.info(f"{t.title} 不匹配包含规则 {include}")
continue return False
# 排除 # 排除
if exclude: if exclude:
if re.search(r"%s" % exclude, if re.search(r"%s" % exclude,
f"{torrent.title} {torrent.description}", re.I): f"{t.title} {t.description}", re.I):
logger.info(f"{torrent.title} 匹配排除规则 {exclude}") logger.info(f"{t.title} 匹配排除规则 {exclude}")
continue return False
new_torrents.append(torrent) return True
return new_torrents
# 使用默认过滤规则再次过滤
return list(filter(lambda t: __filter_torrent(t), torrents))

View File

@@ -1,3 +1,4 @@
import re
from typing import Union, Tuple from typing import Union, Tuple
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -28,6 +29,66 @@ class SiteChain(ChainBase):
self.cookiehelper = CookieHelper() self.cookiehelper = CookieHelper()
self.message = MessageHelper() self.message = MessageHelper()
# 特殊站点登录验证
self.special_site_test = {
"zhuque.in": self.__zhuque_test,
# "m-team.io": self.__mteam_test,
}
@staticmethod
def __zhuque_test(site: Site) -> Tuple[bool, str]:
"""
判断站点是否已经登陆zhuique
"""
# 获取token
token = None
res = RequestUtils(
ua=site.ua,
cookies=site.cookie,
proxies=settings.PROXY if site.proxy else None,
timeout=15
).get_res(url=site.url)
if res and res.status_code == 200:
csrf_token = re.search(r'<meta name="x-csrf-token" content="(.+?)">', res.text)
if csrf_token:
token = csrf_token.group(1)
if not token:
return False, "无法获取Token"
# 调用查询用户信息接口
user_res = RequestUtils(
headers={
'X-CSRF-TOKEN': token,
"Content-Type": "application/json; charset=utf-8",
"User-Agent": f"{site.ua}"
},
cookies=site.cookie,
proxies=settings.PROXY if site.proxy else None,
timeout=15
).get_res(url=f"{site.url}api/user/getInfo")
if user_res and user_res.status_code == 200:
user_info = user_res.json()
if user_info and user_info.get("data"):
return True, "连接成功"
return False, "Cookie已失效"
@staticmethod
def __mteam_test(site: Site) -> Tuple[bool, str]:
"""
判断站点是否已经登陆m-team
"""
url = f"{site.url}api/member/profile"
res = RequestUtils(
ua=site.ua,
cookies=site.cookie,
proxies=settings.PROXY if site.proxy else None,
timeout=15
).post_res(url=url)
if res and res.status_code == 200:
user_info = res.json()
if user_info and user_info.get("data"):
return True, "连接成功"
return False, "Cookie已失效"
def test(self, url: str) -> Tuple[bool, str]: def test(self, url: str) -> Tuple[bool, str]:
""" """
测试站点是否可用 测试站点是否可用
@@ -39,6 +100,12 @@ class SiteChain(ChainBase):
site_info = self.siteoper.get_by_domain(domain) site_info = self.siteoper.get_by_domain(domain)
if not site_info: if not site_info:
return False, f"站点【{url}】不存在" return False, f"站点【{url}】不存在"
# 特殊站点测试
if self.special_site_test.get(domain):
return self.special_site_test[domain](site_info)
# 通用站点测试
site_url = site_info.url site_url = site_info.url
site_cookie = site_info.cookie site_cookie = site_info.cookie
ua = site_info.ua ua = site_info.ua

View File

@@ -132,45 +132,6 @@ class SubscribeChain(ChainBase):
return True return True
return False return False
def remote_refresh(self, channel: MessageChannel, userid: Union[str, int] = None):
"""
远程刷新订阅,发送消息
"""
self.post_message(Notification(channel=channel,
title=f"开始刷新订阅 ...", userid=userid))
self.refresh()
self.post_message(Notification(channel=channel,
title=f"订阅刷新完成!", userid=userid))
def remote_search(self, arg_str: str, channel: MessageChannel, userid: Union[str, int] = None):
"""
远程搜索订阅,发送消息
"""
if arg_str and not str(arg_str).isdigit():
self.post_message(Notification(channel=channel,
title="请输入正确的命令格式:/subscribe_search [id]"
"[id]为订阅编号,不输入订阅编号时搜索所有订阅", userid=userid))
return
if arg_str:
sid = int(arg_str)
subscribe = self.subscribeoper.get(sid)
if not subscribe:
self.post_message(Notification(channel=channel,
title=f"订阅编号 {sid} 不存在!", userid=userid))
return
self.post_message(Notification(channel=channel,
title=f"开始搜索 {subscribe.name} ...", userid=userid))
# 搜索订阅
self.search(sid=int(arg_str))
self.post_message(Notification(channel=channel,
title=f"{subscribe.name} 搜索完成!", userid=userid))
else:
self.post_message(Notification(channel=channel,
title=f"开始搜索所有订阅 ...", userid=userid))
self.search(state='R')
self.post_message(Notification(channel=channel,
title=f"订阅搜索完成!", userid=userid))
def search(self, sid: int = None, state: str = 'N', manual: bool = False): def search(self, sid: int = None, state: str = 'N', manual: bool = False):
""" """
订阅搜索 订阅搜索
@@ -262,16 +223,25 @@ class SubscribeChain(ChainBase):
sites = json.loads(subscribe.sites) sites = json.loads(subscribe.sites)
else: else:
sites = None sites = None
# 过滤规则 # 优先级过滤规则
if subscribe.best_version: if subscribe.best_version:
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules) priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
else: else:
filter_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules) priority_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
# 默认过滤规则
if subscribe.include or subscribe.exclude:
filter_rule = {
"include": subscribe.include,
"exclude": subscribe.exclude
}
else:
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
# 搜索,同时电视剧会过滤掉不需要的剧集 # 搜索,同时电视剧会过滤掉不需要的剧集
contexts = self.searchchain.process(mediainfo=mediainfo, contexts = self.searchchain.process(mediainfo=mediainfo,
keyword=subscribe.keyword, keyword=subscribe.keyword,
no_exists=no_exists, no_exists=no_exists,
sites=sites, sites=sites,
priority_rule=priority_rule,
filter_rule=filter_rule) filter_rule=filter_rule)
if not contexts: if not contexts:
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源') logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
@@ -477,10 +447,10 @@ class SubscribeChain(ChainBase):
} }
else: else:
no_exists = {} no_exists = {}
# 包含与排除规则 # 默认过滤规则
default_include_exclude = self.systemconfig.get(SystemConfigKey.DefaultIncludeExcludeFilter) or {} default_filter = self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {}
include = subscribe.include or default_include_exclude.get("include") include = subscribe.include or default_filter.get("include")
exclude = subscribe.exclude or default_include_exclude.get("exclude") exclude = subscribe.exclude or default_filter.get("exclude")
# 遍历缓存种子 # 遍历缓存种子
_match_context = [] _match_context = []
for domain, contexts in torrents.items(): for domain, contexts in torrents.items():
@@ -493,7 +463,7 @@ class SubscribeChain(ChainBase):
if torrent_mediainfo.tmdb_id != mediainfo.tmdb_id \ if torrent_mediainfo.tmdb_id != mediainfo.tmdb_id \
or torrent_mediainfo.type != mediainfo.type: or torrent_mediainfo.type != mediainfo.type:
continue continue
# 过滤规则 # 优先级过滤规则
if subscribe.best_version: if subscribe.best_version:
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules) filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
else: else:

View File

@@ -1,11 +1,9 @@
import traceback import traceback
from threading import Thread, Event from threading import Thread, Event
from typing import Any, Union from typing import Any, Union, Dict
from app.chain import ChainBase from app.chain import ChainBase
from app.chain.cookiecloud import CookieCloudChain
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.chain.mediaserver import MediaServerChain
from app.chain.site import SiteChain from app.chain.site import SiteChain
from app.chain.subscribe import SubscribeChain from app.chain.subscribe import SubscribeChain
from app.chain.system import SystemChain from app.chain.system import SystemChain
@@ -15,6 +13,8 @@ from app.core.event import eventmanager, EventManager
from app.core.plugin import PluginManager from app.core.plugin import PluginManager
from app.db import SessionFactory from app.db import SessionFactory
from app.log import logger from app.log import logger
from app.scheduler import Scheduler
from app.schemas import Notification
from app.schemas.types import EventType, MessageChannel from app.schemas.types import EventType, MessageChannel
from app.utils.object import ObjectUtils from app.utils.object import ObjectUtils
from app.utils.singleton import Singleton from app.utils.singleton import Singleton
@@ -49,13 +49,15 @@ class Command(metaclass=Singleton):
self.pluginmanager = PluginManager() self.pluginmanager = PluginManager()
# 处理链 # 处理链
self.chain = CommandChian(self._db) self.chain = CommandChian(self._db)
# 定时服务管理
self.scheduler = Scheduler()
# 内置命令 # 内置命令
self._commands = { self._commands = {
"/cookiecloud": { "/cookiecloud": {
"func": CookieCloudChain(self._db).remote_sync, "id": "cookiecloud",
"type": "scheduler",
"description": "同步站点", "description": "同步站点",
"category": "站点", "category": "站点"
"data": {}
}, },
"/sites": { "/sites": {
"func": SiteChain(self._db).remote_list, "func": SiteChain(self._db).remote_list,
@@ -79,10 +81,10 @@ class Command(metaclass=Singleton):
"data": {} "data": {}
}, },
"/mediaserver_sync": { "/mediaserver_sync": {
"func": MediaServerChain(self._db).remote_sync, "id": "mediaserver_sync",
"type": "scheduler",
"description": "同步媒体服务器", "description": "同步媒体服务器",
"category": "管理", "category": "管理"
"data": {}
}, },
"/subscribes": { "/subscribes": {
"func": SubscribeChain(self._db).remote_list, "func": SubscribeChain(self._db).remote_list,
@@ -91,22 +93,27 @@ class Command(metaclass=Singleton):
"data": {} "data": {}
}, },
"/subscribe_refresh": { "/subscribe_refresh": {
"func": SubscribeChain(self._db).remote_refresh, "id": "subscribe_refresh",
"type": "scheduler",
"description": "刷新订阅", "description": "刷新订阅",
"category": "订阅", "category": "订阅"
"data": {}
}, },
"/subscribe_search": { "/subscribe_search": {
"func": SubscribeChain(self._db).remote_search, "id": "subscribe_search",
"type": "scheduler",
"description": "搜索订阅", "description": "搜索订阅",
"category": "订阅", "category": "订阅"
"data": {}
}, },
"/subscribe_delete": { "/subscribe_delete": {
"func": SubscribeChain(self._db).remote_delete, "func": SubscribeChain(self._db).remote_delete,
"description": "删除订阅", "description": "删除订阅",
"data": {} "data": {}
}, },
"/subscribe_tmdb": {
"id": "subscribe_tmdb",
"type": "scheduler",
"description": "订阅元数据更新"
},
"/downloading": { "/downloading": {
"func": DownloadChain(self._db).remote_downloading, "func": DownloadChain(self._db).remote_downloading,
"description": "正在下载", "description": "正在下载",
@@ -114,10 +121,10 @@ class Command(metaclass=Singleton):
"data": {} "data": {}
}, },
"/transfer": { "/transfer": {
"func": TransferChain(self._db).process, "id": "transfer",
"type": "scheduler",
"description": "下载文件整理", "description": "下载文件整理",
"category": "管理", "category": "管理"
"data": {}
}, },
"/redo": { "/redo": {
"func": TransferChain(self._db).remote_transfer, "func": TransferChain(self._db).remote_transfer,
@@ -175,6 +182,56 @@ class Command(metaclass=Singleton):
except Exception as e: except Exception as e:
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}") logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")
def __run_command(self, command: Dict[str, any],
data_str: str = "",
channel: MessageChannel = None, userid: Union[str, int] = None):
"""
运行定时服务
"""
if command.get("type") == "scheduler":
# 定时服务
if userid:
self.chain.post_message(
Notification(
channel=channel,
title=f"开始执行 {command.get('description')} ...",
userid=userid
)
)
# 执行定时任务
self.scheduler.start(job_id=command.get("id"))
if userid:
self.chain.post_message(
Notification(
channel=channel,
title=f"{command.get('description')} 执行完成",
userid=userid
)
)
else:
# 命令
cmd_data = command['data'] if command.get('data') else {}
args_num = ObjectUtils.arguments(command['func'])
if args_num > 0:
if cmd_data:
# 有内置参数直接使用内置参数
data = cmd_data.get("data") or {}
data['channel'] = channel
data['user'] = userid
cmd_data['data'] = data
command['func'](**cmd_data)
elif args_num == 2:
# 没有输入参数只输入渠道和用户ID
command['func'](channel, userid)
elif args_num > 2:
# 多个输入参数用户输入、用户ID
command['func'](data_str, channel, userid)
else:
# 没有参数
command['func']()
def stop(self): def stop(self):
""" """
停止事件处理线程 停止事件处理线程
@@ -216,27 +273,19 @@ class Command(metaclass=Singleton):
command = self.get(cmd) command = self.get(cmd)
if command: if command:
try: try:
logger.info(f"用户 {userid} 开始执行:{command.get('description')} ...") if userid:
cmd_data = command['data'] if command.get('data') else {} logger.info(f"用户 {userid} 开始执行:{command.get('description')} ...")
args_num = ObjectUtils.arguments(command['func'])
if args_num > 0:
if cmd_data:
# 有内置参数直接使用内置参数
data = cmd_data.get("data") or {}
data['channel'] = channel
data['user'] = userid
cmd_data['data'] = data
command['func'](**cmd_data)
elif args_num == 2:
# 没有输入参数只输入渠道和用户ID
command['func'](channel, userid)
elif args_num > 2:
# 多个输入参数用户输入、用户ID
command['func'](data_str, channel, userid)
else: else:
# 没有参数 logger.info(f"开始执行:{command.get('description')} ...")
command['func']()
logger.info(f"用户 {userid} {command.get('description')} 执行完成") # 执行命令
self.__run_command(command, data_str=data_str,
channel=channel, userid=userid)
if userid:
logger.info(f"用户 {userid} {command.get('description')} 执行完成")
else:
logger.info(f"{command.get('description')} 执行完成")
except Exception as err: except Exception as err:
logger.error(f"执行命令 {cmd} 出错:{str(err)}") logger.error(f"执行命令 {cmd} 出错:{str(err)}")
traceback.print_exc() traceback.print_exc()

View File

@@ -76,7 +76,7 @@ class Settings(BaseSettings):
AUTH_SITE: str = "" AUTH_SITE: str = ""
# 交互搜索自动下载用户ID使用,分割 # 交互搜索自动下载用户ID使用,分割
AUTO_DOWNLOAD_USER: str = None AUTO_DOWNLOAD_USER: str = None
# 消息通知渠道 telegram/wechat/slack # 消息通知渠道 telegram/wechat/slack,多个通知渠道用,分隔
MESSAGER: str = "telegram" MESSAGER: str = "telegram"
# WeChat企业ID # WeChat企业ID
WECHAT_CORPID: str = None WECHAT_CORPID: str = None
@@ -142,7 +142,7 @@ class Settings(BaseSettings):
DOWNLOAD_CATEGORY: bool = False DOWNLOAD_CATEGORY: bool = False
# 下载站点字幕 # 下载站点字幕
DOWNLOAD_SUBTITLE: bool = True DOWNLOAD_SUBTITLE: bool = True
# 媒体服务器 emby/jellyfin/plex # 媒体服务器 emby/jellyfin/plex,多个媒体服务器,分割
MEDIASERVER: str = "emby" MEDIASERVER: str = "emby"
# 入库刷新媒体库 # 入库刷新媒体库
REFRESH_MEDIASERVER: bool = True REFRESH_MEDIASERVER: bool = True

View File

@@ -23,14 +23,17 @@ class CookieHelper:
"password": [ "password": [
'//input[@name="password"]', '//input[@name="password"]',
'//input[@id="form_item_password"]', '//input[@id="form_item_password"]',
'//input[@id="password"]' '//input[@id="password"]',
'//input[@type="password"]'
], ],
"captcha": [ "captcha": [
'//input[@name="imagestring"]', '//input[@name="imagestring"]',
'//input[@name="captcha"]', '//input[@name="captcha"]',
'//input[@id="form_item_captcha"]' '//input[@id="form_item_captcha"]',
'//input[@placeholder="驗證碼"]'
], ],
"captcha_img": [ "captcha_img": [
'//img[@alt="captcha"]/@src',
'//img[@alt="CAPTCHA"]/@src', '//img[@alt="CAPTCHA"]/@src',
'//img[@alt="SECURITY CODE"]/@src', '//img[@alt="SECURITY CODE"]/@src',
'//img[@id="LAY-user-get-vercode"]/@src', '//img[@id="LAY-user-get-vercode"]/@src',

View File

@@ -1,4 +1,3 @@
import json
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple, Union, Any, List, Generator from typing import Optional, Tuple, Union, Any, List, Generator
@@ -41,7 +40,7 @@ class EmbyModule(_ModuleBase):
# Emby认证 # Emby认证
return self.emby.authenticate(name, password) return self.emby.authenticate(name, password)
def webhook_parser(self, body: Any, form: Any, args: Any) -> WebhookEventInfo: def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
""" """
解析Webhook报文体 解析Webhook报文体
:param body: 请求体 :param body: 请求体
@@ -49,11 +48,7 @@ class EmbyModule(_ModuleBase):
:param args: 请求参数 :param args: 请求参数
:return: 字典解析为消息时需要包含title、text、image :return: 字典解析为消息时需要包含title、text、image
""" """
if form and form.get("data"): return self.emby.get_webhook_message(form, args)
result = form.get("data")
else:
result = json.dumps(dict(args))
return self.emby.get_webhook_message(result)
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]: def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
""" """
@@ -87,7 +82,7 @@ class EmbyModule(_ModuleBase):
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}") logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
return ExistMediaInfo(type=MediaType.TV, seasons=tvs) return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]: def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
""" """
刷新媒体库 刷新媒体库
:param mediainfo: 识别的媒体信息 :param mediainfo: 识别的媒体信息
@@ -103,25 +98,27 @@ class EmbyModule(_ModuleBase):
target_path=file_path target_path=file_path
) )
] ]
return self.emby.refresh_library_by_items(items) self.emby.refresh_library_by_items(items)
def media_statistic(self) -> schemas.Statistic: def media_statistic(self) -> List[schemas.Statistic]:
""" """
媒体数量统计 媒体数量统计
""" """
media_statistic = self.emby.get_medias_count() media_statistic = self.emby.get_medias_count()
user_count = self.emby.get_user_count() user_count = self.emby.get_user_count()
return schemas.Statistic( return [schemas.Statistic(
movie_count=media_statistic.get("MovieCount") or 0, movie_count=media_statistic.get("MovieCount") or 0,
tv_count=media_statistic.get("SeriesCount") or 0, tv_count=media_statistic.get("SeriesCount") or 0,
episode_count=media_statistic.get("EpisodeCount") or 0, episode_count=media_statistic.get("EpisodeCount") or 0,
user_count=user_count or 0 user_count=user_count or 0
) )]
def mediaserver_librarys(self) -> List[schemas.MediaServerLibrary]: def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
""" """
媒体库列表 媒体库列表
""" """
if server != "emby":
return None
librarys = self.emby.get_librarys() librarys = self.emby.get_librarys()
if not librarys: if not librarys:
return [] return []
@@ -133,10 +130,12 @@ class EmbyModule(_ModuleBase):
path=library.get("path") path=library.get("path")
) for library in librarys] ) for library in librarys]
def mediaserver_items(self, library_id: str) -> Generator: def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
""" """
媒体库项目列表 媒体库项目列表
""" """
if server != "emby":
return None
items = self.emby.get_items(library_id) items = self.emby.get_items(library_id)
for item in items: for item in items:
yield schemas.MediaServerItem( yield schemas.MediaServerItem(
@@ -153,10 +152,13 @@ class EmbyModule(_ModuleBase):
path=item.get("path"), path=item.get("path"),
) )
def mediaserver_tv_episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]: def mediaserver_tv_episodes(self, server: str,
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
""" """
获取剧集信息 获取剧集信息
""" """
if server != "emby":
return None
seasoninfo = self.emby.get_tv_episodes(item_id=item_id) seasoninfo = self.emby.get_tv_episodes(item_id=item_id)
if not seasoninfo: if not seasoninfo:
return [] return []

View File

@@ -545,7 +545,7 @@ class Emby(metaclass=Singleton):
logger.error(f"连接Users/Items出错" + str(e)) logger.error(f"连接Users/Items出错" + str(e))
yield {} yield {}
def get_webhook_message(self, message_str: str) -> WebhookEventInfo: def get_webhook_message(self, form: any, args: dict) -> Optional[WebhookEventInfo]:
""" """
解析Emby Webhook报文 解析Emby Webhook报文
电影: 电影:
@@ -783,9 +783,22 @@ class Emby(metaclass=Singleton):
} }
} }
""" """
message = json.loads(message_str) if not form and not args:
return None
try:
if form and form.get("data"):
result = form.get("data")
else:
result = json.dumps(dict(args))
message = json.loads(result)
except Exception as e:
logger.debug(f"解析emby webhook报文出错" + str(e))
return None
eventType = message.get('Event')
if not eventType:
return None
logger.info(f"接收到emby webhook{message}") logger.info(f"接收到emby webhook{message}")
eventItem = WebhookEventInfo(event=message.get('Event', ''), channel="emby") eventItem = WebhookEventInfo(event=eventType, channel="emby")
if message.get('Item'): if message.get('Item'):
if message.get('Item', {}).get('Type') == 'Episode': if message.get('Item', {}).get('Type') == 'Episode':
eventItem.item_type = "TV" eventItem.item_type = "TV"

View File

@@ -536,16 +536,14 @@ class FileTransferModule(_ModuleBase):
@staticmethod @staticmethod
def get_library_path(path: Path): def get_library_path(path: Path):
""" """
根据目录查询其所在的媒体库目录 根据目录查询其所在的媒体库目录,查询不到的返回输入目录
""" """
if not path: if not path:
return None return None
if not settings.LIBRARY_PATHS: if not settings.LIBRARY_PATHS:
return None return path
# 目的路径,多路径以,分隔 # 目的路径,多路径以,分隔
dest_paths = settings.LIBRARY_PATHS dest_paths = settings.LIBRARY_PATHS
if len(dest_paths) == 1:
return dest_paths[0]
for libpath in dest_paths: for libpath in dest_paths:
try: try:
if path.is_relative_to(libpath): if path.is_relative_to(libpath):
@@ -553,7 +551,7 @@ class FileTransferModule(_ModuleBase):
except Exception as e: except Exception as e:
logger.debug(f"计算媒体库路径时出错:{e}") logger.debug(f"计算媒体库路径时出错:{e}")
continue continue
return None return path
@staticmethod @staticmethod
def get_target_path(in_path: Path = None) -> Optional[Path]: def get_target_path(in_path: Path = None) -> Optional[Path]:

View File

@@ -6,6 +6,7 @@ from ruamel.yaml import CommentedMap
from app.core.context import MediaInfo, TorrentInfo from app.core.context import MediaInfo, TorrentInfo
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.indexer.mtorrent import MTorrentSpider
from app.modules.indexer.spider import TorrentSpider from app.modules.indexer.spider import TorrentSpider
from app.modules.indexer.tnode import TNodeSpider from app.modules.indexer.tnode import TNodeSpider
from app.modules.indexer.torrentleech import TorrentLeech from app.modules.indexer.torrentleech import TorrentLeech
@@ -74,6 +75,12 @@ class IndexerModule(_ModuleBase):
keyword=search_word, keyword=search_word,
page=page page=page
) )
elif site.get('parser') == "mTorrent":
error_flag, result_array = MTorrentSpider(site).search(
keyword=search_word,
mtype=mediainfo.type if mediainfo else None,
page=page
)
else: else:
error_flag, result_array = self.__spider_search( error_flag, result_array = self.__spider_search(
keyword=search_word, keyword=search_word,

View File

@@ -0,0 +1,144 @@
import base64
import json
import re
from typing import Tuple, List
from ruamel.yaml import CommentedMap
from app.core.config import settings
from app.log import logger
from app.schemas import MediaType
from app.utils.http import RequestUtils
from app.utils.string import StringUtils
class MTorrentSpider:
_indexerid = None
_domain = None
_name = ""
_proxy = None
_cookie = None
_ua = None
_size = 100
_searchurl = "%sapi/torrent/search"
_downloadurl = "%sapi/torrent/genDlToken"
_pageurl = "%sdetail/%s"
# 电影分类
_movie_category = ['401', '419', '420', '421', '439', '405', '404']
_tv_category = ['403', '402', '435', '438', '404', '405']
# 标签
_labels = {
0: "",
4: "中字",
6: "国配",
}
def __init__(self, indexer: CommentedMap):
if indexer:
self._indexerid = indexer.get('id')
self._domain = indexer.get('domain')
self._searchurl = self._searchurl % self._domain
self._name = indexer.get('name')
if indexer.get('proxy'):
self._proxy = settings.PROXY
self._cookie = indexer.get('cookie')
self._ua = indexer.get('ua')
def search(self, keyword: str, mtype: MediaType = None, page: int = 0) -> Tuple[bool, List[dict]]:
if not mtype:
categories = []
elif mtype == MediaType.TV:
categories = self._tv_category
else:
categories = self._movie_category
params = {
"keyword": keyword,
"categories": categories,
"pageNumber": int(page) + 1,
"pageSize": self._size,
"visible": 1
}
res = RequestUtils(
headers={
"Content-Type": "application/json",
"User-Agent": f"{self._ua}"
},
cookies=self._cookie,
proxies=self._proxy,
referer=f"{self._domain}browse",
timeout=30
).post_res(url=self._searchurl, json=params)
torrents = []
if res and res.status_code == 200:
results = res.json().get('data', {}).get("data") or []
for result in results:
torrent = {
'title': result.get('name'),
'description': result.get('smallDescr'),
'enclosure': self.__get_download_url(result.get('id')),
'pubdate': StringUtils.format_timestamp(result.get('createdDate')),
'size': result.get('size'),
'seeders': result.get('status', {}).get("seeders"),
'peers': result.get('status', {}).get("leechers"),
'grabs': result.get('status', {}).get("timesCompleted"),
'downloadvolumefactor': self.__get_downloadvolumefactor(result.get('status', {}).get("discount")),
'uploadvolumefactor': self.__get_uploadvolumefactor(result.get('status', {}).get("discount")),
'page_url': self._pageurl % (self._domain, result.get('id')),
'imdbid': self.__find_imdbid(result.get('imdb')),
'labels': [self._labels.get(result.get('labels') or 0)] if result.get('labels') else []
}
torrents.append(torrent)
elif res is not None:
logger.warn(f"{self._name} 搜索失败,错误码:{res.status_code}")
return True, []
else:
logger.warn(f"{self._name} 搜索失败,无法连接 {self._domain}")
return True, []
return False, torrents
@staticmethod
def __find_imdbid(imdb: str) -> str:
if imdb:
m = re.search(r"tt\d+", imdb)
if m:
return m.group(0)
return ""
@staticmethod
def __get_downloadvolumefactor(discount: str) -> float:
discount_dict = {
"FREE": 0,
"PERCENT_50": 0.5,
"PERCENT_70": 0.3,
"_2X_FREE": 0,
"_2X_PERCENT_50": 0.5
}
if discount:
return discount_dict.get(discount, 1)
return 1
@staticmethod
def __get_uploadvolumefactor(discount: str) -> float:
uploadvolumefactor_dict = {
"_2X": 2.0,
"_2X_FREE": 2.0,
"_2X_PERCENT_50": 2.0
}
if discount:
return uploadvolumefactor_dict.get(discount, 1)
return 1
def __get_download_url(self, torrent_id: str) -> str:
url = self._downloadurl % self._domain
params = {
'method': 'post',
'params': {
'id': torrent_id
},
'result': 'data'
}
# base64编码
base64_str = base64.b64encode(json.dumps(params).encode('utf-8')).decode('utf-8')
return f"[{base64_str}]{url}"

View File

@@ -41,7 +41,7 @@ class JellyfinModule(_ModuleBase):
# Jellyfin认证 # Jellyfin认证
return self.jellyfin.authenticate(name, password) return self.jellyfin.authenticate(name, password)
def webhook_parser(self, body: Any, form: Any, args: Any) -> WebhookEventInfo: def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
""" """
解析Webhook报文体 解析Webhook报文体
:param body: 请求体 :param body: 请求体
@@ -49,7 +49,7 @@ class JellyfinModule(_ModuleBase):
:param args: 请求参数 :param args: 请求参数
:return: 字典解析为消息时需要包含title、text、image :return: 字典解析为消息时需要包含title、text、image
""" """
return self.jellyfin.get_webhook_message(json.loads(body)) return self.jellyfin.get_webhook_message(body)
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]: def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
""" """
@@ -83,32 +83,34 @@ class JellyfinModule(_ModuleBase):
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}") logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
return ExistMediaInfo(type=MediaType.TV, seasons=tvs) return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]: def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
""" """
刷新媒体库 刷新媒体库
:param mediainfo: 识别的媒体信息 :param mediainfo: 识别的媒体信息
:param file_path: 文件路径 :param file_path: 文件路径
:return: 成功或失败 :return: 成功或失败
""" """
return self.jellyfin.refresh_root_library() self.jellyfin.refresh_root_library()
def media_statistic(self) -> schemas.Statistic: def media_statistic(self) -> List[schemas.Statistic]:
""" """
媒体数量统计 媒体数量统计
""" """
media_statistic = self.jellyfin.get_medias_count() media_statistic = self.jellyfin.get_medias_count()
user_count = self.jellyfin.get_user_count() user_count = self.jellyfin.get_user_count()
return schemas.Statistic( return [schemas.Statistic(
movie_count=media_statistic.get("MovieCount") or 0, movie_count=media_statistic.get("MovieCount") or 0,
tv_count=media_statistic.get("SeriesCount") or 0, tv_count=media_statistic.get("SeriesCount") or 0,
episode_count=media_statistic.get("EpisodeCount") or 0, episode_count=media_statistic.get("EpisodeCount") or 0,
user_count=user_count or 0 user_count=user_count or 0
) )]
def mediaserver_librarys(self) -> List[schemas.MediaServerLibrary]: def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
""" """
媒体库列表 媒体库列表
""" """
if server != "jellyfin":
return None
librarys = self.jellyfin.get_librarys() librarys = self.jellyfin.get_librarys()
if not librarys: if not librarys:
return [] return []
@@ -120,10 +122,12 @@ class JellyfinModule(_ModuleBase):
path=library.get("path") path=library.get("path")
) for library in librarys] ) for library in librarys]
def mediaserver_items(self, library_id: str) -> Generator: def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
""" """
媒体库项目列表 媒体库项目列表
""" """
if server != "jellyfin":
return None
items = self.jellyfin.get_items(library_id) items = self.jellyfin.get_items(library_id)
for item in items: for item in items:
yield schemas.MediaServerItem( yield schemas.MediaServerItem(
@@ -140,10 +144,13 @@ class JellyfinModule(_ModuleBase):
path=item.get("path"), path=item.get("path"),
) )
def mediaserver_tv_episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]: def mediaserver_tv_episodes(self, server: str,
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
""" """
获取剧集信息 获取剧集信息
""" """
if server != "jellyfin":
return None
seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id) seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id)
if not seasoninfo: if not seasoninfo:
return [] return []

View File

@@ -387,7 +387,7 @@ class Jellyfin(metaclass=Singleton):
logger.error(f"连接Library/Refresh出错" + str(e)) logger.error(f"连接Library/Refresh出错" + str(e))
return False return False
def get_webhook_message(self, message: dict) -> WebhookEventInfo: def get_webhook_message(self, body: any) -> Optional[WebhookEventInfo]:
""" """
解析Jellyfin报文 解析Jellyfin报文
{ {
@@ -450,9 +450,21 @@ class Jellyfin(metaclass=Singleton):
"UserId": "9783d2432b0d40a8a716b6aa46xxxxx" "UserId": "9783d2432b0d40a8a716b6aa46xxxxx"
} }
""" """
if not body:
return None
try:
message = json.loads(body)
except Exception as e:
logger.debug(f"解析Jellyfin Webhook报文出错" + str(e))
return None
if not message:
return None
logger.info(f"接收到jellyfin webhook{message}") logger.info(f"接收到jellyfin webhook{message}")
eventType = message.get('NotificationType')
if not eventType:
return None
eventItem = WebhookEventInfo( eventItem = WebhookEventInfo(
event=message.get('NotificationType', ''), event=eventType,
channel="jellyfin" channel="jellyfin"
) )
eventItem.item_id = message.get('ItemId') eventItem.item_id = message.get('ItemId')

View File

@@ -31,7 +31,7 @@ class PlexModule(_ModuleBase):
if not self.plex.is_inactive(): if not self.plex.is_inactive():
self.plex = Plex() self.plex = Plex()
def webhook_parser(self, body: Any, form: Any, args: Any) -> WebhookEventInfo: def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
""" """
解析Webhook报文体 解析Webhook报文体
:param body: 请求体 :param body: 请求体
@@ -39,7 +39,7 @@ class PlexModule(_ModuleBase):
:param args: 请求参数 :param args: 请求参数
:return: 字典解析为消息时需要包含title、text、image :return: 字典解析为消息时需要包含title、text、image
""" """
return self.plex.get_webhook_message(form.get("payload")) return self.plex.get_webhook_message(form)
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]: def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
""" """
@@ -77,7 +77,7 @@ class PlexModule(_ModuleBase):
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}") logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
return ExistMediaInfo(type=MediaType.TV, seasons=tvs) return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]: def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
""" """
刷新媒体库 刷新媒体库
:param mediainfo: 识别的媒体信息 :param mediainfo: 识别的媒体信息
@@ -93,24 +93,26 @@ class PlexModule(_ModuleBase):
target_path=file_path target_path=file_path
) )
] ]
return self.plex.refresh_library_by_items(items) self.plex.refresh_library_by_items(items)
def media_statistic(self) -> schemas.Statistic: def media_statistic(self) -> List[schemas.Statistic]:
""" """
媒体数量统计 媒体数量统计
""" """
media_statistic = self.plex.get_medias_count() media_statistic = self.plex.get_medias_count()
return schemas.Statistic( return [schemas.Statistic(
movie_count=media_statistic.get("MovieCount") or 0, movie_count=media_statistic.get("MovieCount") or 0,
tv_count=media_statistic.get("SeriesCount") or 0, tv_count=media_statistic.get("SeriesCount") or 0,
episode_count=media_statistic.get("EpisodeCount") or 0, episode_count=media_statistic.get("EpisodeCount") or 0,
user_count=1 user_count=1
) )]
def mediaserver_librarys(self) -> List[schemas.MediaServerLibrary]: def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
""" """
媒体库列表 媒体库列表
""" """
if server != "plex":
return None
librarys = self.plex.get_librarys() librarys = self.plex.get_librarys()
if not librarys: if not librarys:
return [] return []
@@ -122,10 +124,12 @@ class PlexModule(_ModuleBase):
path=library.get("path") path=library.get("path")
) for library in librarys] ) for library in librarys]
def mediaserver_items(self, library_id: str) -> Generator: def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
""" """
媒体库项目列表 媒体库项目列表
""" """
if server != "plex":
return None
items = self.plex.get_items(library_id) items = self.plex.get_items(library_id)
for item in items: for item in items:
yield schemas.MediaServerItem( yield schemas.MediaServerItem(
@@ -142,10 +146,13 @@ class PlexModule(_ModuleBase):
path=item.get("path"), path=item.get("path"),
) )
def mediaserver_tv_episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]: def mediaserver_tv_episodes(self, server: str,
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
""" """
获取剧集信息 获取剧集信息
""" """
if server != "plex":
return None
seasoninfo = self.plex.get_tv_episodes(item_id=item_id) seasoninfo = self.plex.get_tv_episodes(item_id=item_id)
if not seasoninfo: if not seasoninfo:
return [] return []

View File

@@ -353,7 +353,7 @@ class Plex(metaclass=Singleton):
logger.error(f"获取媒体库列表出错:{err}") logger.error(f"获取媒体库列表出错:{err}")
yield {} yield {}
def get_webhook_message(self, message_str: str) -> WebhookEventInfo: def get_webhook_message(self, form: any) -> Optional[WebhookEventInfo]:
""" """
解析Plex报文 解析Plex报文
eventItem 字段的含义 eventItem 字段的含义
@@ -457,9 +457,21 @@ class Plex(metaclass=Singleton):
} }
} }
""" """
message = json.loads(message_str) if not form:
return None
payload = form.get("payload")
if not payload:
return None
try:
message = json.loads(payload)
except Exception as e:
logger.debug(f"解析plex webhook出错{str(e)}")
return None
eventType = message.get('event')
if not eventType:
return None
logger.info(f"接收到plex webhook{message}") logger.info(f"接收到plex webhook{message}")
eventItem = WebhookEventInfo(event=message.get('event', ''), channel="plex") eventItem = WebhookEventInfo(event=eventType, channel="plex")
if message.get('Metadata'): if message.get('Metadata'):
if message.get('Metadata', {}).get('type') == 'episode': if message.get('Metadata', {}).get('type') == 'episode':
eventItem.item_type = "TV" eventItem.item_type = "TV"

View File

@@ -131,130 +131,130 @@ class BestFilmVersion(_PluginBase):
拼装插件配置页面需要返回两块数据1、页面配置2、数据结构 拼装插件配置页面需要返回两块数据1、页面配置2、数据结构
""" """
return [ return [
{ {
'component': 'VForm', 'component': 'VForm',
'content': [ 'content': [
{ {
'component': 'VRow', 'component': 'VRow',
'content': [ 'content': [
{ {
'component': 'VCol', 'component': 'VCol',
'props': { 'props': {
'cols': 12, 'cols': 12,
'md': 3 'md': 3
}, },
'content': [ 'content': [
{ {
'component': 'VSwitch', 'component': 'VSwitch',
'props': { 'props': {
'model': 'enabled', 'model': 'enabled',
'label': '启用插件', 'label': '启用插件',
} }
} }
] ]
}, },
{ {
'component': 'VCol', 'component': 'VCol',
'props': { 'props': {
'cols': 12, 'cols': 12,
'md': 3 'md': 3
}, },
'content': [ 'content': [
{ {
'component': 'VSwitch', 'component': 'VSwitch',
'props': { 'props': {
'model': 'notify', 'model': 'notify',
'label': '发送通知', 'label': '发送通知',
} }
} }
] ]
}, },
{ {
'component': 'VCol', 'component': 'VCol',
'props': { 'props': {
'cols': 12, 'cols': 12,
'md': 3 'md': 3
}, },
'content': [ 'content': [
{ {
'component': 'VSwitch', 'component': 'VSwitch',
'props': { 'props': {
'model': 'only_once', 'model': 'only_once',
'label': '立即运行一次', 'label': '立即运行一次',
} }
} }
] ]
}, },
{ {
'component': 'VCol', 'component': 'VCol',
'props': { 'props': {
'cols': 12, 'cols': 12,
'md': 3 'md': 3
}, },
'content': [ 'content': [
{ {
'component': 'VSwitch', 'component': 'VSwitch',
'props': { 'props': {
'model': 'webhook_enabled', 'model': 'webhook_enabled',
'label': 'Webhook', 'label': 'Webhook',
} }
} }
] ]
} }
] ]
}, },
{ {
'component': 'VRow', 'component': 'VRow',
'content': [ 'content': [
{ {
'component': 'VCol', 'component': 'VCol',
'props': { 'props': {
'cols': 12, 'cols': 12,
}, },
'content': [ 'content': [
{ {
'component': 'VTextField', 'component': 'VTextField',
'props': { 'props': {
'model': 'cron', 'model': 'cron',
'label': '执行周期', 'label': '执行周期',
'placeholder': '5位cron表达式留空自动' 'placeholder': '5位cron表达式留空自动'
} }
} }
] ]
} }
] ]
}, },
{ {
'component': 'VRow', 'component': 'VRow',
'content': [ 'content': [
{ {
'component': 'VCol', 'component': 'VCol',
'props': { 'props': {
'cols': 12, 'cols': 12,
}, },
'content': [ 'content': [
{ {
'component': 'VAlert', 'component': 'VAlert',
'props': { 'props': {
'text': '支持主动定时获取媒体库数据和Webhook实时触发两种方式两者只能选其一' 'text': '支持主动定时获取媒体库数据和Webhook实时触发两种方式两者只能选其一'
'Webhook需要在媒体服务器设置发送Webhook报文。' 'Webhook需要在媒体服务器设置发送Webhook报文。'
'Plex使用主动获取时建议执行周期设置大于1小时' 'Plex使用主动获取时建议执行周期设置大于1小时'
'收藏Api调用Plex官网接口有频率限制。' '收藏Api调用Plex官网接口有频率限制。'
} }
} }
] ]
} }
] ]
} }
] ]
} }
], { ], {
"enabled": False, "enabled": False,
"notify": False, "notify": False,
"cron": "*/30 * * * *", "cron": "*/30 * * * *",
"webhook_enabled": False, "webhook_enabled": False,
"only_once": False "only_once": False
} }
def get_page(self) -> List[dict]: def get_page(self) -> List[dict]:
""" """
@@ -386,81 +386,85 @@ class BestFilmVersion(_PluginBase):
# 读取历史记录 # 读取历史记录
history = self.get_data('history') or [] history = self.get_data('history') or []
all_item = [] # 媒体服务器类型,多个以,分隔
if not settings.MEDIASERVER:
return
media_servers = settings.MEDIASERVER.split(',')
# 读取收藏 # 读取收藏
if settings.MEDIASERVER == 'jellyfin': all_items = {}
self.jellyfin_get_items(all_item) for media_server in media_servers:
elif settings.MEDIASERVER == 'emby': if media_server == 'jellyfin':
self.emby_get_items(all_item) all_items['jellyfin'] = self.jellyfin_get_items()
else: elif media_server == 'emby':
resp = self.plex_get_watchlist() all_items['emby'] = self.emby_get_items()
if not resp: else:
return all_items['plex'] = self.plex_get_watchlist()
all_item.extend(resp)
def function(y, x): def function(y, x):
return y if (x['Name'] in [i['Name'] for i in y]) else (lambda z, u: (z.append(u), z))(y, x)[1] return y if (x['Name'] in [i['Name'] for i in y]) else (lambda z, u: (z.append(u), z))(y, x)[1]
# all_item 根据电影名去重 # 处理所有结果
result = reduce(function, all_item, []) for server, all_item in all_items.items():
# all_item 根据电影名去重
for data in result: result = reduce(function, all_item, [])
# 检查缓存 for data in result:
if data.get('Name') in caches: # 检查缓存
continue if data.get('Name') in caches:
# 获取详情
if settings.MEDIASERVER == 'jellyfin':
item_info_resp = Jellyfin().get_iteminfo(itemid=data.get('Id'))
elif settings.MEDIASERVER == 'emby':
item_info_resp = Emby().get_iteminfo(itemid=data.get('Id'))
else:
item_info_resp = self.plex_get_iteminfo(itemid=data.get('Id'))
logger.info(f'BestFilmVersion插件 item打印 {item_info_resp}')
if not item_info_resp:
continue
# 只接受Movie类型
if data.get('Type') != 'Movie':
continue
# 获取tmdb_id
media_info_ids = item_info_resp.get('ExternalUrls')
if not media_info_ids:
continue
for media_info_id in media_info_ids:
if 'TheMovieDb' != media_info_id.get('Name'):
continue continue
tmdb_find_id = str(media_info_id.get('Url')).split('/')
tmdb_find_id.reverse() # 获取详情
tmdb_id = tmdb_find_id[0] if server == 'jellyfin':
# 识别媒体信息 item_info_resp = Jellyfin().get_iteminfo(itemid=data.get('Id'))
mediainfo: MediaInfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE) elif server == 'emby':
if not mediainfo: item_info_resp = Emby().get_iteminfo(itemid=data.get('Id'))
logger.warn(f'未识别到媒体信息,标题:{data.get("Name")}tmdbID{tmdb_id}') else:
item_info_resp = self.plex_get_iteminfo(itemid=data.get('Id'))
logger.info(f'BestFilmVersion插件 item打印 {item_info_resp}')
if not item_info_resp:
continue continue
# 添加订阅
self.subscribechain.add(mtype=MediaType.MOVIE, # 只接受Movie类型
title=mediainfo.title, if data.get('Type') != 'Movie':
year=mediainfo.year, continue
tmdbid=mediainfo.tmdb_id,
best_version=True, # 获取tmdb_id
username="收藏洗版", media_info_ids = item_info_resp.get('ExternalUrls')
exist_ok=True) if not media_info_ids:
# 加入缓存 continue
caches.append(data.get('Name')) for media_info_id in media_info_ids:
# 存储历史记录 if 'TheMovieDb' != media_info_id.get('Name'):
if mediainfo.tmdb_id not in [h.get("tmdbid") for h in history]: continue
history.append({ tmdb_find_id = str(media_info_id.get('Url')).split('/')
"title": mediainfo.title, tmdb_find_id.reverse()
"type": mediainfo.type.value, tmdb_id = tmdb_find_id[0]
"year": mediainfo.year, # 识别媒体信息
"poster": mediainfo.get_poster_image(), mediainfo: MediaInfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE)
"overview": mediainfo.overview, if not mediainfo:
"tmdbid": mediainfo.tmdb_id, logger.warn(f'未识别到媒体信息,标题:{data.get("Name")}tmdbID{tmdb_id}')
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") continue
}) # 添加订阅
self.subscribechain.add(mtype=MediaType.MOVIE,
title=mediainfo.title,
year=mediainfo.year,
tmdbid=mediainfo.tmdb_id,
best_version=True,
username="收藏洗版",
exist_ok=True)
# 加入缓存
caches.append(data.get('Name'))
# 存储历史记录
if mediainfo.tmdb_id not in [h.get("tmdbid") for h in history]:
history.append({
"title": mediainfo.title,
"type": mediainfo.type.value,
"year": mediainfo.year,
"poster": mediainfo.get_poster_image(),
"overview": mediainfo.overview,
"tmdbid": mediainfo.tmdb_id,
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
# 保存历史记录 # 保存历史记录
self.save_data('history', history) self.save_data('history', history)
# 保存缓存 # 保存缓存
@@ -468,13 +472,14 @@ class BestFilmVersion(_PluginBase):
finally: finally:
lock.release() lock.release()
def jellyfin_get_items(self, all_item): def jellyfin_get_items(self) -> List[dict]:
# 获取所有user # 获取所有user
users_url = "{HOST}Users?&apikey={APIKEY}" users_url = "{HOST}Users?&apikey={APIKEY}"
users = self.get_users(Jellyfin().get_data(users_url)) users = self.get_users(Jellyfin().get_data(users_url))
if not users: if not users:
logger.info(f"bestfilmversion/users_url: {users_url}") logger.info(f"bestfilmversion/users_url: {users_url}")
return return []
all_items = []
for user in users: for user in users:
# 根据加入日期 降序排序 # 根据加入日期 降序排序
url = "{HOST}Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \ url = "{HOST}Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \
@@ -490,14 +495,16 @@ class BestFilmVersion(_PluginBase):
resp = self.get_items(Jellyfin().get_data(url)) resp = self.get_items(Jellyfin().get_data(url))
if not resp: if not resp:
continue continue
all_item.extend(resp) all_items.extend(resp)
return all_items
def emby_get_items(self, all_item): def emby_get_items(self) -> List[dict]:
# 获取所有user # 获取所有user
get_users_url = "{HOST}Users?&api_key={APIKEY}" get_users_url = "{HOST}Users?&api_key={APIKEY}"
users = self.get_users(Emby().get_data(get_users_url)) users = self.get_users(Emby().get_data(get_users_url))
if not users: if not users:
return return []
all_items = []
for user in users: for user in users:
# 根据加入日期 降序排序 # 根据加入日期 降序排序
url = "{HOST}emby/Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \ url = "{HOST}emby/Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \
@@ -512,7 +519,8 @@ class BestFilmVersion(_PluginBase):
resp = self.get_items(Emby().get_data(url)) resp = self.get_items(Emby().get_data(url))
if not resp: if not resp:
continue continue
all_item.extend(resp) all_items.extend(resp)
return all_items
@staticmethod @staticmethod
def get_items(resp: Response): def get_items(resp: Response):
@@ -538,7 +546,7 @@ class BestFilmVersion(_PluginBase):
return [] return []
@staticmethod @staticmethod
def plex_get_watchlist(): def plex_get_watchlist() -> List[dict]:
# 根据加入日期 降序排序 # 根据加入日期 降序排序
url = f"https://metadata.provider.plex.tv/library/sections/watchlist/all?type=1&sort=addedAt%3Adesc" \ url = f"https://metadata.provider.plex.tv/library/sections/watchlist/all?type=1&sort=addedAt%3Adesc" \
f"&X-Plex-Container-Start=0&X-Plex-Container-Size=50" \ f"&X-Plex-Container-Start=0&X-Plex-Container-Size=50" \

View File

@@ -598,7 +598,7 @@ class DirMonitor(_PluginBase):
'rows': 5, 'rows': 5,
'placeholder': '每一行一个目录,支持两种配置方式:\n' 'placeholder': '每一行一个目录,支持两种配置方式:\n'
'监控目录\n' '监控目录\n'
'监控目录:转移目的目录' '监控目录:转移目的目录(需同时在媒体库目录中配置该目的目录)'
} }
} }
] ]

View File

@@ -718,19 +718,21 @@ class MediaSyncDel(_PluginBase):
""" """
# 读取历史记录 # 读取历史记录
history = self.get_data('history') or [] history = self.get_data('history') or []
# 媒体服务器类型
media_server = settings.MEDIASERVER
last_time = self.get_data("last_time") last_time = self.get_data("last_time")
del_medias = [] del_medias = []
if media_server == 'emby':
del_medias = self.parse_emby_log(last_time) # 媒体服务器类型,多个以,分隔
elif media_server == 'jellyfin': if not settings.MEDIASERVER:
del_medias = self.parse_jellyfin_log(last_time)
elif media_server == 'plex':
# TODO plex解析日志
return return
media_servers = settings.MEDIASERVER.split(',')
for media_server in media_servers:
if media_server == 'emby':
del_medias.extend(self.parse_emby_log(last_time))
elif media_server == 'jellyfin':
del_medias.extend(self.parse_jellyfin_log(last_time))
elif media_server == 'plex':
# TODO plex解析日志
return
if not del_medias: if not del_medias:
logger.error("未解析到已删除媒体信息") logger.error("未解析到已删除媒体信息")

View File

@@ -383,78 +383,86 @@ class SpeedLimiter(_PluginBase):
return return
# 当前播放的总比特率 # 当前播放的总比特率
total_bit_rate = 0 total_bit_rate = 0
# 查询播放中会话 # 媒体服务器类型,多个以,分隔
playing_sessions = [] if not settings.MEDIASERVER:
if settings.MEDIASERVER == "emby": return
req_url = "{HOST}emby/Sessions?api_key={APIKEY}" media_servers = settings.MEDIASERVER.split(',')
try: # 查询所有媒体服务器状态
res = Emby().get_data(req_url) for media_server in media_servers:
if res and res.status_code == 200: # 查询播放中会话
sessions = res.json() playing_sessions = []
for session in sessions: if media_server == "emby":
if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"): req_url = "{HOST}emby/Sessions?api_key={APIKEY}"
playing_sessions.append(session) try:
except Exception as e: res = Emby().get_data(req_url)
logger.error(f"获取Emby播放会话失败{str(e)}") if res and res.status_code == 200:
# 计算有效比特率 sessions = res.json()
for session in playing_sessions: for session in sessions:
# 设置了不限速范围则判断session ip是否在不限速范围内 if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"):
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]: playing_sessions.append(session)
if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \ except Exception as e:
and session.get("NowPlayingItem", {}).get("MediaType") == "Video": logger.error(f"获取Emby播放会话失败{str(e)}")
total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0) continue
# 未设置不限速范围则默认不限速内网ip
elif not IpUtils.is_private_ip(session.get("RemoteEndPoint")) \
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0)
elif settings.MEDIASERVER == "jellyfin":
req_url = "{HOST}Sessions?api_key={APIKEY}"
try:
res = Jellyfin().get_data(req_url)
if res and res.status_code == 200:
sessions = res.json()
for session in sessions:
if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"):
playing_sessions.append(session)
except Exception as e:
logger.error(f"获取Jellyfin播放会话失败{str(e)}")
# 计算有效比特率
for session in playing_sessions:
# 设置了不限速范围则判断session ip是否在不限速范围内
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
media_streams = session.get("NowPlayingItem", {}).get("MediaStreams") or []
for media_stream in media_streams:
total_bit_rate += int(media_stream.get("BitRate") or 0)
# 未设置不限速范围则默认不限速内网ip
elif not IpUtils.is_private_ip(session.get("RemoteEndPoint")) \
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
media_streams = session.get("NowPlayingItem", {}).get("MediaStreams") or []
for media_stream in media_streams:
total_bit_rate += int(media_stream.get("BitRate") or 0)
elif settings.MEDIASERVER == "plex":
_plex = Plex().get_plex()
if _plex:
sessions = _plex.sessions()
for session in sessions:
bitrate = sum([m.bitrate or 0 for m in session.media])
playing_sessions.append({
"type": session.TAG,
"bitrate": bitrate,
"address": session.player.address
})
# 计算有效比特率 # 计算有效比特率
for session in playing_sessions: for session in playing_sessions:
# 设置了不限速范围则判断session ip是否在不限速范围内 # 设置了不限速范围则判断session ip是否在不限速范围内
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]: if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
if not self.__allow_access(self._unlimited_ips, session.get("address")) \ if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0)
# 未设置不限速范围则默认不限速内网ip
elif not IpUtils.is_private_ip(session.get("RemoteEndPoint")) \
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0)
elif media_server == "jellyfin":
req_url = "{HOST}Sessions?api_key={APIKEY}"
try:
res = Jellyfin().get_data(req_url)
if res and res.status_code == 200:
sessions = res.json()
for session in sessions:
if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"):
playing_sessions.append(session)
except Exception as e:
logger.error(f"获取Jellyfin播放会话失败{str(e)}")
continue
# 计算有效比特率
for session in playing_sessions:
# 设置了不限速范围则判断session ip是否在不限速范围内
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
media_streams = session.get("NowPlayingItem", {}).get("MediaStreams") or []
for media_stream in media_streams:
total_bit_rate += int(media_stream.get("BitRate") or 0)
# 未设置不限速范围则默认不限速内网ip
elif not IpUtils.is_private_ip(session.get("RemoteEndPoint")) \
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
media_streams = session.get("NowPlayingItem", {}).get("MediaStreams") or []
for media_stream in media_streams:
total_bit_rate += int(media_stream.get("BitRate") or 0)
elif media_server == "plex":
_plex = Plex().get_plex()
if _plex:
sessions = _plex.sessions()
for session in sessions:
bitrate = sum([m.bitrate or 0 for m in session.media])
playing_sessions.append({
"type": session.TAG,
"bitrate": bitrate,
"address": session.player.address
})
# 计算有效比特率
for session in playing_sessions:
# 设置了不限速范围则判断session ip是否在不限速范围内
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
if not self.__allow_access(self._unlimited_ips, session.get("address")) \
and session.get("type") == "Video":
total_bit_rate += int(session.get("bitrate") or 0)
# 未设置不限速范围则默认不限速内网ip
elif not IpUtils.is_private_ip(session.get("address")) \
and session.get("type") == "Video": and session.get("type") == "Video":
total_bit_rate += int(session.get("bitrate") or 0) total_bit_rate += int(session.get("bitrate") or 0)
# 未设置不限速范围则默认不限速内网ip
elif not IpUtils.is_private_ip(session.get("address")) \
and session.get("type") == "Video":
total_bit_rate += int(session.get("bitrate") or 0)
if total_bit_rate: if total_bit_rate:
# 开启智能限速计算上传限速 # 开启智能限速计算上传限速

View File

@@ -644,7 +644,7 @@ class TorrentRemover(_PluginBase):
return None return None
if self._torrentstates and torrent.state not in self._torrentstates: if self._torrentstates and torrent.state not in self._torrentstates:
return None return None
if self._torrentcategorys and torrent.category not in self._torrentcategorys: if self._torrentcategorys and (not torrent.category or torrent.category not in self._torrentcategorys):
return None return None
return { return {
"id": torrent.hash, "id": torrent.hash,

View File

@@ -1,10 +1,12 @@
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import List
import pytz import pytz
from apscheduler.executors.pool import ThreadPoolExecutor from apscheduler.executors.pool import ThreadPoolExecutor
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from app import schemas
from app.chain import ChainBase from app.chain import ChainBase
from app.chain.cookiecloud import CookieCloudChain from app.chain.cookiecloud import CookieCloudChain
from app.chain.mediaserver import MediaServerChain from app.chain.mediaserver import MediaServerChain
@@ -40,57 +42,153 @@ class Scheduler(metaclass=Singleton):
def __init__(self): def __init__(self):
# 数据库连接 # 数据库连接
self._db = SessionFactory() self._db = SessionFactory()
# 各服务的运行状态
self._jobs = {
"cookiecloud": {
"func": CookieCloudChain(self._db).process,
"running": False,
},
"mediaserver_sync": {
"func": MediaServerChain(self._db).sync,
"running": False,
},
"subscribe_tmdb": {
"func": SubscribeChain(self._db).check,
"running": False,
},
"subscribe_search": {
"func": SubscribeChain(self._db).search,
"running": False,
},
"subscribe_refresh": {
"func": SubscribeChain(self._db).refresh,
"running": False,
},
"transfer": {
"func": TransferChain(self._db).process,
"running": False,
}
}
# 调试模式不启动定时服务 # 调试模式不启动定时服务
if settings.DEV: if settings.DEV:
return return
# CookieCloud定时同步 # CookieCloud定时同步
if settings.COOKIECLOUD_INTERVAL: if settings.COOKIECLOUD_INTERVAL:
self._scheduler.add_job(CookieCloudChain(self._db).process, self._scheduler.add_job(
"interval", self.start,
minutes=settings.COOKIECLOUD_INTERVAL, "interval",
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=1), id="cookiecloud",
name="同步CookieCloud站点") name="同步CookieCloud站点",
minutes=settings.COOKIECLOUD_INTERVAL,
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=1),
kwargs={
'job_id': 'cookiecloud'
}
)
# 媒体服务器同步 # 媒体服务器同步
if settings.MEDIASERVER_SYNC_INTERVAL: if settings.MEDIASERVER_SYNC_INTERVAL:
self._scheduler.add_job(MediaServerChain(self._db).sync, "interval", self._scheduler.add_job(
hours=settings.MEDIASERVER_SYNC_INTERVAL, self.start,
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=5), "interval",
name="同步媒体服务器") id="mediaserver_sync",
name="同步媒体服务器",
hours=settings.MEDIASERVER_SYNC_INTERVAL,
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=5),
kwargs={
'job_id': 'mediaserver_sync'
}
)
# 新增订阅时搜索5分钟检查一次 # 新增订阅时搜索5分钟检查一次
self._scheduler.add_job(SubscribeChain(self._db).search, "interval", self._scheduler.add_job(
minutes=5, kwargs={'state': 'N'}) self.start,
"interval",
minutes=5,
kwargs={
'job_id': 'subscribe_search',
'state': 'N'
}
)
# 检查更新订阅TMDB数据每隔6小时 # 检查更新订阅TMDB数据每隔6小时
self._scheduler.add_job(SubscribeChain(self._db).check, "interval", hours=6) self._scheduler.add_job(
self.start,
"interval",
id="subscribe_tmdb",
name="订阅元数据更新",
hours=6,
kwargs={
'job_id': 'subscribe_tmdb'
}
)
# 订阅状态每隔24小时搜索一次 # 订阅状态每隔24小时搜索一次
if settings.SUBSCRIBE_SEARCH: if settings.SUBSCRIBE_SEARCH:
self._scheduler.add_job(SubscribeChain(self._db).search, "interval", self._scheduler.add_job(
hours=24, kwargs={'state': 'R'}, name="订阅搜索") self.start,
"interval",
id="subscribe_search",
name="订阅搜索",
hours=24,
kwargs={
'job_id': 'subscribe_search',
'state': 'R'
}
)
if settings.SUBSCRIBE_MODE == "spider": if settings.SUBSCRIBE_MODE == "spider":
# 站点首页种子定时刷新模式 # 站点首页种子定时刷新模式
triggers = TimerUtils.random_scheduler(num_executions=30) triggers = TimerUtils.random_scheduler(num_executions=30)
for trigger in triggers: for trigger in triggers:
self._scheduler.add_job(SubscribeChain(self._db).refresh, "cron", self._scheduler.add_job(
hour=trigger.hour, minute=trigger.minute, name="订阅刷新") self.start,
"cron",
id=f"subscribe_refresh|{trigger.hour}:{trigger.minute}",
name="订阅刷新",
hour=trigger.hour,
minute=trigger.minute,
kwargs={
'job_id': 'subscribe_refresh'
})
else: else:
# RSS订阅模式 # RSS订阅模式
if not settings.SUBSCRIBE_RSS_INTERVAL: if not settings.SUBSCRIBE_RSS_INTERVAL:
settings.SUBSCRIBE_RSS_INTERVAL = 30 settings.SUBSCRIBE_RSS_INTERVAL = 30
elif settings.SUBSCRIBE_RSS_INTERVAL < 5: elif settings.SUBSCRIBE_RSS_INTERVAL < 5:
settings.SUBSCRIBE_RSS_INTERVAL = 5 settings.SUBSCRIBE_RSS_INTERVAL = 5
self._scheduler.add_job(SubscribeChain(self._db).refresh, "interval", self._scheduler.add_job(
minutes=settings.SUBSCRIBE_RSS_INTERVAL, name="订阅刷新") self.start,
"interval",
id="subscribe_refresh",
name="RSS订阅刷新",
minutes=settings.SUBSCRIBE_RSS_INTERVAL,
kwargs={
'job_id': 'subscribe_refresh'
}
)
# 下载器文件转移每5分钟 # 下载器文件转移每5分钟
if settings.DOWNLOADER_MONITOR: if settings.DOWNLOADER_MONITOR:
self._scheduler.add_job(TransferChain(self._db).process, "interval", minutes=5, name="下载文件整理") self._scheduler.add_job(
self.start,
"interval",
id="transfer",
name="下载文件整理",
minutes=5,
kwargs={
'job_id': 'transfer'
}
)
# 公共定时服务 # 公共定时服务
self._scheduler.add_job(SchedulerChain(self._db).scheduler_job, "interval", minutes=10) self._scheduler.add_job(
SchedulerChain(self._db).scheduler_job,
"interval",
minutes=10
)
# 打印服务 # 打印服务
logger.debug(self._scheduler.print_jobs()) logger.debug(self._scheduler.print_jobs())
@@ -98,11 +196,54 @@ class Scheduler(metaclass=Singleton):
# 启动定时服务 # 启动定时服务
self._scheduler.start() self._scheduler.start()
def list(self): def start(self, job_id: str, *args, **kwargs):
"""
启动定时服务
"""
# 处理job_id格式
job = self._jobs.get(job_id)
if not job:
return
if job.get("running"):
logger.warning(f"定时任务 {job_id} 正在运行 ...")
return
self._jobs[job_id]["running"] = True
try:
job["func"](*args, **kwargs)
except Exception as e:
logger.error(f"定时任务 {job_id} 执行失败:{e}")
self._jobs[job_id]["running"] = False
def list(self) -> List[schemas.ScheduleInfo]:
""" """
当前所有任务 当前所有任务
""" """
return self._scheduler.get_jobs() # 返回计时任务
schedulers = []
# 去重
added = []
jobs = self._scheduler.get_jobs()
# 按照下次运行时间排序
jobs.sort(key=lambda x: x.next_run_time)
for job in jobs:
if job.name not in added:
added.append(job.name)
else:
continue
job_id = job.id.split("|")[0]
if not self._jobs.get(job_id):
continue
# 任务状态
status = "正在运行" if self._jobs[job_id].get("running") else "等待"
# 下次运行时间
next_run = TimerUtils.time_difference(job.next_run_time)
schedulers.append(schemas.ScheduleInfo(
id=job_id,
name=job.name,
status=status,
next_run=next_run
))
return schedulers
def stop(self): def stop(self):
""" """

View File

@@ -66,8 +66,8 @@ class SystemConfigKey(Enum):
SubscribeFilterRules = "SubscribeFilterRules" SubscribeFilterRules = "SubscribeFilterRules"
# 洗版规则 # 洗版规则
BestVersionFilterRules = "BestVersionFilterRules" BestVersionFilterRules = "BestVersionFilterRules"
# 默认包含与排除规则 # 默认过滤规则
DefaultIncludeExcludeFilter = "DefaultIncludeExcludeFilter" DefaultFilterRules = "DefaultFilterRules"
# 转移屏蔽词 # 转移屏蔽词
TransferExcludeWords = "TransferExcludeWords" TransferExcludeWords = "TransferExcludeWords"

View File

@@ -17,19 +17,18 @@ class SiteUtils:
if html.xpath("//input[@type='password']"): if html.xpath("//input[@type='password']"):
return False return False
# 是否存在登出和用户面板等链接 # 是否存在登出和用户面板等链接
xpaths = ['//a[contains(@href, "logout")' xpaths = [
' or contains(@data-url, "logout")' '//a[contains(@href, "logout")'
' or contains(@href, "mybonus") ' ' or contains(@data-url, "logout")'
' or contains(@onclick, "logout")' ' or contains(@href, "mybonus") '
' or contains(@href, "usercp")]', ' or contains(@onclick, "logout")'
'//form[contains(@action, "logout")]'] ' or contains(@href, "usercp")]',
'//form[contains(@action, "logout")]',
'//div[@class="user-info-side"]'
]
for xpath in xpaths: for xpath in xpaths:
if html.xpath(xpath): if html.xpath(xpath):
return True return True
user_info_div = html.xpath('//div[@class="user-info-side"]')
if user_info_div:
return True
return False return False
@classmethod @classmethod

View File

@@ -343,9 +343,9 @@ class SystemUtils:
# 获取当前容器的 ID # 获取当前容器的 ID
with open('/proc/self/mountinfo', 'r') as f: with open('/proc/self/mountinfo', 'r') as f:
data = f.read() data = f.read()
index_resolv_conf = data.find("resolv.conf") index_resolv_conf = data.find("/sys/fs/cgroup/devices")
if index_resolv_conf != -1: if index_resolv_conf != -1:
index_second_slash = data.rfind("/", 0, index_resolv_conf) index_second_slash = data.rfind(" ", 0, index_resolv_conf)
index_first_slash = data.rfind("/", 0, index_second_slash) + 1 index_first_slash = data.rfind("/", 0, index_second_slash) + 1
container_id = data[index_first_slash:index_second_slash] container_id = data[index_first_slash:index_second_slash]
if not container_id: if not container_id:

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
APP_VERSION = 'v1.2.4' APP_VERSION = 'v1.2.5'