mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-09 07:32:41 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da4ff99570 | ||
|
|
b3c0dc813b | ||
|
|
a7b51d9fcc | ||
|
|
76f1de42a8 | ||
|
|
bad016b2b4 | ||
|
|
5cd48d5447 | ||
|
|
41ff5363ea | ||
|
|
85014f4acb | ||
|
|
d9a68daddd | ||
|
|
141e78f274 | ||
|
|
de98ccd33c | ||
|
|
d490dadfdd | ||
|
|
f46bbf73ba | ||
|
|
17eba86f7a | ||
|
|
fdf25b8c66 | ||
|
|
516cb443b9 | ||
|
|
7c4c3b3f9a | ||
|
|
e298a1a8a0 | ||
|
|
fd9eef2089 | ||
|
|
78dab04c96 | ||
|
|
c34475653f | ||
|
|
eb6a6eee0a |
@@ -132,7 +132,7 @@ docker pull jxxghp/moviepilot:latest
|
||||
|
||||
- **DOWNLOADER_MONITOR:** 下载器监控,`true`/`false`,默认为`true`,开启后下载完成时才会自动整理入库
|
||||
|
||||
- **MEDIASERVER:** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
|
||||
- **MEDIASERVER:** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时开启多个使用`,`分隔。还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
|
||||
|
||||
- `emby`设置项:
|
||||
|
||||
|
||||
@@ -27,4 +27,4 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
pass
|
||||
|
||||
29
alembic/versions/30329639c12b_1_0_7.py
Normal file
29
alembic/versions/30329639c12b_1_0_7.py
Normal 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
|
||||
@@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, List
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from requests import Session
|
||||
@@ -11,9 +11,7 @@ from app.core.security import verify_token
|
||||
from app.db import get_db
|
||||
from app.db.models.transferhistory import TransferHistory
|
||||
from app.scheduler import Scheduler
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.system import SystemUtils
|
||||
from app.utils.timer import TimerUtils
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -24,14 +22,16 @@ def statistic(db: Session = Depends(get_db),
|
||||
"""
|
||||
查询媒体数量统计信息
|
||||
"""
|
||||
media_statistic = DashboardChain(db).media_statistic()
|
||||
if media_statistic:
|
||||
return schemas.Statistic(
|
||||
movie_count=media_statistic.movie_count,
|
||||
tv_count=media_statistic.tv_count,
|
||||
episode_count=media_statistic.episode_count,
|
||||
user_count=media_statistic.user_count
|
||||
)
|
||||
media_statistics: Optional[List[schemas.Statistic]] = DashboardChain(db).media_statistic()
|
||||
if media_statistics:
|
||||
# 汇总各媒体库统计信息
|
||||
ret_statistic = schemas.Statistic()
|
||||
for media_statistic in media_statistics:
|
||||
ret_statistic.movie_count += media_statistic.movie_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:
|
||||
return schemas.Statistic()
|
||||
|
||||
@@ -81,37 +81,7 @@ def schedule(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
查询后台服务信息
|
||||
"""
|
||||
# 返回计时任务
|
||||
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
|
||||
return Scheduler().list()
|
||||
|
||||
|
||||
@router.get("/transfer", summary="文件整理统计", response_model=List[int])
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from typing import Any, List
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import schemas
|
||||
from app.core.plugin import PluginManager
|
||||
from app.core.security import verify_token
|
||||
from app.db import get_db
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.schemas.types import SystemConfigKey
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from sqlalchemy.orm import Session
|
||||
from starlette.background import BackgroundTasks
|
||||
|
||||
from app import schemas
|
||||
from app.chain.cookiecloud import CookieCloudChain
|
||||
from app.chain.site import SiteChain
|
||||
from app.chain.torrents import TorrentsChain
|
||||
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.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import SystemConfigKey, EventType
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def start_cookiecloud_sync(db: Session):
|
||||
"""
|
||||
后台启动CookieCloud站点同步
|
||||
"""
|
||||
CookieCloudChain(db).process(manual=True)
|
||||
|
||||
|
||||
@router.get("/", summary="所有站点", response_model=List[schemas.Site])
|
||||
def read_sites(db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> List[dict]:
|
||||
@@ -101,12 +94,11 @@ def delete_site(
|
||||
|
||||
@router.get("/cookiecloud", summary="CookieCloud同步", response_model=schemas.Response)
|
||||
def cookie_cloud_sync(background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
运行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同步任务已启动!")
|
||||
|
||||
|
||||
@@ -119,7 +111,8 @@ def cookie_cloud_sync(db: Session = Depends(get_db),
|
||||
Site.reset(db)
|
||||
SystemConfigOper().set(SystemConfigKey.IndexerSites, [])
|
||||
SystemConfigOper().set(SystemConfigKey.RssSites, [])
|
||||
CookieCloudChain().process(manual=True)
|
||||
# 启动定时服务
|
||||
Scheduler().start("cookiecloud", manual=True)
|
||||
# 插件站点删除
|
||||
EventManager().send_event(EventType.SiteDeleted,
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from typing import List, Any, Optional
|
||||
from typing import List, Any
|
||||
|
||||
from fastapi import APIRouter, Request, BackgroundTasks, Depends, HTTPException, Header
|
||||
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.user import User
|
||||
from app.db.userauth import get_current_active_user
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
router = APIRouter()
|
||||
@@ -26,13 +27,6 @@ def start_subscribe_add(db: Session, title: str, year: str,
|
||||
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])
|
||||
def read_subscribes(
|
||||
db: Session = Depends(get_db),
|
||||
@@ -140,35 +134,36 @@ def subscribe_mediaid(
|
||||
|
||||
@router.get("/refresh", summary="刷新订阅", response_model=schemas.Response)
|
||||
def refresh_subscribes(
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
刷新所有订阅
|
||||
"""
|
||||
SubscribeChain(db).refresh()
|
||||
Scheduler().start("subscribe_refresh")
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/check", summary="刷新订阅 TMDB 信息", response_model=schemas.Response)
|
||||
def check_subscribes(
|
||||
db: Session = Depends(get_db),
|
||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
刷新所有订阅
|
||||
刷新订阅 TMDB 信息
|
||||
"""
|
||||
SubscribeChain(db).check()
|
||||
Scheduler().start("subscribe_tmdb")
|
||||
return schemas.Response(success=True)
|
||||
|
||||
|
||||
@router.get("/search", summary="搜索所有订阅", response_model=schemas.Response)
|
||||
def search_subscribes(
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
_: 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)
|
||||
|
||||
|
||||
@@ -176,12 +171,15 @@ def search_subscribes(
|
||||
def search_subscribe(
|
||||
subscribe_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
_: 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)
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import json
|
||||
import time
|
||||
import tailer
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
import tailer
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -16,6 +16,7 @@ from app.db import get_db
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.message import MessageHelper
|
||||
from app.helper.progress import ProgressHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.system import SystemUtils
|
||||
@@ -211,3 +212,15 @@ def restart_system(_: schemas.TokenPayload = Depends(verify_token)):
|
||||
# 执行重启
|
||||
ret, msg = SystemUtils.restart()
|
||||
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)
|
||||
|
||||
@@ -684,7 +684,7 @@ def arr_serie(apikey: str, tid: int, db: Session = Depends(get_db)) -> Any:
|
||||
"monitored": True,
|
||||
}],
|
||||
year=subscribe.year,
|
||||
remotePoster=subscribe.image,
|
||||
remotePoster=subscribe.poster,
|
||||
tmdbId=subscribe.tmdbid,
|
||||
tvdbId=subscribe.tvdbid,
|
||||
imdbId=subscribe.imdbid,
|
||||
|
||||
@@ -336,14 +336,14 @@ class ChainBase(metaclass=ABCMeta):
|
||||
"""
|
||||
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 file_path: 文件路径
|
||||
: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:
|
||||
"""
|
||||
@@ -391,22 +391,22 @@ class ChainBase(metaclass=ABCMeta):
|
||||
:param mediainfo: 识别的媒体信息
|
||||
: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:
|
||||
"""
|
||||
注册菜单命令
|
||||
"""
|
||||
return self.run_module("register_commands", commands=commands)
|
||||
self.run_module("register_commands", commands=commands)
|
||||
|
||||
def scheduler_job(self) -> None:
|
||||
"""
|
||||
定时任务,每10分钟调用一次,模块实现该接口以实现定时服务
|
||||
"""
|
||||
return self.run_module("scheduler_job")
|
||||
self.run_module("scheduler_job")
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""
|
||||
清理缓存,模块实现该接口响应清理缓存事件
|
||||
"""
|
||||
return self.run_module("clear_cache")
|
||||
self.run_module("clear_cache")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import base64
|
||||
from typing import Tuple, Optional, Union
|
||||
from typing import Tuple, Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from lxml import etree
|
||||
@@ -16,7 +16,6 @@ from app.helper.message import MessageHelper
|
||||
from app.helper.rss import RssHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.log import logger
|
||||
from app.schemas import Notification, NotificationType, MessageChannel
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.site import SiteUtils
|
||||
|
||||
@@ -40,21 +39,6 @@ class CookieCloudChain(ChainBase):
|
||||
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]:
|
||||
"""
|
||||
通过CookieCloud同步站点Cookie
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Optional, List
|
||||
|
||||
from app import schemas
|
||||
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]]:
|
||||
"""
|
||||
媒体数量统计
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
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.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification
|
||||
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
@@ -79,8 +82,68 @@ class DownloadChain(ChainBase):
|
||||
下载种子文件,如果是磁力链,会返回磁力链接本身
|
||||
: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(
|
||||
url=torrent.enclosure,
|
||||
url=torrent_url,
|
||||
cookie=torrent.site_cookie,
|
||||
ua=torrent.site_ua,
|
||||
proxy=torrent.site_proxy)
|
||||
@@ -90,7 +153,7 @@ class DownloadChain(ChainBase):
|
||||
return content, "", []
|
||||
|
||||
if not torrent_file:
|
||||
logger.error(f"下载种子文件失败:{torrent.title} - {torrent.enclosure}")
|
||||
logger.error(f"下载种子文件失败:{torrent.title} - {torrent_url}")
|
||||
self.post_message(Notification(
|
||||
channel=channel,
|
||||
mtype=NotificationType.Manual,
|
||||
@@ -122,7 +185,9 @@ class DownloadChain(ChainBase):
|
||||
_folder_name = ""
|
||||
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:
|
||||
return
|
||||
else:
|
||||
@@ -253,12 +318,14 @@ class DownloadChain(ChainBase):
|
||||
contexts: List[Context],
|
||||
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
||||
save_path: str = None,
|
||||
channel: str = None,
|
||||
userid: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
|
||||
"""
|
||||
根据缺失数据,自动种子列表中组合择优下载
|
||||
:param contexts: 资源上下文列表
|
||||
:param no_exists: 缺失的剧集信息
|
||||
:param save_path: 保存路径
|
||||
:param channel: 通知渠道
|
||||
:param userid: 用户ID
|
||||
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id] = {season: NotExistMediaInfo}
|
||||
"""
|
||||
@@ -323,7 +390,8 @@ class DownloadChain(ChainBase):
|
||||
# 如果是电影,直接下载
|
||||
for context in contexts:
|
||||
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)
|
||||
|
||||
@@ -390,11 +458,13 @@ class DownloadChain(ChainBase):
|
||||
context=context,
|
||||
torrent_file=content if isinstance(content, Path) else None,
|
||||
save_path=save_path,
|
||||
channel=channel,
|
||||
userid=userid
|
||||
)
|
||||
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:
|
||||
# 下载成功
|
||||
@@ -452,7 +522,8 @@ class DownloadChain(ChainBase):
|
||||
# 为需要集的子集则下载
|
||||
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:
|
||||
# 下载成功
|
||||
downloaded_list.append(context)
|
||||
@@ -508,7 +579,7 @@ class DownloadChain(ChainBase):
|
||||
and len(meta.season_list) == 1 \
|
||||
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:
|
||||
continue
|
||||
if isinstance(content, str):
|
||||
@@ -529,6 +600,7 @@ class DownloadChain(ChainBase):
|
||||
torrent_file=content if isinstance(content, Path) else None,
|
||||
episodes=selected_episodes,
|
||||
save_path=save_path,
|
||||
channel=channel,
|
||||
userid=userid
|
||||
)
|
||||
if not download_id:
|
||||
|
||||
@@ -10,7 +10,6 @@ from app.core.config import settings
|
||||
from app.db import SessionFactory
|
||||
from app.db.mediaserver_oper import MediaServerOper
|
||||
from app.log import logger
|
||||
from app.schemas import MessageChannel, Notification
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
@@ -23,33 +22,23 @@ class MediaServerChain(ChainBase):
|
||||
def __init__(self, db: Session = None):
|
||||
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)
|
||||
|
||||
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))
|
||||
return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id)
|
||||
|
||||
def sync(self):
|
||||
"""
|
||||
@@ -59,7 +48,6 @@ class MediaServerChain(ChainBase):
|
||||
# 媒体服务器同步使用独立的会话
|
||||
_db = SessionFactory()
|
||||
_dbOper = MediaServerOper(_db)
|
||||
logger.info("开始同步媒体库数据 ...")
|
||||
# 汇总统计
|
||||
total_count = 0
|
||||
# 清空登记薄
|
||||
@@ -67,35 +55,42 @@ class MediaServerChain(ChainBase):
|
||||
# 同步黑名单
|
||||
sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split(
|
||||
",") 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
|
||||
logger.info(f"正在同步媒体库 {library.name} ...")
|
||||
library_count = 0
|
||||
for item in self.items(library.id):
|
||||
if not item:
|
||||
if library.name in sync_blacklist:
|
||||
continue
|
||||
if not item.item_id:
|
||||
continue
|
||||
# 计数
|
||||
library_count += 1
|
||||
seasoninfo = {}
|
||||
# 类型
|
||||
item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影"
|
||||
if item_type == "电视剧":
|
||||
# 查询剧集信息
|
||||
espisodes_info = self.episodes(item.item_id) or []
|
||||
for episode in espisodes_info:
|
||||
seasoninfo[episode.season] = episode.episodes
|
||||
# 插入数据
|
||||
item_dict = item.dict()
|
||||
item_dict['seasoninfo'] = json.dumps(seasoninfo)
|
||||
item_dict['item_type'] = item_type
|
||||
_dbOper.add(**item_dict)
|
||||
logger.info(f"媒体库 {library.name} 同步完成,共同步数量:{library_count}")
|
||||
# 总数累加
|
||||
total_count += library_count
|
||||
logger.info(f"正在同步 {mediaserver} 媒体库 {library.name} ...")
|
||||
library_count = 0
|
||||
for item in self.items(mediaserver, library.id):
|
||||
if not item:
|
||||
continue
|
||||
if not item.item_id:
|
||||
continue
|
||||
# 计数
|
||||
library_count += 1
|
||||
seasoninfo = {}
|
||||
# 类型
|
||||
item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影"
|
||||
if item_type == "电视剧":
|
||||
# 查询剧集信息
|
||||
espisodes_info = self.episodes(mediaserver, item.item_id) or []
|
||||
for episode in espisodes_info:
|
||||
seasoninfo[episode.season] = episode.episodes
|
||||
# 插入数据
|
||||
item_dict = item.dict()
|
||||
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:
|
||||
_db.close()
|
||||
|
||||
@@ -348,6 +348,7 @@ class MessageChain(ChainBase):
|
||||
# 批量下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=cache_list,
|
||||
no_exists=no_exists,
|
||||
channel=channel,
|
||||
userid=userid)
|
||||
if downloads and not lefts:
|
||||
# 全部下载完成
|
||||
|
||||
@@ -81,7 +81,8 @@ class SearchChain(ChainBase):
|
||||
keyword: str = None,
|
||||
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
||||
sites: List[int] = None,
|
||||
filter_rule: str = None,
|
||||
priority_rule: str = None,
|
||||
filter_rule: Dict[str, str] = None,
|
||||
area: str = "title") -> List[Context]:
|
||||
"""
|
||||
根据媒体信息搜索种子资源,精确匹配,应用过滤规则,同时根据no_exists过滤本地已存在的资源
|
||||
@@ -89,7 +90,8 @@ class SearchChain(ChainBase):
|
||||
:param keyword: 搜索关键词
|
||||
:param no_exists: 缺失的媒体信息
|
||||
:param sites: 站点ID列表,为空时搜索所有站点
|
||||
:param filter_rule: 过滤规则,为空是使用默认搜索过滤规则
|
||||
:param priority_rule: 优先级规则,为空时使用搜索优先级规则
|
||||
:param filter_rule: 过滤规则,为空是使用默认过滤规则
|
||||
:param area: 搜索范围,title or imdbid
|
||||
"""
|
||||
logger.info(f'开始搜索资源,关键词:{keyword or mediainfo.title} ...')
|
||||
@@ -129,12 +131,12 @@ class SearchChain(ChainBase):
|
||||
logger.warn(f'{keyword or mediainfo.title} 未搜索到资源')
|
||||
return []
|
||||
# 过滤种子
|
||||
if filter_rule is None:
|
||||
if priority_rule is None:
|
||||
# 取搜索优先级规则
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.SearchFilterRules)
|
||||
if filter_rule:
|
||||
logger.info(f'开始过滤资源,当前规则:{filter_rule} ...')
|
||||
result: List[TorrentInfo] = self.filter_torrents(rule_string=filter_rule,
|
||||
priority_rule = self.systemconfig.get(SystemConfigKey.SearchFilterRules)
|
||||
if priority_rule:
|
||||
logger.info(f'开始过滤资源,当前规则:{priority_rule} ...')
|
||||
result: List[TorrentInfo] = self.filter_torrents(rule_string=priority_rule,
|
||||
torrent_list=torrents,
|
||||
season_episodes=season_episodes,
|
||||
mediainfo=mediainfo)
|
||||
@@ -144,9 +146,10 @@ class SearchChain(ChainBase):
|
||||
logger.warn(f'{keyword or mediainfo.title} 没有符合优先级规则的资源')
|
||||
return []
|
||||
# 使用默认过滤规则再次过滤
|
||||
torrents = self.filter_torrents_by_default_rule(torrents)
|
||||
torrents = self.filter_torrents_by_rule(torrents=torrents,
|
||||
filter_rule=filter_rule)
|
||||
if not torrents:
|
||||
logger.warn(f'{keyword or mediainfo.title} 没有符合默认过滤规则的资源')
|
||||
logger.warn(f'{keyword or mediainfo.title} 没有符合过滤规则的资源')
|
||||
return []
|
||||
# 匹配的资源
|
||||
_match_torrents = []
|
||||
@@ -314,28 +317,43 @@ class SearchChain(ChainBase):
|
||||
# 返回
|
||||
return results
|
||||
|
||||
def filter_torrents_by_default_rule(self,
|
||||
torrents: List[TorrentInfo]) -> List[TorrentInfo]:
|
||||
def filter_torrents_by_rule(self,
|
||||
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 default_include_exclude:
|
||||
if not filter_rule:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
|
||||
if not filter_rule:
|
||||
return torrents
|
||||
include = default_include_exclude.get("include")
|
||||
exclude = default_include_exclude.get("exclude")
|
||||
new_torrents = []
|
||||
for torrent in torrents:
|
||||
# 包含
|
||||
include = filter_rule.get("include")
|
||||
# 排除
|
||||
exclude = filter_rule.get("exclude")
|
||||
|
||||
def __filter_torrent(t: TorrentInfo) -> bool:
|
||||
"""
|
||||
过滤种子
|
||||
"""
|
||||
# 包含
|
||||
if include:
|
||||
if not re.search(r"%s" % include,
|
||||
f"{torrent.title} {torrent.description}", re.I):
|
||||
logger.info(f"{torrent.title} 不匹配包含规则 {include}")
|
||||
continue
|
||||
f"{t.title} {t.description}", re.I):
|
||||
logger.info(f"{t.title} 不匹配包含规则 {include}")
|
||||
return False
|
||||
# 排除
|
||||
if exclude:
|
||||
if re.search(r"%s" % exclude,
|
||||
f"{torrent.title} {torrent.description}", re.I):
|
||||
logger.info(f"{torrent.title} 匹配排除规则 {exclude}")
|
||||
continue
|
||||
new_torrents.append(torrent)
|
||||
return new_torrents
|
||||
f"{t.title} {t.description}", re.I):
|
||||
logger.info(f"{t.title} 匹配排除规则 {exclude}")
|
||||
return False
|
||||
return True
|
||||
|
||||
# 使用默认过滤规则再次过滤
|
||||
return list(filter(lambda t: __filter_torrent(t), torrents))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
from typing import Union, Tuple
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -28,6 +29,66 @@ class SiteChain(ChainBase):
|
||||
self.cookiehelper = CookieHelper()
|
||||
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]:
|
||||
"""
|
||||
测试站点是否可用
|
||||
@@ -39,6 +100,12 @@ class SiteChain(ChainBase):
|
||||
site_info = self.siteoper.get_by_domain(domain)
|
||||
if not site_info:
|
||||
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_cookie = site_info.cookie
|
||||
ua = site_info.ua
|
||||
|
||||
@@ -132,45 +132,6 @@ class SubscribeChain(ChainBase):
|
||||
return True
|
||||
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):
|
||||
"""
|
||||
订阅搜索
|
||||
@@ -262,16 +223,25 @@ class SubscribeChain(ChainBase):
|
||||
sites = json.loads(subscribe.sites)
|
||||
else:
|
||||
sites = None
|
||||
# 过滤规则
|
||||
# 优先级过滤规则
|
||||
if subscribe.best_version:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
||||
priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
||||
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,
|
||||
keyword=subscribe.keyword,
|
||||
no_exists=no_exists,
|
||||
sites=sites,
|
||||
priority_rule=priority_rule,
|
||||
filter_rule=filter_rule)
|
||||
if not contexts:
|
||||
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
|
||||
@@ -477,10 +447,10 @@ class SubscribeChain(ChainBase):
|
||||
}
|
||||
else:
|
||||
no_exists = {}
|
||||
# 包含与排除规则
|
||||
default_include_exclude = self.systemconfig.get(SystemConfigKey.DefaultIncludeExcludeFilter) or {}
|
||||
include = subscribe.include or default_include_exclude.get("include")
|
||||
exclude = subscribe.exclude or default_include_exclude.get("exclude")
|
||||
# 默认过滤规则
|
||||
default_filter = self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {}
|
||||
include = subscribe.include or default_filter.get("include")
|
||||
exclude = subscribe.exclude or default_filter.get("exclude")
|
||||
# 遍历缓存种子
|
||||
_match_context = []
|
||||
for domain, contexts in torrents.items():
|
||||
@@ -493,7 +463,7 @@ class SubscribeChain(ChainBase):
|
||||
if torrent_mediainfo.tmdb_id != mediainfo.tmdb_id \
|
||||
or torrent_mediainfo.type != mediainfo.type:
|
||||
continue
|
||||
# 过滤规则
|
||||
# 优先级过滤规则
|
||||
if subscribe.best_version:
|
||||
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
||||
else:
|
||||
|
||||
125
app/command.py
125
app/command.py
@@ -1,11 +1,9 @@
|
||||
import traceback
|
||||
from threading import Thread, Event
|
||||
from typing import Any, Union
|
||||
from typing import Any, Union, Dict
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.chain.cookiecloud import CookieCloudChain
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.site import SiteChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
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.db import SessionFactory
|
||||
from app.log import logger
|
||||
from app.scheduler import Scheduler
|
||||
from app.schemas import Notification
|
||||
from app.schemas.types import EventType, MessageChannel
|
||||
from app.utils.object import ObjectUtils
|
||||
from app.utils.singleton import Singleton
|
||||
@@ -49,13 +49,15 @@ class Command(metaclass=Singleton):
|
||||
self.pluginmanager = PluginManager()
|
||||
# 处理链
|
||||
self.chain = CommandChian(self._db)
|
||||
# 定时服务管理
|
||||
self.scheduler = Scheduler()
|
||||
# 内置命令
|
||||
self._commands = {
|
||||
"/cookiecloud": {
|
||||
"func": CookieCloudChain(self._db).remote_sync,
|
||||
"id": "cookiecloud",
|
||||
"type": "scheduler",
|
||||
"description": "同步站点",
|
||||
"category": "站点",
|
||||
"data": {}
|
||||
"category": "站点"
|
||||
},
|
||||
"/sites": {
|
||||
"func": SiteChain(self._db).remote_list,
|
||||
@@ -79,10 +81,10 @@ class Command(metaclass=Singleton):
|
||||
"data": {}
|
||||
},
|
||||
"/mediaserver_sync": {
|
||||
"func": MediaServerChain(self._db).remote_sync,
|
||||
"id": "mediaserver_sync",
|
||||
"type": "scheduler",
|
||||
"description": "同步媒体服务器",
|
||||
"category": "管理",
|
||||
"data": {}
|
||||
"category": "管理"
|
||||
},
|
||||
"/subscribes": {
|
||||
"func": SubscribeChain(self._db).remote_list,
|
||||
@@ -91,22 +93,27 @@ class Command(metaclass=Singleton):
|
||||
"data": {}
|
||||
},
|
||||
"/subscribe_refresh": {
|
||||
"func": SubscribeChain(self._db).remote_refresh,
|
||||
"id": "subscribe_refresh",
|
||||
"type": "scheduler",
|
||||
"description": "刷新订阅",
|
||||
"category": "订阅",
|
||||
"data": {}
|
||||
"category": "订阅"
|
||||
},
|
||||
"/subscribe_search": {
|
||||
"func": SubscribeChain(self._db).remote_search,
|
||||
"id": "subscribe_search",
|
||||
"type": "scheduler",
|
||||
"description": "搜索订阅",
|
||||
"category": "订阅",
|
||||
"data": {}
|
||||
"category": "订阅"
|
||||
},
|
||||
"/subscribe_delete": {
|
||||
"func": SubscribeChain(self._db).remote_delete,
|
||||
"description": "删除订阅",
|
||||
"data": {}
|
||||
},
|
||||
"/subscribe_tmdb": {
|
||||
"id": "subscribe_tmdb",
|
||||
"type": "scheduler",
|
||||
"description": "订阅元数据更新"
|
||||
},
|
||||
"/downloading": {
|
||||
"func": DownloadChain(self._db).remote_downloading,
|
||||
"description": "正在下载",
|
||||
@@ -114,10 +121,10 @@ class Command(metaclass=Singleton):
|
||||
"data": {}
|
||||
},
|
||||
"/transfer": {
|
||||
"func": TransferChain(self._db).process,
|
||||
"id": "transfer",
|
||||
"type": "scheduler",
|
||||
"description": "下载文件整理",
|
||||
"category": "管理",
|
||||
"data": {}
|
||||
"category": "管理"
|
||||
},
|
||||
"/redo": {
|
||||
"func": TransferChain(self._db).remote_transfer,
|
||||
@@ -175,6 +182,56 @@ class Command(metaclass=Singleton):
|
||||
except Exception as e:
|
||||
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):
|
||||
"""
|
||||
停止事件处理线程
|
||||
@@ -216,27 +273,19 @@ class Command(metaclass=Singleton):
|
||||
command = self.get(cmd)
|
||||
if command:
|
||||
try:
|
||||
logger.info(f"用户 {userid} 开始执行:{command.get('description')} ...")
|
||||
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)
|
||||
if userid:
|
||||
logger.info(f"用户 {userid} 开始执行:{command.get('description')} ...")
|
||||
else:
|
||||
# 没有参数
|
||||
command['func']()
|
||||
logger.info(f"用户 {userid} {command.get('description')} 执行完成")
|
||||
logger.info(f"开始执行:{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:
|
||||
logger.error(f"执行命令 {cmd} 出错:{str(err)}")
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -76,7 +76,7 @@ class Settings(BaseSettings):
|
||||
AUTH_SITE: str = ""
|
||||
# 交互搜索自动下载用户ID,使用,分割
|
||||
AUTO_DOWNLOAD_USER: str = None
|
||||
# 消息通知渠道 telegram/wechat/slack
|
||||
# 消息通知渠道 telegram/wechat/slack,多个通知渠道用,分隔
|
||||
MESSAGER: str = "telegram"
|
||||
# WeChat企业ID
|
||||
WECHAT_CORPID: str = None
|
||||
@@ -142,7 +142,7 @@ class Settings(BaseSettings):
|
||||
DOWNLOAD_CATEGORY: bool = False
|
||||
# 下载站点字幕
|
||||
DOWNLOAD_SUBTITLE: bool = True
|
||||
# 媒体服务器 emby/jellyfin/plex
|
||||
# 媒体服务器 emby/jellyfin/plex,多个媒体服务器,分割
|
||||
MEDIASERVER: str = "emby"
|
||||
# 入库刷新媒体库
|
||||
REFRESH_MEDIASERVER: bool = True
|
||||
|
||||
@@ -23,14 +23,17 @@ class CookieHelper:
|
||||
"password": [
|
||||
'//input[@name="password"]',
|
||||
'//input[@id="form_item_password"]',
|
||||
'//input[@id="password"]'
|
||||
'//input[@id="password"]',
|
||||
'//input[@type="password"]'
|
||||
],
|
||||
"captcha": [
|
||||
'//input[@name="imagestring"]',
|
||||
'//input[@name="captcha"]',
|
||||
'//input[@id="form_item_captcha"]'
|
||||
'//input[@id="form_item_captcha"]',
|
||||
'//input[@placeholder="驗證碼"]'
|
||||
],
|
||||
"captcha_img": [
|
||||
'//img[@alt="captcha"]/@src',
|
||||
'//img[@alt="CAPTCHA"]/@src',
|
||||
'//img[@alt="SECURITY CODE"]/@src',
|
||||
'//img[@id="LAY-user-get-vercode"]/@src',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union, Any, List, Generator
|
||||
|
||||
@@ -41,7 +40,7 @@ class EmbyModule(_ModuleBase):
|
||||
# Emby认证
|
||||
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报文体
|
||||
:param body: 请求体
|
||||
@@ -49,11 +48,7 @@ class EmbyModule(_ModuleBase):
|
||||
:param args: 请求参数
|
||||
:return: 字典,解析为消息时需要包含:title、text、image
|
||||
"""
|
||||
if form and form.get("data"):
|
||||
result = form.get("data")
|
||||
else:
|
||||
result = json.dumps(dict(args))
|
||||
return self.emby.get_webhook_message(result)
|
||||
return self.emby.get_webhook_message(form, args)
|
||||
|
||||
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}")
|
||||
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: 识别的媒体信息
|
||||
@@ -103,25 +98,27 @@ class EmbyModule(_ModuleBase):
|
||||
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()
|
||||
user_count = self.emby.get_user_count()
|
||||
return schemas.Statistic(
|
||||
return [schemas.Statistic(
|
||||
movie_count=media_statistic.get("MovieCount") or 0,
|
||||
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||
episode_count=media_statistic.get("EpisodeCount") 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()
|
||||
if not librarys:
|
||||
return []
|
||||
@@ -133,10 +130,12 @@ class EmbyModule(_ModuleBase):
|
||||
path=library.get("path")
|
||||
) 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)
|
||||
for item in items:
|
||||
yield schemas.MediaServerItem(
|
||||
@@ -153,10 +152,13 @@ class EmbyModule(_ModuleBase):
|
||||
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)
|
||||
if not seasoninfo:
|
||||
return []
|
||||
|
||||
@@ -545,7 +545,7 @@ class Emby(metaclass=Singleton):
|
||||
logger.error(f"连接Users/Items出错:" + str(e))
|
||||
yield {}
|
||||
|
||||
def get_webhook_message(self, message_str: str) -> WebhookEventInfo:
|
||||
def get_webhook_message(self, form: any, args: dict) -> Optional[WebhookEventInfo]:
|
||||
"""
|
||||
解析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}")
|
||||
eventItem = WebhookEventInfo(event=message.get('Event', ''), channel="emby")
|
||||
eventItem = WebhookEventInfo(event=eventType, channel="emby")
|
||||
if message.get('Item'):
|
||||
if message.get('Item', {}).get('Type') == 'Episode':
|
||||
eventItem.item_type = "TV"
|
||||
|
||||
@@ -536,16 +536,14 @@ class FileTransferModule(_ModuleBase):
|
||||
@staticmethod
|
||||
def get_library_path(path: Path):
|
||||
"""
|
||||
根据目录查询其所在的媒体库目录
|
||||
根据目录查询其所在的媒体库目录,查询不到的返回输入目录
|
||||
"""
|
||||
if not path:
|
||||
return None
|
||||
if not settings.LIBRARY_PATHS:
|
||||
return None
|
||||
return path
|
||||
# 目的路径,多路径以,分隔
|
||||
dest_paths = settings.LIBRARY_PATHS
|
||||
if len(dest_paths) == 1:
|
||||
return dest_paths[0]
|
||||
for libpath in dest_paths:
|
||||
try:
|
||||
if path.is_relative_to(libpath):
|
||||
@@ -553,7 +551,7 @@ class FileTransferModule(_ModuleBase):
|
||||
except Exception as e:
|
||||
logger.debug(f"计算媒体库路径时出错:{e}")
|
||||
continue
|
||||
return None
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def get_target_path(in_path: Path = None) -> Optional[Path]:
|
||||
|
||||
@@ -6,6 +6,7 @@ from ruamel.yaml import CommentedMap
|
||||
from app.core.context import MediaInfo, TorrentInfo
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
from app.modules.indexer.mtorrent import MTorrentSpider
|
||||
from app.modules.indexer.spider import TorrentSpider
|
||||
from app.modules.indexer.tnode import TNodeSpider
|
||||
from app.modules.indexer.torrentleech import TorrentLeech
|
||||
@@ -74,6 +75,12 @@ class IndexerModule(_ModuleBase):
|
||||
keyword=search_word,
|
||||
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:
|
||||
error_flag, result_array = self.__spider_search(
|
||||
keyword=search_word,
|
||||
|
||||
144
app/modules/indexer/mtorrent.py
Normal file
144
app/modules/indexer/mtorrent.py
Normal 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}"
|
||||
@@ -41,7 +41,7 @@ class JellyfinModule(_ModuleBase):
|
||||
# Jellyfin认证
|
||||
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报文体
|
||||
:param body: 请求体
|
||||
@@ -49,7 +49,7 @@ class JellyfinModule(_ModuleBase):
|
||||
:param args: 请求参数
|
||||
: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]:
|
||||
"""
|
||||
@@ -83,32 +83,34 @@ class JellyfinModule(_ModuleBase):
|
||||
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{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 file_path: 文件路径
|
||||
: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()
|
||||
user_count = self.jellyfin.get_user_count()
|
||||
return schemas.Statistic(
|
||||
return [schemas.Statistic(
|
||||
movie_count=media_statistic.get("MovieCount") or 0,
|
||||
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||
episode_count=media_statistic.get("EpisodeCount") 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()
|
||||
if not librarys:
|
||||
return []
|
||||
@@ -120,10 +122,12 @@ class JellyfinModule(_ModuleBase):
|
||||
path=library.get("path")
|
||||
) 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)
|
||||
for item in items:
|
||||
yield schemas.MediaServerItem(
|
||||
@@ -140,10 +144,13 @@ class JellyfinModule(_ModuleBase):
|
||||
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)
|
||||
if not seasoninfo:
|
||||
return []
|
||||
|
||||
@@ -387,7 +387,7 @@ class Jellyfin(metaclass=Singleton):
|
||||
logger.error(f"连接Library/Refresh出错:" + str(e))
|
||||
return False
|
||||
|
||||
def get_webhook_message(self, message: dict) -> WebhookEventInfo:
|
||||
def get_webhook_message(self, body: any) -> Optional[WebhookEventInfo]:
|
||||
"""
|
||||
解析Jellyfin报文
|
||||
{
|
||||
@@ -450,9 +450,21 @@ class Jellyfin(metaclass=Singleton):
|
||||
"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}")
|
||||
eventType = message.get('NotificationType')
|
||||
if not eventType:
|
||||
return None
|
||||
eventItem = WebhookEventInfo(
|
||||
event=message.get('NotificationType', ''),
|
||||
event=eventType,
|
||||
channel="jellyfin"
|
||||
)
|
||||
eventItem.item_id = message.get('ItemId')
|
||||
|
||||
@@ -31,7 +31,7 @@ class PlexModule(_ModuleBase):
|
||||
if not self.plex.is_inactive():
|
||||
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报文体
|
||||
:param body: 请求体
|
||||
@@ -39,7 +39,7 @@ class PlexModule(_ModuleBase):
|
||||
:param args: 请求参数
|
||||
: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]:
|
||||
"""
|
||||
@@ -77,7 +77,7 @@ class PlexModule(_ModuleBase):
|
||||
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{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: 识别的媒体信息
|
||||
@@ -93,24 +93,26 @@ class PlexModule(_ModuleBase):
|
||||
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()
|
||||
return schemas.Statistic(
|
||||
return [schemas.Statistic(
|
||||
movie_count=media_statistic.get("MovieCount") or 0,
|
||||
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||
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()
|
||||
if not librarys:
|
||||
return []
|
||||
@@ -122,10 +124,12 @@ class PlexModule(_ModuleBase):
|
||||
path=library.get("path")
|
||||
) 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)
|
||||
for item in items:
|
||||
yield schemas.MediaServerItem(
|
||||
@@ -142,10 +146,13 @@ class PlexModule(_ModuleBase):
|
||||
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)
|
||||
if not seasoninfo:
|
||||
return []
|
||||
|
||||
@@ -353,7 +353,7 @@ class Plex(metaclass=Singleton):
|
||||
logger.error(f"获取媒体库列表出错:{err}")
|
||||
yield {}
|
||||
|
||||
def get_webhook_message(self, message_str: str) -> WebhookEventInfo:
|
||||
def get_webhook_message(self, form: any) -> Optional[WebhookEventInfo]:
|
||||
"""
|
||||
解析Plex报文
|
||||
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}")
|
||||
eventItem = WebhookEventInfo(event=message.get('event', ''), channel="plex")
|
||||
eventItem = WebhookEventInfo(event=eventType, channel="plex")
|
||||
if message.get('Metadata'):
|
||||
if message.get('Metadata', {}).get('type') == 'episode':
|
||||
eventItem.item_type = "TV"
|
||||
|
||||
@@ -131,130 +131,130 @@ class BestFilmVersion(_PluginBase):
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '发送通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'only_once',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'webhook_enabled',
|
||||
'label': 'Webhook',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '5位cron表达式,留空自动'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'text': '支持主动定时获取媒体库数据和Webhook实时触发两种方式,两者只能选其一,'
|
||||
'Webhook需要在媒体服务器设置发送Webhook报文。'
|
||||
'Plex使用主动获取时,建议执行周期设置大于1小时,'
|
||||
'收藏Api调用Plex官网接口,有频率限制。'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"notify": False,
|
||||
"cron": "*/30 * * * *",
|
||||
"webhook_enabled": False,
|
||||
"only_once": False
|
||||
}
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '发送通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'only_once',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'webhook_enabled',
|
||||
'label': 'Webhook',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '5位cron表达式,留空自动'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'text': '支持主动定时获取媒体库数据和Webhook实时触发两种方式,两者只能选其一,'
|
||||
'Webhook需要在媒体服务器设置发送Webhook报文。'
|
||||
'Plex使用主动获取时,建议执行周期设置大于1小时,'
|
||||
'收藏Api调用Plex官网接口,有频率限制。'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"notify": False,
|
||||
"cron": "*/30 * * * *",
|
||||
"webhook_enabled": False,
|
||||
"only_once": False
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
"""
|
||||
@@ -386,81 +386,85 @@ class BestFilmVersion(_PluginBase):
|
||||
# 读取历史记录
|
||||
history = self.get_data('history') or []
|
||||
|
||||
all_item = []
|
||||
# 媒体服务器类型,多个以,分隔
|
||||
if not settings.MEDIASERVER:
|
||||
return
|
||||
media_servers = settings.MEDIASERVER.split(',')
|
||||
|
||||
# 读取收藏
|
||||
if settings.MEDIASERVER == 'jellyfin':
|
||||
self.jellyfin_get_items(all_item)
|
||||
elif settings.MEDIASERVER == 'emby':
|
||||
self.emby_get_items(all_item)
|
||||
else:
|
||||
resp = self.plex_get_watchlist()
|
||||
if not resp:
|
||||
return
|
||||
all_item.extend(resp)
|
||||
all_items = {}
|
||||
for media_server in media_servers:
|
||||
if media_server == 'jellyfin':
|
||||
all_items['jellyfin'] = self.jellyfin_get_items()
|
||||
elif media_server == 'emby':
|
||||
all_items['emby'] = self.emby_get_items()
|
||||
else:
|
||||
all_items['plex'] = self.plex_get_watchlist()
|
||||
|
||||
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]
|
||||
|
||||
# all_item 根据电影名去重
|
||||
result = reduce(function, all_item, [])
|
||||
|
||||
for data in result:
|
||||
# 检查缓存
|
||||
if data.get('Name') in caches:
|
||||
continue
|
||||
|
||||
# 获取详情
|
||||
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'):
|
||||
# 处理所有结果
|
||||
for server, all_item in all_items.items():
|
||||
# all_item 根据电影名去重
|
||||
result = reduce(function, all_item, [])
|
||||
for data in result:
|
||||
# 检查缓存
|
||||
if data.get('Name') in caches:
|
||||
continue
|
||||
tmdb_find_id = str(media_info_id.get('Url')).split('/')
|
||||
tmdb_find_id.reverse()
|
||||
tmdb_id = tmdb_find_id[0]
|
||||
# 识别媒体信息
|
||||
mediainfo: MediaInfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{data.get("Name")},tmdbID:{tmdb_id}')
|
||||
|
||||
# 获取详情
|
||||
if server == 'jellyfin':
|
||||
item_info_resp = Jellyfin().get_iteminfo(itemid=data.get('Id'))
|
||||
elif server == '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
|
||||
# 添加订阅
|
||||
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")
|
||||
})
|
||||
|
||||
# 只接受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
|
||||
tmdb_find_id = str(media_info_id.get('Url')).split('/')
|
||||
tmdb_find_id.reverse()
|
||||
tmdb_id = tmdb_find_id[0]
|
||||
# 识别媒体信息
|
||||
mediainfo: MediaInfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{data.get("Name")},tmdbID:{tmdb_id}')
|
||||
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)
|
||||
# 保存缓存
|
||||
@@ -468,13 +472,14 @@ class BestFilmVersion(_PluginBase):
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
def jellyfin_get_items(self, all_item):
|
||||
def jellyfin_get_items(self) -> List[dict]:
|
||||
# 获取所有user
|
||||
users_url = "{HOST}Users?&apikey={APIKEY}"
|
||||
users = self.get_users(Jellyfin().get_data(users_url))
|
||||
if not users:
|
||||
logger.info(f"bestfilmversion/users_url: {users_url}")
|
||||
return
|
||||
return []
|
||||
all_items = []
|
||||
for user in users:
|
||||
# 根据加入日期 降序排序
|
||||
url = "{HOST}Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \
|
||||
@@ -490,14 +495,16 @@ class BestFilmVersion(_PluginBase):
|
||||
resp = self.get_items(Jellyfin().get_data(url))
|
||||
if not resp:
|
||||
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
|
||||
get_users_url = "{HOST}Users?&api_key={APIKEY}"
|
||||
users = self.get_users(Emby().get_data(get_users_url))
|
||||
if not users:
|
||||
return
|
||||
return []
|
||||
all_items = []
|
||||
for user in users:
|
||||
# 根据加入日期 降序排序
|
||||
url = "{HOST}emby/Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \
|
||||
@@ -512,7 +519,8 @@ class BestFilmVersion(_PluginBase):
|
||||
resp = self.get_items(Emby().get_data(url))
|
||||
if not resp:
|
||||
continue
|
||||
all_item.extend(resp)
|
||||
all_items.extend(resp)
|
||||
return all_items
|
||||
|
||||
@staticmethod
|
||||
def get_items(resp: Response):
|
||||
@@ -538,7 +546,7 @@ class BestFilmVersion(_PluginBase):
|
||||
return []
|
||||
|
||||
@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" \
|
||||
f"&X-Plex-Container-Start=0&X-Plex-Container-Size=50" \
|
||||
|
||||
@@ -598,7 +598,7 @@ class DirMonitor(_PluginBase):
|
||||
'rows': 5,
|
||||
'placeholder': '每一行一个目录,支持两种配置方式:\n'
|
||||
'监控目录\n'
|
||||
'监控目录:转移目的目录'
|
||||
'监控目录:转移目的目录(需同时在媒体库目录中配置该目的目录)'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -718,19 +718,21 @@ class MediaSyncDel(_PluginBase):
|
||||
"""
|
||||
# 读取历史记录
|
||||
history = self.get_data('history') or []
|
||||
|
||||
# 媒体服务器类型
|
||||
media_server = settings.MEDIASERVER
|
||||
|
||||
last_time = self.get_data("last_time")
|
||||
del_medias = []
|
||||
if media_server == 'emby':
|
||||
del_medias = self.parse_emby_log(last_time)
|
||||
elif media_server == 'jellyfin':
|
||||
del_medias = self.parse_jellyfin_log(last_time)
|
||||
elif media_server == 'plex':
|
||||
# TODO plex解析日志
|
||||
|
||||
# 媒体服务器类型,多个以,分隔
|
||||
if not settings.MEDIASERVER:
|
||||
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:
|
||||
logger.error("未解析到已删除媒体信息")
|
||||
|
||||
@@ -383,78 +383,86 @@ class SpeedLimiter(_PluginBase):
|
||||
return
|
||||
# 当前播放的总比特率
|
||||
total_bit_rate = 0
|
||||
# 查询播放中会话
|
||||
playing_sessions = []
|
||||
if settings.MEDIASERVER == "emby":
|
||||
req_url = "{HOST}emby/Sessions?api_key={APIKEY}"
|
||||
try:
|
||||
res = Emby().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"获取Emby播放会话失败:{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":
|
||||
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 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
|
||||
})
|
||||
# 媒体服务器类型,多个以,分隔
|
||||
if not settings.MEDIASERVER:
|
||||
return
|
||||
media_servers = settings.MEDIASERVER.split(',')
|
||||
# 查询所有媒体服务器状态
|
||||
for media_server in media_servers:
|
||||
# 查询播放中会话
|
||||
playing_sessions = []
|
||||
if media_server == "emby":
|
||||
req_url = "{HOST}emby/Sessions?api_key={APIKEY}"
|
||||
try:
|
||||
res = Emby().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"获取Emby播放会话失败:{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("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":
|
||||
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:
|
||||
# 开启智能限速计算上传限速
|
||||
|
||||
@@ -644,7 +644,7 @@ class TorrentRemover(_PluginBase):
|
||||
return None
|
||||
if self._torrentstates and torrent.state not in self._torrentstates:
|
||||
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 {
|
||||
"id": torrent.hash,
|
||||
|
||||
185
app/scheduler.py
185
app/scheduler.py
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
|
||||
import pytz
|
||||
from apscheduler.executors.pool import ThreadPoolExecutor
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
from app import schemas
|
||||
from app.chain import ChainBase
|
||||
from app.chain.cookiecloud import CookieCloudChain
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
@@ -40,57 +42,153 @@ class Scheduler(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
# 数据库连接
|
||||
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:
|
||||
return
|
||||
|
||||
# CookieCloud定时同步
|
||||
if settings.COOKIECLOUD_INTERVAL:
|
||||
self._scheduler.add_job(CookieCloudChain(self._db).process,
|
||||
"interval",
|
||||
minutes=settings.COOKIECLOUD_INTERVAL,
|
||||
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=1),
|
||||
name="同步CookieCloud站点")
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
"interval",
|
||||
id="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:
|
||||
self._scheduler.add_job(MediaServerChain(self._db).sync, "interval",
|
||||
hours=settings.MEDIASERVER_SYNC_INTERVAL,
|
||||
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=5),
|
||||
name="同步媒体服务器")
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
"interval",
|
||||
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分钟检查一次)
|
||||
self._scheduler.add_job(SubscribeChain(self._db).search, "interval",
|
||||
minutes=5, kwargs={'state': 'N'})
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
"interval",
|
||||
minutes=5,
|
||||
kwargs={
|
||||
'job_id': 'subscribe_search',
|
||||
'state': 'N'
|
||||
}
|
||||
)
|
||||
|
||||
# 检查更新订阅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小时搜索一次
|
||||
if settings.SUBSCRIBE_SEARCH:
|
||||
self._scheduler.add_job(SubscribeChain(self._db).search, "interval",
|
||||
hours=24, kwargs={'state': 'R'}, name="订阅搜索")
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
"interval",
|
||||
id="subscribe_search",
|
||||
name="订阅搜索",
|
||||
hours=24,
|
||||
kwargs={
|
||||
'job_id': 'subscribe_search',
|
||||
'state': 'R'
|
||||
}
|
||||
)
|
||||
|
||||
if settings.SUBSCRIBE_MODE == "spider":
|
||||
# 站点首页种子定时刷新模式
|
||||
triggers = TimerUtils.random_scheduler(num_executions=30)
|
||||
for trigger in triggers:
|
||||
self._scheduler.add_job(SubscribeChain(self._db).refresh, "cron",
|
||||
hour=trigger.hour, minute=trigger.minute, name="订阅刷新")
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
"cron",
|
||||
id=f"subscribe_refresh|{trigger.hour}:{trigger.minute}",
|
||||
name="订阅刷新",
|
||||
hour=trigger.hour,
|
||||
minute=trigger.minute,
|
||||
kwargs={
|
||||
'job_id': 'subscribe_refresh'
|
||||
})
|
||||
else:
|
||||
# RSS订阅模式
|
||||
if not settings.SUBSCRIBE_RSS_INTERVAL:
|
||||
settings.SUBSCRIBE_RSS_INTERVAL = 30
|
||||
elif settings.SUBSCRIBE_RSS_INTERVAL < 5:
|
||||
settings.SUBSCRIBE_RSS_INTERVAL = 5
|
||||
self._scheduler.add_job(SubscribeChain(self._db).refresh, "interval",
|
||||
minutes=settings.SUBSCRIBE_RSS_INTERVAL, name="订阅刷新")
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
"interval",
|
||||
id="subscribe_refresh",
|
||||
name="RSS订阅刷新",
|
||||
minutes=settings.SUBSCRIBE_RSS_INTERVAL,
|
||||
kwargs={
|
||||
'job_id': 'subscribe_refresh'
|
||||
}
|
||||
)
|
||||
|
||||
# 下载器文件转移(每5分钟)
|
||||
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())
|
||||
@@ -98,11 +196,54 @@ class Scheduler(metaclass=Singleton):
|
||||
# 启动定时服务
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -66,8 +66,8 @@ class SystemConfigKey(Enum):
|
||||
SubscribeFilterRules = "SubscribeFilterRules"
|
||||
# 洗版规则
|
||||
BestVersionFilterRules = "BestVersionFilterRules"
|
||||
# 默认包含与排除规则
|
||||
DefaultIncludeExcludeFilter = "DefaultIncludeExcludeFilter"
|
||||
# 默认过滤规则
|
||||
DefaultFilterRules = "DefaultFilterRules"
|
||||
# 转移屏蔽词
|
||||
TransferExcludeWords = "TransferExcludeWords"
|
||||
|
||||
|
||||
@@ -17,19 +17,18 @@ class SiteUtils:
|
||||
if html.xpath("//input[@type='password']"):
|
||||
return False
|
||||
# 是否存在登出和用户面板等链接
|
||||
xpaths = ['//a[contains(@href, "logout")'
|
||||
' or contains(@data-url, "logout")'
|
||||
' or contains(@href, "mybonus") '
|
||||
' or contains(@onclick, "logout")'
|
||||
' or contains(@href, "usercp")]',
|
||||
'//form[contains(@action, "logout")]']
|
||||
xpaths = [
|
||||
'//a[contains(@href, "logout")'
|
||||
' or contains(@data-url, "logout")'
|
||||
' or contains(@href, "mybonus") '
|
||||
' or contains(@onclick, "logout")'
|
||||
' or contains(@href, "usercp")]',
|
||||
'//form[contains(@action, "logout")]',
|
||||
'//div[@class="user-info-side"]'
|
||||
]
|
||||
for xpath in xpaths:
|
||||
if html.xpath(xpath):
|
||||
return True
|
||||
user_info_div = html.xpath('//div[@class="user-info-side"]')
|
||||
if user_info_div:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -343,9 +343,9 @@ class SystemUtils:
|
||||
# 获取当前容器的 ID
|
||||
with open('/proc/self/mountinfo', 'r') as f:
|
||||
data = f.read()
|
||||
index_resolv_conf = data.find("resolv.conf")
|
||||
index_resolv_conf = data.find("/sys/fs/cgroup/devices")
|
||||
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
|
||||
container_id = data[index_first_slash:index_second_slash]
|
||||
if not container_id:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.2.4'
|
||||
APP_VERSION = 'v1.2.5'
|
||||
|
||||
Reference in New Issue
Block a user