mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-19 01:39:29 +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`,开启后下载完成时才会自动整理入库
|
- **DOWNLOADER_MONITOR:** 下载器监控,`true`/`false`,默认为`true`,开启后下载完成时才会自动整理入库
|
||||||
|
|
||||||
- **MEDIASERVER:** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
|
- **MEDIASERVER:** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时开启多个使用`,`分隔。还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
|
||||||
|
|
||||||
- `emby`设置项:
|
- `emby`设置项:
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,4 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def downgrade() -> 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 pathlib import Path
|
||||||
from typing import Any, List
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from requests import Session
|
from requests import Session
|
||||||
@@ -11,9 +11,7 @@ from app.core.security import verify_token
|
|||||||
from app.db import get_db
|
from app.db import get_db
|
||||||
from app.db.models.transferhistory import TransferHistory
|
from app.db.models.transferhistory import TransferHistory
|
||||||
from app.scheduler import Scheduler
|
from app.scheduler import Scheduler
|
||||||
from app.utils.string import StringUtils
|
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
from app.utils.timer import TimerUtils
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -24,14 +22,16 @@ def statistic(db: Session = Depends(get_db),
|
|||||||
"""
|
"""
|
||||||
查询媒体数量统计信息
|
查询媒体数量统计信息
|
||||||
"""
|
"""
|
||||||
media_statistic = DashboardChain(db).media_statistic()
|
media_statistics: Optional[List[schemas.Statistic]] = DashboardChain(db).media_statistic()
|
||||||
if media_statistic:
|
if media_statistics:
|
||||||
return schemas.Statistic(
|
# 汇总各媒体库统计信息
|
||||||
movie_count=media_statistic.movie_count,
|
ret_statistic = schemas.Statistic()
|
||||||
tv_count=media_statistic.tv_count,
|
for media_statistic in media_statistics:
|
||||||
episode_count=media_statistic.episode_count,
|
ret_statistic.movie_count += media_statistic.movie_count
|
||||||
user_count=media_statistic.user_count
|
ret_statistic.tv_count += media_statistic.tv_count
|
||||||
)
|
ret_statistic.episode_count += media_statistic.episode_count
|
||||||
|
ret_statistic.user_count += media_statistic.user_count
|
||||||
|
return ret_statistic
|
||||||
else:
|
else:
|
||||||
return schemas.Statistic()
|
return schemas.Statistic()
|
||||||
|
|
||||||
@@ -81,37 +81,7 @@ def schedule(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
|||||||
"""
|
"""
|
||||||
查询后台服务信息
|
查询后台服务信息
|
||||||
"""
|
"""
|
||||||
# 返回计时任务
|
return Scheduler().list()
|
||||||
schedulers = []
|
|
||||||
# 去重
|
|
||||||
added = []
|
|
||||||
jobs = Scheduler().list()
|
|
||||||
# 按照下次运行时间排序
|
|
||||||
jobs.sort(key=lambda x: x.next_run_time)
|
|
||||||
for job in jobs:
|
|
||||||
if job.name not in added:
|
|
||||||
added.append(job.name)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
if not StringUtils.is_chinese(job.name):
|
|
||||||
continue
|
|
||||||
if not job.next_run_time:
|
|
||||||
status = "已停止"
|
|
||||||
next_run = ""
|
|
||||||
else:
|
|
||||||
next_run = TimerUtils.time_difference(job.next_run_time)
|
|
||||||
if not next_run:
|
|
||||||
status = "正在运行"
|
|
||||||
else:
|
|
||||||
status = "阻塞" if job.pending else "等待"
|
|
||||||
schedulers.append(schemas.ScheduleInfo(
|
|
||||||
id=job.id,
|
|
||||||
name=job.name,
|
|
||||||
status=status,
|
|
||||||
next_run=next_run
|
|
||||||
))
|
|
||||||
|
|
||||||
return schedulers
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/transfer", summary="文件整理统计", response_model=List[int])
|
@router.get("/transfer", summary="文件整理统计", response_model=List[int])
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from typing import Any, List
|
from typing import Any, List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.core.plugin import PluginManager
|
from app.core.plugin import PluginManager
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
from app.db import get_db
|
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.schemas.types import SystemConfigKey
|
from app.schemas.types import SystemConfigKey
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from sqlalchemy.orm import Session
|
|||||||
from starlette.background import BackgroundTasks
|
from starlette.background import BackgroundTasks
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.cookiecloud import CookieCloudChain
|
|
||||||
from app.chain.site import SiteChain
|
from app.chain.site import SiteChain
|
||||||
from app.chain.torrents import TorrentsChain
|
from app.chain.torrents import TorrentsChain
|
||||||
from app.core.event import EventManager
|
from app.core.event import EventManager
|
||||||
@@ -15,19 +14,13 @@ from app.db.models.site import Site
|
|||||||
from app.db.models.siteicon import SiteIcon
|
from app.db.models.siteicon import SiteIcon
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.helper.sites import SitesHelper
|
from app.helper.sites import SitesHelper
|
||||||
|
from app.scheduler import Scheduler
|
||||||
from app.schemas.types import SystemConfigKey, EventType
|
from app.schemas.types import SystemConfigKey, EventType
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
def start_cookiecloud_sync(db: Session):
|
|
||||||
"""
|
|
||||||
后台启动CookieCloud站点同步
|
|
||||||
"""
|
|
||||||
CookieCloudChain(db).process(manual=True)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", summary="所有站点", response_model=List[schemas.Site])
|
@router.get("/", summary="所有站点", response_model=List[schemas.Site])
|
||||||
def read_sites(db: Session = Depends(get_db),
|
def read_sites(db: Session = Depends(get_db),
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> List[dict]:
|
_: schemas.TokenPayload = Depends(verify_token)) -> List[dict]:
|
||||||
@@ -101,12 +94,11 @@ def delete_site(
|
|||||||
|
|
||||||
@router.get("/cookiecloud", summary="CookieCloud同步", response_model=schemas.Response)
|
@router.get("/cookiecloud", summary="CookieCloud同步", response_model=schemas.Response)
|
||||||
def cookie_cloud_sync(background_tasks: BackgroundTasks,
|
def cookie_cloud_sync(background_tasks: BackgroundTasks,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
运行CookieCloud同步站点信息
|
运行CookieCloud同步站点信息
|
||||||
"""
|
"""
|
||||||
background_tasks.add_task(start_cookiecloud_sync, db)
|
background_tasks.add_task(Scheduler().start, job_id="cookiecloud")
|
||||||
return schemas.Response(success=True, message="CookieCloud同步任务已启动!")
|
return schemas.Response(success=True, message="CookieCloud同步任务已启动!")
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +111,8 @@ def cookie_cloud_sync(db: Session = Depends(get_db),
|
|||||||
Site.reset(db)
|
Site.reset(db)
|
||||||
SystemConfigOper().set(SystemConfigKey.IndexerSites, [])
|
SystemConfigOper().set(SystemConfigKey.IndexerSites, [])
|
||||||
SystemConfigOper().set(SystemConfigKey.RssSites, [])
|
SystemConfigOper().set(SystemConfigKey.RssSites, [])
|
||||||
CookieCloudChain().process(manual=True)
|
# 启动定时服务
|
||||||
|
Scheduler().start("cookiecloud", manual=True)
|
||||||
# 插件站点删除
|
# 插件站点删除
|
||||||
EventManager().send_event(EventType.SiteDeleted,
|
EventManager().send_event(EventType.SiteDeleted,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
from typing import List, Any, Optional
|
from typing import List, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, BackgroundTasks, Depends, HTTPException, Header
|
from fastapi import APIRouter, Request, BackgroundTasks, Depends, HTTPException, Header
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -12,6 +12,7 @@ from app.db import get_db
|
|||||||
from app.db.models.subscribe import Subscribe
|
from app.db.models.subscribe import Subscribe
|
||||||
from app.db.models.user import User
|
from app.db.models.user import User
|
||||||
from app.db.userauth import get_current_active_user
|
from app.db.userauth import get_current_active_user
|
||||||
|
from app.scheduler import Scheduler
|
||||||
from app.schemas.types import MediaType
|
from app.schemas.types import MediaType
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -26,13 +27,6 @@ def start_subscribe_add(db: Session, title: str, year: str,
|
|||||||
mtype=mtype, tmdbid=tmdbid, season=season, username=username)
|
mtype=mtype, tmdbid=tmdbid, season=season, username=username)
|
||||||
|
|
||||||
|
|
||||||
def start_subscribe_search(db: Session, sid: Optional[int], state: Optional[str]):
|
|
||||||
"""
|
|
||||||
启动订阅搜索任务
|
|
||||||
"""
|
|
||||||
SubscribeChain(db).search(sid=sid, state=state, manual=True)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", summary="所有订阅", response_model=List[schemas.Subscribe])
|
@router.get("/", summary="所有订阅", response_model=List[schemas.Subscribe])
|
||||||
def read_subscribes(
|
def read_subscribes(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -140,35 +134,36 @@ def subscribe_mediaid(
|
|||||||
|
|
||||||
@router.get("/refresh", summary="刷新订阅", response_model=schemas.Response)
|
@router.get("/refresh", summary="刷新订阅", response_model=schemas.Response)
|
||||||
def refresh_subscribes(
|
def refresh_subscribes(
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
刷新所有订阅
|
刷新所有订阅
|
||||||
"""
|
"""
|
||||||
SubscribeChain(db).refresh()
|
Scheduler().start("subscribe_refresh")
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/check", summary="刷新订阅 TMDB 信息", response_model=schemas.Response)
|
@router.get("/check", summary="刷新订阅 TMDB 信息", response_model=schemas.Response)
|
||||||
def check_subscribes(
|
def check_subscribes(
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
刷新所有订阅
|
刷新订阅 TMDB 信息
|
||||||
"""
|
"""
|
||||||
SubscribeChain(db).check()
|
Scheduler().start("subscribe_tmdb")
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/search", summary="搜索所有订阅", response_model=schemas.Response)
|
@router.get("/search", summary="搜索所有订阅", response_model=schemas.Response)
|
||||||
def search_subscribes(
|
def search_subscribes(
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
搜索所有订阅
|
搜索所有订阅
|
||||||
"""
|
"""
|
||||||
background_tasks.add_task(start_subscribe_search, db=db, sid=None, state='R')
|
background_tasks.add_task(
|
||||||
|
Scheduler().start,
|
||||||
|
job_id="subscribe_search",
|
||||||
|
sid=None, state='R'
|
||||||
|
)
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -176,12 +171,15 @@ def search_subscribes(
|
|||||||
def search_subscribe(
|
def search_subscribe(
|
||||||
subscribe_id: int,
|
subscribe_id: int,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
"""
|
"""
|
||||||
根据订阅编号搜索订阅
|
根据订阅编号搜索订阅
|
||||||
"""
|
"""
|
||||||
background_tasks.add_task(start_subscribe_search, db=db, sid=subscribe_id, state=None)
|
background_tasks.add_task(
|
||||||
|
Scheduler().start,
|
||||||
|
job_id="subscribe_search",
|
||||||
|
sid=subscribe_id, state=None
|
||||||
|
)
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import tailer
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
import tailer
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -16,6 +16,7 @@ from app.db import get_db
|
|||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.helper.message import MessageHelper
|
from app.helper.message import MessageHelper
|
||||||
from app.helper.progress import ProgressHelper
|
from app.helper.progress import ProgressHelper
|
||||||
|
from app.scheduler import Scheduler
|
||||||
from app.schemas.types import SystemConfigKey
|
from app.schemas.types import SystemConfigKey
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
@@ -211,3 +212,15 @@ def restart_system(_: schemas.TokenPayload = Depends(verify_token)):
|
|||||||
# 执行重启
|
# 执行重启
|
||||||
ret, msg = SystemUtils.restart()
|
ret, msg = SystemUtils.restart()
|
||||||
return schemas.Response(success=ret, message=msg)
|
return schemas.Response(success=ret, message=msg)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/runscheduler", summary="运行服务", response_model=schemas.Response)
|
||||||
|
def execute_command(jobid: str,
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)):
|
||||||
|
"""
|
||||||
|
执行命令
|
||||||
|
"""
|
||||||
|
if not jobid:
|
||||||
|
return schemas.Response(success=False, message="命令不能为空!")
|
||||||
|
Scheduler().start(jobid)
|
||||||
|
return schemas.Response(success=True)
|
||||||
|
|||||||
@@ -684,7 +684,7 @@ def arr_serie(apikey: str, tid: int, db: Session = Depends(get_db)) -> Any:
|
|||||||
"monitored": True,
|
"monitored": True,
|
||||||
}],
|
}],
|
||||||
year=subscribe.year,
|
year=subscribe.year,
|
||||||
remotePoster=subscribe.image,
|
remotePoster=subscribe.poster,
|
||||||
tmdbId=subscribe.tmdbid,
|
tmdbId=subscribe.tmdbid,
|
||||||
tvdbId=subscribe.tvdbid,
|
tvdbId=subscribe.tvdbid,
|
||||||
imdbId=subscribe.imdbid,
|
imdbId=subscribe.imdbid,
|
||||||
|
|||||||
@@ -336,14 +336,14 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
"""
|
"""
|
||||||
return self.run_module("media_exists", mediainfo=mediainfo, itemid=itemid)
|
return self.run_module("media_exists", mediainfo=mediainfo, itemid=itemid)
|
||||||
|
|
||||||
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]:
|
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
刷新媒体库
|
刷新媒体库
|
||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
:param file_path: 文件路径
|
:param file_path: 文件路径
|
||||||
:return: 成功或失败
|
:return: 成功或失败
|
||||||
"""
|
"""
|
||||||
return self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path)
|
self.run_module("refresh_mediaserver", mediainfo=mediainfo, file_path=file_path)
|
||||||
|
|
||||||
def post_message(self, message: Notification) -> None:
|
def post_message(self, message: Notification) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -391,22 +391,22 @@ class ChainBase(metaclass=ABCMeta):
|
|||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
:return: 成功或失败
|
:return: 成功或失败
|
||||||
"""
|
"""
|
||||||
return self.run_module("scrape_metadata", path=path, mediainfo=mediainfo)
|
self.run_module("scrape_metadata", path=path, mediainfo=mediainfo)
|
||||||
|
|
||||||
def register_commands(self, commands: Dict[str, dict]) -> None:
|
def register_commands(self, commands: Dict[str, dict]) -> None:
|
||||||
"""
|
"""
|
||||||
注册菜单命令
|
注册菜单命令
|
||||||
"""
|
"""
|
||||||
return self.run_module("register_commands", commands=commands)
|
self.run_module("register_commands", commands=commands)
|
||||||
|
|
||||||
def scheduler_job(self) -> None:
|
def scheduler_job(self) -> None:
|
||||||
"""
|
"""
|
||||||
定时任务,每10分钟调用一次,模块实现该接口以实现定时服务
|
定时任务,每10分钟调用一次,模块实现该接口以实现定时服务
|
||||||
"""
|
"""
|
||||||
return self.run_module("scheduler_job")
|
self.run_module("scheduler_job")
|
||||||
|
|
||||||
def clear_cache(self) -> None:
|
def clear_cache(self) -> None:
|
||||||
"""
|
"""
|
||||||
清理缓存,模块实现该接口响应清理缓存事件
|
清理缓存,模块实现该接口响应清理缓存事件
|
||||||
"""
|
"""
|
||||||
return self.run_module("clear_cache")
|
self.run_module("clear_cache")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
from typing import Tuple, Optional, Union
|
from typing import Tuple, Optional
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@@ -16,7 +16,6 @@ from app.helper.message import MessageHelper
|
|||||||
from app.helper.rss import RssHelper
|
from app.helper.rss import RssHelper
|
||||||
from app.helper.sites import SitesHelper
|
from app.helper.sites import SitesHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import Notification, NotificationType, MessageChannel
|
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.site import SiteUtils
|
from app.utils.site import SiteUtils
|
||||||
|
|
||||||
@@ -40,21 +39,6 @@ class CookieCloudChain(ChainBase):
|
|||||||
password=settings.COOKIECLOUD_PASSWORD
|
password=settings.COOKIECLOUD_PASSWORD
|
||||||
)
|
)
|
||||||
|
|
||||||
def remote_sync(self, channel: MessageChannel, userid: Union[int, str]):
|
|
||||||
"""
|
|
||||||
远程触发同步站点,发送消息
|
|
||||||
"""
|
|
||||||
self.post_message(Notification(channel=channel, mtype=NotificationType.SiteMessage,
|
|
||||||
title="开始同步CookieCloud站点 ...", userid=userid))
|
|
||||||
# 开始同步
|
|
||||||
success, msg = self.process()
|
|
||||||
if success:
|
|
||||||
self.post_message(Notification(channel=channel, mtype=NotificationType.SiteMessage,
|
|
||||||
title=f"同步站点成功,{msg}", userid=userid))
|
|
||||||
else:
|
|
||||||
self.post_message(Notification(channel=channel, mtype=NotificationType.SiteMessage,
|
|
||||||
title=f"同步站点失败:{msg}", userid=userid))
|
|
||||||
|
|
||||||
def process(self, manual=False) -> Tuple[bool, str]:
|
def process(self, manual=False) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
通过CookieCloud同步站点Cookie
|
通过CookieCloud同步站点Cookie
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
|
|
||||||
@@ -6,7 +8,7 @@ class DashboardChain(ChainBase):
|
|||||||
"""
|
"""
|
||||||
各类仪表板统计处理链
|
各类仪表板统计处理链
|
||||||
"""
|
"""
|
||||||
def media_statistic(self) -> schemas.Statistic:
|
def media_statistic(self) -> Optional[List[schemas.Statistic]]:
|
||||||
"""
|
"""
|
||||||
媒体数量统计
|
媒体数量统计
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Tuple, Set, Dict, Union
|
from typing import List, Optional, Tuple, Set, Dict, Union
|
||||||
@@ -15,6 +17,7 @@ from app.helper.torrent import TorrentHelper
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification
|
from app.schemas import ExistMediaInfo, NotExistMediaInfo, DownloadingTorrent, Notification
|
||||||
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType
|
from app.schemas.types import MediaType, TorrentStatus, EventType, MessageChannel, NotificationType
|
||||||
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -79,8 +82,68 @@ class DownloadChain(ChainBase):
|
|||||||
下载种子文件,如果是磁力链,会返回磁力链接本身
|
下载种子文件,如果是磁力链,会返回磁力链接本身
|
||||||
:return: 种子路径,种子目录名,种子文件清单
|
:return: 种子路径,种子目录名,种子文件清单
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __get_redict_url(url: str, ua: str = None, cookie: str = None) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
获取下载链接, url格式:[base64]url
|
||||||
|
"""
|
||||||
|
# 获取[]中的内容
|
||||||
|
m = re.search(r"\[(.*)](.*)", url)
|
||||||
|
if m:
|
||||||
|
# 参数
|
||||||
|
base64_str = m.group(1)
|
||||||
|
# URL
|
||||||
|
url = m.group(2)
|
||||||
|
if not base64_str:
|
||||||
|
return url
|
||||||
|
# 解码参数
|
||||||
|
req_str = base64.b64decode(base64_str.encode('utf-8')).decode('utf-8')
|
||||||
|
req_params: Dict[str, dict] = json.loads(req_str)
|
||||||
|
if req_params.get('method') == 'get':
|
||||||
|
# GET请求
|
||||||
|
res = RequestUtils(
|
||||||
|
ua=ua,
|
||||||
|
cookies=cookie
|
||||||
|
).get_res(url, params=req_params.get('params'))
|
||||||
|
else:
|
||||||
|
# POST请求
|
||||||
|
res = RequestUtils(
|
||||||
|
ua=ua,
|
||||||
|
cookies=cookie
|
||||||
|
).post_res(url, params=req_params.get('params'))
|
||||||
|
if not res:
|
||||||
|
return None
|
||||||
|
if not req_params.get('result'):
|
||||||
|
return res.text
|
||||||
|
else:
|
||||||
|
data = res.json()
|
||||||
|
for key in str(req_params.get('result')).split("."):
|
||||||
|
data = data.get(key)
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
logger.info(f"获取到下载地址:{data}")
|
||||||
|
return data
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 获取下载链接
|
||||||
|
if not torrent.enclosure:
|
||||||
|
return None, "", []
|
||||||
|
if torrent.enclosure.startswith("magnet:"):
|
||||||
|
return torrent.enclosure, "", []
|
||||||
|
|
||||||
|
if torrent.enclosure.startswith("["):
|
||||||
|
# 需要解码获取下载地址
|
||||||
|
torrent_url = __get_redict_url(url=torrent.enclosure,
|
||||||
|
ua=torrent.site_ua,
|
||||||
|
cookie=torrent.site_cookie)
|
||||||
|
else:
|
||||||
|
torrent_url = torrent.enclosure
|
||||||
|
if not torrent_url:
|
||||||
|
logger.error(f"{torrent.title} 无法获取下载地址:{torrent.enclosure}!")
|
||||||
|
return None, "", []
|
||||||
|
# 下载种子文件
|
||||||
torrent_file, content, download_folder, files, error_msg = self.torrent.download_torrent(
|
torrent_file, content, download_folder, files, error_msg = self.torrent.download_torrent(
|
||||||
url=torrent.enclosure,
|
url=torrent_url,
|
||||||
cookie=torrent.site_cookie,
|
cookie=torrent.site_cookie,
|
||||||
ua=torrent.site_ua,
|
ua=torrent.site_ua,
|
||||||
proxy=torrent.site_proxy)
|
proxy=torrent.site_proxy)
|
||||||
@@ -90,7 +153,7 @@ class DownloadChain(ChainBase):
|
|||||||
return content, "", []
|
return content, "", []
|
||||||
|
|
||||||
if not torrent_file:
|
if not torrent_file:
|
||||||
logger.error(f"下载种子文件失败:{torrent.title} - {torrent.enclosure}")
|
logger.error(f"下载种子文件失败:{torrent.title} - {torrent_url}")
|
||||||
self.post_message(Notification(
|
self.post_message(Notification(
|
||||||
channel=channel,
|
channel=channel,
|
||||||
mtype=NotificationType.Manual,
|
mtype=NotificationType.Manual,
|
||||||
@@ -122,7 +185,9 @@ class DownloadChain(ChainBase):
|
|||||||
_folder_name = ""
|
_folder_name = ""
|
||||||
if not torrent_file:
|
if not torrent_file:
|
||||||
# 下载种子文件,得到的可能是文件也可能是磁力链
|
# 下载种子文件,得到的可能是文件也可能是磁力链
|
||||||
content, _folder_name, _file_list = self.download_torrent(_torrent, userid=userid)
|
content, _folder_name, _file_list = self.download_torrent(_torrent,
|
||||||
|
channel=channel,
|
||||||
|
userid=userid)
|
||||||
if not content:
|
if not content:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@@ -253,12 +318,14 @@ class DownloadChain(ChainBase):
|
|||||||
contexts: List[Context],
|
contexts: List[Context],
|
||||||
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
||||||
save_path: str = None,
|
save_path: str = None,
|
||||||
|
channel: str = None,
|
||||||
userid: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
|
userid: str = None) -> Tuple[List[Context], Dict[int, Dict[int, NotExistMediaInfo]]]:
|
||||||
"""
|
"""
|
||||||
根据缺失数据,自动种子列表中组合择优下载
|
根据缺失数据,自动种子列表中组合择优下载
|
||||||
:param contexts: 资源上下文列表
|
:param contexts: 资源上下文列表
|
||||||
:param no_exists: 缺失的剧集信息
|
:param no_exists: 缺失的剧集信息
|
||||||
:param save_path: 保存路径
|
:param save_path: 保存路径
|
||||||
|
:param channel: 通知渠道
|
||||||
:param userid: 用户ID
|
:param userid: 用户ID
|
||||||
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id] = {season: NotExistMediaInfo}
|
:return: 已经下载的资源列表、剩余未下载到的剧集 no_exists[tmdb_id] = {season: NotExistMediaInfo}
|
||||||
"""
|
"""
|
||||||
@@ -323,7 +390,8 @@ class DownloadChain(ChainBase):
|
|||||||
# 如果是电影,直接下载
|
# 如果是电影,直接下载
|
||||||
for context in contexts:
|
for context in contexts:
|
||||||
if context.media_info.type == MediaType.MOVIE:
|
if context.media_info.type == MediaType.MOVIE:
|
||||||
if self.download_single(context, save_path=save_path, userid=userid):
|
if self.download_single(context, save_path=save_path,
|
||||||
|
channel=channel, userid=userid):
|
||||||
# 下载成功
|
# 下载成功
|
||||||
downloaded_list.append(context)
|
downloaded_list.append(context)
|
||||||
|
|
||||||
@@ -390,11 +458,13 @@ class DownloadChain(ChainBase):
|
|||||||
context=context,
|
context=context,
|
||||||
torrent_file=content if isinstance(content, Path) else None,
|
torrent_file=content if isinstance(content, Path) else None,
|
||||||
save_path=save_path,
|
save_path=save_path,
|
||||||
|
channel=channel,
|
||||||
userid=userid
|
userid=userid
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 下载
|
# 下载
|
||||||
download_id = self.download_single(context, save_path=save_path, userid=userid)
|
download_id = self.download_single(context, save_path=save_path,
|
||||||
|
channel=channel, userid=userid)
|
||||||
|
|
||||||
if download_id:
|
if download_id:
|
||||||
# 下载成功
|
# 下载成功
|
||||||
@@ -452,7 +522,8 @@ class DownloadChain(ChainBase):
|
|||||||
# 为需要集的子集则下载
|
# 为需要集的子集则下载
|
||||||
if torrent_episodes.issubset(set(need_episodes)):
|
if torrent_episodes.issubset(set(need_episodes)):
|
||||||
# 下载
|
# 下载
|
||||||
download_id = self.download_single(context, save_path=save_path, userid=userid)
|
download_id = self.download_single(context, save_path=save_path,
|
||||||
|
channel=channel, userid=userid)
|
||||||
if download_id:
|
if download_id:
|
||||||
# 下载成功
|
# 下载成功
|
||||||
downloaded_list.append(context)
|
downloaded_list.append(context)
|
||||||
@@ -508,7 +579,7 @@ class DownloadChain(ChainBase):
|
|||||||
and len(meta.season_list) == 1 \
|
and len(meta.season_list) == 1 \
|
||||||
and meta.season_list[0] == need_season:
|
and meta.season_list[0] == need_season:
|
||||||
# 检查种子看是否有需要的集
|
# 检查种子看是否有需要的集
|
||||||
content, _, torrent_files = self.download_torrent(torrent, userid=userid)
|
content, _, torrent_files = self.download_torrent(torrent)
|
||||||
if not content:
|
if not content:
|
||||||
continue
|
continue
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
@@ -529,6 +600,7 @@ class DownloadChain(ChainBase):
|
|||||||
torrent_file=content if isinstance(content, Path) else None,
|
torrent_file=content if isinstance(content, Path) else None,
|
||||||
episodes=selected_episodes,
|
episodes=selected_episodes,
|
||||||
save_path=save_path,
|
save_path=save_path,
|
||||||
|
channel=channel,
|
||||||
userid=userid
|
userid=userid
|
||||||
)
|
)
|
||||||
if not download_id:
|
if not download_id:
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from app.core.config import settings
|
|||||||
from app.db import SessionFactory
|
from app.db import SessionFactory
|
||||||
from app.db.mediaserver_oper import MediaServerOper
|
from app.db.mediaserver_oper import MediaServerOper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import MessageChannel, Notification
|
|
||||||
|
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
|
|
||||||
@@ -23,33 +22,23 @@ class MediaServerChain(ChainBase):
|
|||||||
def __init__(self, db: Session = None):
|
def __init__(self, db: Session = None):
|
||||||
super().__init__(db)
|
super().__init__(db)
|
||||||
|
|
||||||
def librarys(self) -> List[schemas.MediaServerLibrary]:
|
def librarys(self, server: str) -> List[schemas.MediaServerLibrary]:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器所有媒体库
|
获取媒体服务器所有媒体库
|
||||||
"""
|
"""
|
||||||
return self.run_module("mediaserver_librarys")
|
return self.run_module("mediaserver_librarys", server=server)
|
||||||
|
|
||||||
def items(self, library_id: Union[str, int]) -> Generator:
|
def items(self, server: str, library_id: Union[str, int]) -> Generator:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器所有项目
|
获取媒体服务器所有项目
|
||||||
"""
|
"""
|
||||||
return self.run_module("mediaserver_items", library_id=library_id)
|
return self.run_module("mediaserver_items", server=server, library_id=library_id)
|
||||||
|
|
||||||
def episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
def episodes(self, server: str, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
||||||
"""
|
"""
|
||||||
获取媒体服务器剧集信息
|
获取媒体服务器剧集信息
|
||||||
"""
|
"""
|
||||||
return self.run_module("mediaserver_tv_episodes", item_id=item_id)
|
return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id)
|
||||||
|
|
||||||
def remote_sync(self, channel: MessageChannel, userid: Union[int, str]):
|
|
||||||
"""
|
|
||||||
同步豆瓣想看数据,发送消息
|
|
||||||
"""
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title="开始媒体服务器 ...", userid=userid))
|
|
||||||
self.sync()
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title="同步媒体服务器完成!", userid=userid))
|
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
"""
|
"""
|
||||||
@@ -59,7 +48,6 @@ class MediaServerChain(ChainBase):
|
|||||||
# 媒体服务器同步使用独立的会话
|
# 媒体服务器同步使用独立的会话
|
||||||
_db = SessionFactory()
|
_db = SessionFactory()
|
||||||
_dbOper = MediaServerOper(_db)
|
_dbOper = MediaServerOper(_db)
|
||||||
logger.info("开始同步媒体库数据 ...")
|
|
||||||
# 汇总统计
|
# 汇总统计
|
||||||
total_count = 0
|
total_count = 0
|
||||||
# 清空登记薄
|
# 清空登记薄
|
||||||
@@ -67,35 +55,42 @@ class MediaServerChain(ChainBase):
|
|||||||
# 同步黑名单
|
# 同步黑名单
|
||||||
sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split(
|
sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split(
|
||||||
",") if settings.MEDIASERVER_SYNC_BLACKLIST else []
|
",") if settings.MEDIASERVER_SYNC_BLACKLIST else []
|
||||||
for library in self.librarys():
|
# 设置的媒体服务器
|
||||||
if library.name in sync_blacklist:
|
if not settings.MEDIASERVER:
|
||||||
|
return
|
||||||
|
mediaservers = settings.MEDIASERVER.split(",")
|
||||||
|
# 遍历媒体服务器
|
||||||
|
for mediaserver in mediaservers:
|
||||||
|
logger.info(f"开始同步媒体库 {mediaserver} 的数据 ...")
|
||||||
|
for library in self.librarys(mediaserver):
|
||||||
# 同步黑名单 跳过
|
# 同步黑名单 跳过
|
||||||
continue
|
if library.name in sync_blacklist:
|
||||||
logger.info(f"正在同步媒体库 {library.name} ...")
|
|
||||||
library_count = 0
|
|
||||||
for item in self.items(library.id):
|
|
||||||
if not item:
|
|
||||||
continue
|
continue
|
||||||
if not item.item_id:
|
logger.info(f"正在同步 {mediaserver} 媒体库 {library.name} ...")
|
||||||
continue
|
library_count = 0
|
||||||
# 计数
|
for item in self.items(mediaserver, library.id):
|
||||||
library_count += 1
|
if not item:
|
||||||
seasoninfo = {}
|
continue
|
||||||
# 类型
|
if not item.item_id:
|
||||||
item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影"
|
continue
|
||||||
if item_type == "电视剧":
|
# 计数
|
||||||
# 查询剧集信息
|
library_count += 1
|
||||||
espisodes_info = self.episodes(item.item_id) or []
|
seasoninfo = {}
|
||||||
for episode in espisodes_info:
|
# 类型
|
||||||
seasoninfo[episode.season] = episode.episodes
|
item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影"
|
||||||
# 插入数据
|
if item_type == "电视剧":
|
||||||
item_dict = item.dict()
|
# 查询剧集信息
|
||||||
item_dict['seasoninfo'] = json.dumps(seasoninfo)
|
espisodes_info = self.episodes(mediaserver, item.item_id) or []
|
||||||
item_dict['item_type'] = item_type
|
for episode in espisodes_info:
|
||||||
_dbOper.add(**item_dict)
|
seasoninfo[episode.season] = episode.episodes
|
||||||
logger.info(f"媒体库 {library.name} 同步完成,共同步数量:{library_count}")
|
# 插入数据
|
||||||
# 总数累加
|
item_dict = item.dict()
|
||||||
total_count += library_count
|
item_dict['seasoninfo'] = json.dumps(seasoninfo)
|
||||||
|
item_dict['item_type'] = item_type
|
||||||
|
_dbOper.add(**item_dict)
|
||||||
|
logger.info(f"{mediaserver} 媒体库 {library.name} 同步完成,共同步数量:{library_count}")
|
||||||
|
# 总数累加
|
||||||
|
total_count += library_count
|
||||||
# 关闭数据库连接
|
# 关闭数据库连接
|
||||||
if _db:
|
if _db:
|
||||||
_db.close()
|
_db.close()
|
||||||
|
|||||||
@@ -348,6 +348,7 @@ class MessageChain(ChainBase):
|
|||||||
# 批量下载
|
# 批量下载
|
||||||
downloads, lefts = self.downloadchain.batch_download(contexts=cache_list,
|
downloads, lefts = self.downloadchain.batch_download(contexts=cache_list,
|
||||||
no_exists=no_exists,
|
no_exists=no_exists,
|
||||||
|
channel=channel,
|
||||||
userid=userid)
|
userid=userid)
|
||||||
if downloads and not lefts:
|
if downloads and not lefts:
|
||||||
# 全部下载完成
|
# 全部下载完成
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ class SearchChain(ChainBase):
|
|||||||
keyword: str = None,
|
keyword: str = None,
|
||||||
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
no_exists: Dict[int, Dict[int, NotExistMediaInfo]] = None,
|
||||||
sites: List[int] = None,
|
sites: List[int] = None,
|
||||||
filter_rule: str = None,
|
priority_rule: str = None,
|
||||||
|
filter_rule: Dict[str, str] = None,
|
||||||
area: str = "title") -> List[Context]:
|
area: str = "title") -> List[Context]:
|
||||||
"""
|
"""
|
||||||
根据媒体信息搜索种子资源,精确匹配,应用过滤规则,同时根据no_exists过滤本地已存在的资源
|
根据媒体信息搜索种子资源,精确匹配,应用过滤规则,同时根据no_exists过滤本地已存在的资源
|
||||||
@@ -89,7 +90,8 @@ class SearchChain(ChainBase):
|
|||||||
:param keyword: 搜索关键词
|
:param keyword: 搜索关键词
|
||||||
:param no_exists: 缺失的媒体信息
|
:param no_exists: 缺失的媒体信息
|
||||||
:param sites: 站点ID列表,为空时搜索所有站点
|
:param sites: 站点ID列表,为空时搜索所有站点
|
||||||
:param filter_rule: 过滤规则,为空是使用默认搜索过滤规则
|
:param priority_rule: 优先级规则,为空时使用搜索优先级规则
|
||||||
|
:param filter_rule: 过滤规则,为空是使用默认过滤规则
|
||||||
:param area: 搜索范围,title or imdbid
|
:param area: 搜索范围,title or imdbid
|
||||||
"""
|
"""
|
||||||
logger.info(f'开始搜索资源,关键词:{keyword or mediainfo.title} ...')
|
logger.info(f'开始搜索资源,关键词:{keyword or mediainfo.title} ...')
|
||||||
@@ -129,12 +131,12 @@ class SearchChain(ChainBase):
|
|||||||
logger.warn(f'{keyword or mediainfo.title} 未搜索到资源')
|
logger.warn(f'{keyword or mediainfo.title} 未搜索到资源')
|
||||||
return []
|
return []
|
||||||
# 过滤种子
|
# 过滤种子
|
||||||
if filter_rule is None:
|
if priority_rule is None:
|
||||||
# 取搜索优先级规则
|
# 取搜索优先级规则
|
||||||
filter_rule = self.systemconfig.get(SystemConfigKey.SearchFilterRules)
|
priority_rule = self.systemconfig.get(SystemConfigKey.SearchFilterRules)
|
||||||
if filter_rule:
|
if priority_rule:
|
||||||
logger.info(f'开始过滤资源,当前规则:{filter_rule} ...')
|
logger.info(f'开始过滤资源,当前规则:{priority_rule} ...')
|
||||||
result: List[TorrentInfo] = self.filter_torrents(rule_string=filter_rule,
|
result: List[TorrentInfo] = self.filter_torrents(rule_string=priority_rule,
|
||||||
torrent_list=torrents,
|
torrent_list=torrents,
|
||||||
season_episodes=season_episodes,
|
season_episodes=season_episodes,
|
||||||
mediainfo=mediainfo)
|
mediainfo=mediainfo)
|
||||||
@@ -144,9 +146,10 @@ class SearchChain(ChainBase):
|
|||||||
logger.warn(f'{keyword or mediainfo.title} 没有符合优先级规则的资源')
|
logger.warn(f'{keyword or mediainfo.title} 没有符合优先级规则的资源')
|
||||||
return []
|
return []
|
||||||
# 使用默认过滤规则再次过滤
|
# 使用默认过滤规则再次过滤
|
||||||
torrents = self.filter_torrents_by_default_rule(torrents)
|
torrents = self.filter_torrents_by_rule(torrents=torrents,
|
||||||
|
filter_rule=filter_rule)
|
||||||
if not torrents:
|
if not torrents:
|
||||||
logger.warn(f'{keyword or mediainfo.title} 没有符合默认过滤规则的资源')
|
logger.warn(f'{keyword or mediainfo.title} 没有符合过滤规则的资源')
|
||||||
return []
|
return []
|
||||||
# 匹配的资源
|
# 匹配的资源
|
||||||
_match_torrents = []
|
_match_torrents = []
|
||||||
@@ -314,28 +317,43 @@ class SearchChain(ChainBase):
|
|||||||
# 返回
|
# 返回
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def filter_torrents_by_default_rule(self,
|
def filter_torrents_by_rule(self,
|
||||||
torrents: List[TorrentInfo]) -> List[TorrentInfo]:
|
torrents: List[TorrentInfo],
|
||||||
|
filter_rule: Dict[str, str] = None
|
||||||
|
) -> List[TorrentInfo]:
|
||||||
|
"""
|
||||||
|
使用过滤规则过滤种子
|
||||||
|
:param torrents: 种子列表
|
||||||
|
:param filter_rule: 过滤规则
|
||||||
|
"""
|
||||||
|
|
||||||
# 取默认过滤规则
|
# 取默认过滤规则
|
||||||
default_include_exclude = self.systemconfig.get(SystemConfigKey.DefaultIncludeExcludeFilter) or {}
|
if not filter_rule:
|
||||||
if not default_include_exclude:
|
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
|
||||||
|
if not filter_rule:
|
||||||
return torrents
|
return torrents
|
||||||
include = default_include_exclude.get("include")
|
# 包含
|
||||||
exclude = default_include_exclude.get("exclude")
|
include = filter_rule.get("include")
|
||||||
new_torrents = []
|
# 排除
|
||||||
for torrent in torrents:
|
exclude = filter_rule.get("exclude")
|
||||||
|
|
||||||
|
def __filter_torrent(t: TorrentInfo) -> bool:
|
||||||
|
"""
|
||||||
|
过滤种子
|
||||||
|
"""
|
||||||
# 包含
|
# 包含
|
||||||
if include:
|
if include:
|
||||||
if not re.search(r"%s" % include,
|
if not re.search(r"%s" % include,
|
||||||
f"{torrent.title} {torrent.description}", re.I):
|
f"{t.title} {t.description}", re.I):
|
||||||
logger.info(f"{torrent.title} 不匹配包含规则 {include}")
|
logger.info(f"{t.title} 不匹配包含规则 {include}")
|
||||||
continue
|
return False
|
||||||
# 排除
|
# 排除
|
||||||
if exclude:
|
if exclude:
|
||||||
if re.search(r"%s" % exclude,
|
if re.search(r"%s" % exclude,
|
||||||
f"{torrent.title} {torrent.description}", re.I):
|
f"{t.title} {t.description}", re.I):
|
||||||
logger.info(f"{torrent.title} 匹配排除规则 {exclude}")
|
logger.info(f"{t.title} 匹配排除规则 {exclude}")
|
||||||
continue
|
return False
|
||||||
new_torrents.append(torrent)
|
return True
|
||||||
return new_torrents
|
|
||||||
|
# 使用默认过滤规则再次过滤
|
||||||
|
return list(filter(lambda t: __filter_torrent(t), torrents))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
from typing import Union, Tuple
|
from typing import Union, Tuple
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -28,6 +29,66 @@ class SiteChain(ChainBase):
|
|||||||
self.cookiehelper = CookieHelper()
|
self.cookiehelper = CookieHelper()
|
||||||
self.message = MessageHelper()
|
self.message = MessageHelper()
|
||||||
|
|
||||||
|
# 特殊站点登录验证
|
||||||
|
self.special_site_test = {
|
||||||
|
"zhuque.in": self.__zhuque_test,
|
||||||
|
# "m-team.io": self.__mteam_test,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __zhuque_test(site: Site) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
判断站点是否已经登陆:zhuique
|
||||||
|
"""
|
||||||
|
# 获取token
|
||||||
|
token = None
|
||||||
|
res = RequestUtils(
|
||||||
|
ua=site.ua,
|
||||||
|
cookies=site.cookie,
|
||||||
|
proxies=settings.PROXY if site.proxy else None,
|
||||||
|
timeout=15
|
||||||
|
).get_res(url=site.url)
|
||||||
|
if res and res.status_code == 200:
|
||||||
|
csrf_token = re.search(r'<meta name="x-csrf-token" content="(.+?)">', res.text)
|
||||||
|
if csrf_token:
|
||||||
|
token = csrf_token.group(1)
|
||||||
|
if not token:
|
||||||
|
return False, "无法获取Token"
|
||||||
|
# 调用查询用户信息接口
|
||||||
|
user_res = RequestUtils(
|
||||||
|
headers={
|
||||||
|
'X-CSRF-TOKEN': token,
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"User-Agent": f"{site.ua}"
|
||||||
|
},
|
||||||
|
cookies=site.cookie,
|
||||||
|
proxies=settings.PROXY if site.proxy else None,
|
||||||
|
timeout=15
|
||||||
|
).get_res(url=f"{site.url}api/user/getInfo")
|
||||||
|
if user_res and user_res.status_code == 200:
|
||||||
|
user_info = user_res.json()
|
||||||
|
if user_info and user_info.get("data"):
|
||||||
|
return True, "连接成功"
|
||||||
|
return False, "Cookie已失效"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __mteam_test(site: Site) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
判断站点是否已经登陆:m-team
|
||||||
|
"""
|
||||||
|
url = f"{site.url}api/member/profile"
|
||||||
|
res = RequestUtils(
|
||||||
|
ua=site.ua,
|
||||||
|
cookies=site.cookie,
|
||||||
|
proxies=settings.PROXY if site.proxy else None,
|
||||||
|
timeout=15
|
||||||
|
).post_res(url=url)
|
||||||
|
if res and res.status_code == 200:
|
||||||
|
user_info = res.json()
|
||||||
|
if user_info and user_info.get("data"):
|
||||||
|
return True, "连接成功"
|
||||||
|
return False, "Cookie已失效"
|
||||||
|
|
||||||
def test(self, url: str) -> Tuple[bool, str]:
|
def test(self, url: str) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
测试站点是否可用
|
测试站点是否可用
|
||||||
@@ -39,6 +100,12 @@ class SiteChain(ChainBase):
|
|||||||
site_info = self.siteoper.get_by_domain(domain)
|
site_info = self.siteoper.get_by_domain(domain)
|
||||||
if not site_info:
|
if not site_info:
|
||||||
return False, f"站点【{url}】不存在"
|
return False, f"站点【{url}】不存在"
|
||||||
|
|
||||||
|
# 特殊站点测试
|
||||||
|
if self.special_site_test.get(domain):
|
||||||
|
return self.special_site_test[domain](site_info)
|
||||||
|
|
||||||
|
# 通用站点测试
|
||||||
site_url = site_info.url
|
site_url = site_info.url
|
||||||
site_cookie = site_info.cookie
|
site_cookie = site_info.cookie
|
||||||
ua = site_info.ua
|
ua = site_info.ua
|
||||||
|
|||||||
@@ -132,45 +132,6 @@ class SubscribeChain(ChainBase):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remote_refresh(self, channel: MessageChannel, userid: Union[str, int] = None):
|
|
||||||
"""
|
|
||||||
远程刷新订阅,发送消息
|
|
||||||
"""
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title=f"开始刷新订阅 ...", userid=userid))
|
|
||||||
self.refresh()
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title=f"订阅刷新完成!", userid=userid))
|
|
||||||
|
|
||||||
def remote_search(self, arg_str: str, channel: MessageChannel, userid: Union[str, int] = None):
|
|
||||||
"""
|
|
||||||
远程搜索订阅,发送消息
|
|
||||||
"""
|
|
||||||
if arg_str and not str(arg_str).isdigit():
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title="请输入正确的命令格式:/subscribe_search [id],"
|
|
||||||
"[id]为订阅编号,不输入订阅编号时搜索所有订阅", userid=userid))
|
|
||||||
return
|
|
||||||
if arg_str:
|
|
||||||
sid = int(arg_str)
|
|
||||||
subscribe = self.subscribeoper.get(sid)
|
|
||||||
if not subscribe:
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title=f"订阅编号 {sid} 不存在!", userid=userid))
|
|
||||||
return
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title=f"开始搜索 {subscribe.name} ...", userid=userid))
|
|
||||||
# 搜索订阅
|
|
||||||
self.search(sid=int(arg_str))
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title=f"{subscribe.name} 搜索完成!", userid=userid))
|
|
||||||
else:
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title=f"开始搜索所有订阅 ...", userid=userid))
|
|
||||||
self.search(state='R')
|
|
||||||
self.post_message(Notification(channel=channel,
|
|
||||||
title=f"订阅搜索完成!", userid=userid))
|
|
||||||
|
|
||||||
def search(self, sid: int = None, state: str = 'N', manual: bool = False):
|
def search(self, sid: int = None, state: str = 'N', manual: bool = False):
|
||||||
"""
|
"""
|
||||||
订阅搜索
|
订阅搜索
|
||||||
@@ -262,16 +223,25 @@ class SubscribeChain(ChainBase):
|
|||||||
sites = json.loads(subscribe.sites)
|
sites = json.loads(subscribe.sites)
|
||||||
else:
|
else:
|
||||||
sites = None
|
sites = None
|
||||||
# 过滤规则
|
# 优先级过滤规则
|
||||||
if subscribe.best_version:
|
if subscribe.best_version:
|
||||||
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
priority_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
||||||
else:
|
else:
|
||||||
filter_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
|
priority_rule = self.systemconfig.get(SystemConfigKey.SubscribeFilterRules)
|
||||||
|
# 默认过滤规则
|
||||||
|
if subscribe.include or subscribe.exclude:
|
||||||
|
filter_rule = {
|
||||||
|
"include": subscribe.include,
|
||||||
|
"exclude": subscribe.exclude
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
filter_rule = self.systemconfig.get(SystemConfigKey.DefaultFilterRules)
|
||||||
# 搜索,同时电视剧会过滤掉不需要的剧集
|
# 搜索,同时电视剧会过滤掉不需要的剧集
|
||||||
contexts = self.searchchain.process(mediainfo=mediainfo,
|
contexts = self.searchchain.process(mediainfo=mediainfo,
|
||||||
keyword=subscribe.keyword,
|
keyword=subscribe.keyword,
|
||||||
no_exists=no_exists,
|
no_exists=no_exists,
|
||||||
sites=sites,
|
sites=sites,
|
||||||
|
priority_rule=priority_rule,
|
||||||
filter_rule=filter_rule)
|
filter_rule=filter_rule)
|
||||||
if not contexts:
|
if not contexts:
|
||||||
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
|
logger.warn(f'订阅 {subscribe.keyword or subscribe.name} 未搜索到资源')
|
||||||
@@ -477,10 +447,10 @@ class SubscribeChain(ChainBase):
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
no_exists = {}
|
no_exists = {}
|
||||||
# 包含与排除规则
|
# 默认过滤规则
|
||||||
default_include_exclude = self.systemconfig.get(SystemConfigKey.DefaultIncludeExcludeFilter) or {}
|
default_filter = self.systemconfig.get(SystemConfigKey.DefaultFilterRules) or {}
|
||||||
include = subscribe.include or default_include_exclude.get("include")
|
include = subscribe.include or default_filter.get("include")
|
||||||
exclude = subscribe.exclude or default_include_exclude.get("exclude")
|
exclude = subscribe.exclude or default_filter.get("exclude")
|
||||||
# 遍历缓存种子
|
# 遍历缓存种子
|
||||||
_match_context = []
|
_match_context = []
|
||||||
for domain, contexts in torrents.items():
|
for domain, contexts in torrents.items():
|
||||||
@@ -493,7 +463,7 @@ class SubscribeChain(ChainBase):
|
|||||||
if torrent_mediainfo.tmdb_id != mediainfo.tmdb_id \
|
if torrent_mediainfo.tmdb_id != mediainfo.tmdb_id \
|
||||||
or torrent_mediainfo.type != mediainfo.type:
|
or torrent_mediainfo.type != mediainfo.type:
|
||||||
continue
|
continue
|
||||||
# 过滤规则
|
# 优先级过滤规则
|
||||||
if subscribe.best_version:
|
if subscribe.best_version:
|
||||||
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
filter_rule = self.systemconfig.get(SystemConfigKey.BestVersionFilterRules)
|
||||||
else:
|
else:
|
||||||
|
|||||||
125
app/command.py
125
app/command.py
@@ -1,11 +1,9 @@
|
|||||||
import traceback
|
import traceback
|
||||||
from threading import Thread, Event
|
from threading import Thread, Event
|
||||||
from typing import Any, Union
|
from typing import Any, Union, Dict
|
||||||
|
|
||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
from app.chain.cookiecloud import CookieCloudChain
|
|
||||||
from app.chain.download import DownloadChain
|
from app.chain.download import DownloadChain
|
||||||
from app.chain.mediaserver import MediaServerChain
|
|
||||||
from app.chain.site import SiteChain
|
from app.chain.site import SiteChain
|
||||||
from app.chain.subscribe import SubscribeChain
|
from app.chain.subscribe import SubscribeChain
|
||||||
from app.chain.system import SystemChain
|
from app.chain.system import SystemChain
|
||||||
@@ -15,6 +13,8 @@ from app.core.event import eventmanager, EventManager
|
|||||||
from app.core.plugin import PluginManager
|
from app.core.plugin import PluginManager
|
||||||
from app.db import SessionFactory
|
from app.db import SessionFactory
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
|
from app.scheduler import Scheduler
|
||||||
|
from app.schemas import Notification
|
||||||
from app.schemas.types import EventType, MessageChannel
|
from app.schemas.types import EventType, MessageChannel
|
||||||
from app.utils.object import ObjectUtils
|
from app.utils.object import ObjectUtils
|
||||||
from app.utils.singleton import Singleton
|
from app.utils.singleton import Singleton
|
||||||
@@ -49,13 +49,15 @@ class Command(metaclass=Singleton):
|
|||||||
self.pluginmanager = PluginManager()
|
self.pluginmanager = PluginManager()
|
||||||
# 处理链
|
# 处理链
|
||||||
self.chain = CommandChian(self._db)
|
self.chain = CommandChian(self._db)
|
||||||
|
# 定时服务管理
|
||||||
|
self.scheduler = Scheduler()
|
||||||
# 内置命令
|
# 内置命令
|
||||||
self._commands = {
|
self._commands = {
|
||||||
"/cookiecloud": {
|
"/cookiecloud": {
|
||||||
"func": CookieCloudChain(self._db).remote_sync,
|
"id": "cookiecloud",
|
||||||
|
"type": "scheduler",
|
||||||
"description": "同步站点",
|
"description": "同步站点",
|
||||||
"category": "站点",
|
"category": "站点"
|
||||||
"data": {}
|
|
||||||
},
|
},
|
||||||
"/sites": {
|
"/sites": {
|
||||||
"func": SiteChain(self._db).remote_list,
|
"func": SiteChain(self._db).remote_list,
|
||||||
@@ -79,10 +81,10 @@ class Command(metaclass=Singleton):
|
|||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
"/mediaserver_sync": {
|
"/mediaserver_sync": {
|
||||||
"func": MediaServerChain(self._db).remote_sync,
|
"id": "mediaserver_sync",
|
||||||
|
"type": "scheduler",
|
||||||
"description": "同步媒体服务器",
|
"description": "同步媒体服务器",
|
||||||
"category": "管理",
|
"category": "管理"
|
||||||
"data": {}
|
|
||||||
},
|
},
|
||||||
"/subscribes": {
|
"/subscribes": {
|
||||||
"func": SubscribeChain(self._db).remote_list,
|
"func": SubscribeChain(self._db).remote_list,
|
||||||
@@ -91,22 +93,27 @@ class Command(metaclass=Singleton):
|
|||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
"/subscribe_refresh": {
|
"/subscribe_refresh": {
|
||||||
"func": SubscribeChain(self._db).remote_refresh,
|
"id": "subscribe_refresh",
|
||||||
|
"type": "scheduler",
|
||||||
"description": "刷新订阅",
|
"description": "刷新订阅",
|
||||||
"category": "订阅",
|
"category": "订阅"
|
||||||
"data": {}
|
|
||||||
},
|
},
|
||||||
"/subscribe_search": {
|
"/subscribe_search": {
|
||||||
"func": SubscribeChain(self._db).remote_search,
|
"id": "subscribe_search",
|
||||||
|
"type": "scheduler",
|
||||||
"description": "搜索订阅",
|
"description": "搜索订阅",
|
||||||
"category": "订阅",
|
"category": "订阅"
|
||||||
"data": {}
|
|
||||||
},
|
},
|
||||||
"/subscribe_delete": {
|
"/subscribe_delete": {
|
||||||
"func": SubscribeChain(self._db).remote_delete,
|
"func": SubscribeChain(self._db).remote_delete,
|
||||||
"description": "删除订阅",
|
"description": "删除订阅",
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
|
"/subscribe_tmdb": {
|
||||||
|
"id": "subscribe_tmdb",
|
||||||
|
"type": "scheduler",
|
||||||
|
"description": "订阅元数据更新"
|
||||||
|
},
|
||||||
"/downloading": {
|
"/downloading": {
|
||||||
"func": DownloadChain(self._db).remote_downloading,
|
"func": DownloadChain(self._db).remote_downloading,
|
||||||
"description": "正在下载",
|
"description": "正在下载",
|
||||||
@@ -114,10 +121,10 @@ class Command(metaclass=Singleton):
|
|||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
"/transfer": {
|
"/transfer": {
|
||||||
"func": TransferChain(self._db).process,
|
"id": "transfer",
|
||||||
|
"type": "scheduler",
|
||||||
"description": "下载文件整理",
|
"description": "下载文件整理",
|
||||||
"category": "管理",
|
"category": "管理"
|
||||||
"data": {}
|
|
||||||
},
|
},
|
||||||
"/redo": {
|
"/redo": {
|
||||||
"func": TransferChain(self._db).remote_transfer,
|
"func": TransferChain(self._db).remote_transfer,
|
||||||
@@ -175,6 +182,56 @@ class Command(metaclass=Singleton):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")
|
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")
|
||||||
|
|
||||||
|
def __run_command(self, command: Dict[str, any],
|
||||||
|
data_str: str = "",
|
||||||
|
channel: MessageChannel = None, userid: Union[str, int] = None):
|
||||||
|
"""
|
||||||
|
运行定时服务
|
||||||
|
"""
|
||||||
|
if command.get("type") == "scheduler":
|
||||||
|
# 定时服务
|
||||||
|
if userid:
|
||||||
|
self.chain.post_message(
|
||||||
|
Notification(
|
||||||
|
channel=channel,
|
||||||
|
title=f"开始执行 {command.get('description')} ...",
|
||||||
|
userid=userid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 执行定时任务
|
||||||
|
self.scheduler.start(job_id=command.get("id"))
|
||||||
|
|
||||||
|
if userid:
|
||||||
|
self.chain.post_message(
|
||||||
|
Notification(
|
||||||
|
channel=channel,
|
||||||
|
title=f"{command.get('description')} 执行完成",
|
||||||
|
userid=userid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 命令
|
||||||
|
cmd_data = command['data'] if command.get('data') else {}
|
||||||
|
args_num = ObjectUtils.arguments(command['func'])
|
||||||
|
if args_num > 0:
|
||||||
|
if cmd_data:
|
||||||
|
# 有内置参数直接使用内置参数
|
||||||
|
data = cmd_data.get("data") or {}
|
||||||
|
data['channel'] = channel
|
||||||
|
data['user'] = userid
|
||||||
|
cmd_data['data'] = data
|
||||||
|
command['func'](**cmd_data)
|
||||||
|
elif args_num == 2:
|
||||||
|
# 没有输入参数,只输入渠道和用户ID
|
||||||
|
command['func'](channel, userid)
|
||||||
|
elif args_num > 2:
|
||||||
|
# 多个输入参数:用户输入、用户ID
|
||||||
|
command['func'](data_str, channel, userid)
|
||||||
|
else:
|
||||||
|
# 没有参数
|
||||||
|
command['func']()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""
|
"""
|
||||||
停止事件处理线程
|
停止事件处理线程
|
||||||
@@ -216,27 +273,19 @@ class Command(metaclass=Singleton):
|
|||||||
command = self.get(cmd)
|
command = self.get(cmd)
|
||||||
if command:
|
if command:
|
||||||
try:
|
try:
|
||||||
logger.info(f"用户 {userid} 开始执行:{command.get('description')} ...")
|
if userid:
|
||||||
cmd_data = command['data'] if command.get('data') else {}
|
logger.info(f"用户 {userid} 开始执行:{command.get('description')} ...")
|
||||||
args_num = ObjectUtils.arguments(command['func'])
|
|
||||||
if args_num > 0:
|
|
||||||
if cmd_data:
|
|
||||||
# 有内置参数直接使用内置参数
|
|
||||||
data = cmd_data.get("data") or {}
|
|
||||||
data['channel'] = channel
|
|
||||||
data['user'] = userid
|
|
||||||
cmd_data['data'] = data
|
|
||||||
command['func'](**cmd_data)
|
|
||||||
elif args_num == 2:
|
|
||||||
# 没有输入参数,只输入渠道和用户ID
|
|
||||||
command['func'](channel, userid)
|
|
||||||
elif args_num > 2:
|
|
||||||
# 多个输入参数:用户输入、用户ID
|
|
||||||
command['func'](data_str, channel, userid)
|
|
||||||
else:
|
else:
|
||||||
# 没有参数
|
logger.info(f"开始执行:{command.get('description')} ...")
|
||||||
command['func']()
|
|
||||||
logger.info(f"用户 {userid} {command.get('description')} 执行完成")
|
# 执行命令
|
||||||
|
self.__run_command(command, data_str=data_str,
|
||||||
|
channel=channel, userid=userid)
|
||||||
|
|
||||||
|
if userid:
|
||||||
|
logger.info(f"用户 {userid} {command.get('description')} 执行完成")
|
||||||
|
else:
|
||||||
|
logger.info(f"{command.get('description')} 执行完成")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(f"执行命令 {cmd} 出错:{str(err)}")
|
logger.error(f"执行命令 {cmd} 出错:{str(err)}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class Settings(BaseSettings):
|
|||||||
AUTH_SITE: str = ""
|
AUTH_SITE: str = ""
|
||||||
# 交互搜索自动下载用户ID,使用,分割
|
# 交互搜索自动下载用户ID,使用,分割
|
||||||
AUTO_DOWNLOAD_USER: str = None
|
AUTO_DOWNLOAD_USER: str = None
|
||||||
# 消息通知渠道 telegram/wechat/slack
|
# 消息通知渠道 telegram/wechat/slack,多个通知渠道用,分隔
|
||||||
MESSAGER: str = "telegram"
|
MESSAGER: str = "telegram"
|
||||||
# WeChat企业ID
|
# WeChat企业ID
|
||||||
WECHAT_CORPID: str = None
|
WECHAT_CORPID: str = None
|
||||||
@@ -142,7 +142,7 @@ class Settings(BaseSettings):
|
|||||||
DOWNLOAD_CATEGORY: bool = False
|
DOWNLOAD_CATEGORY: bool = False
|
||||||
# 下载站点字幕
|
# 下载站点字幕
|
||||||
DOWNLOAD_SUBTITLE: bool = True
|
DOWNLOAD_SUBTITLE: bool = True
|
||||||
# 媒体服务器 emby/jellyfin/plex
|
# 媒体服务器 emby/jellyfin/plex,多个媒体服务器,分割
|
||||||
MEDIASERVER: str = "emby"
|
MEDIASERVER: str = "emby"
|
||||||
# 入库刷新媒体库
|
# 入库刷新媒体库
|
||||||
REFRESH_MEDIASERVER: bool = True
|
REFRESH_MEDIASERVER: bool = True
|
||||||
|
|||||||
@@ -23,14 +23,17 @@ class CookieHelper:
|
|||||||
"password": [
|
"password": [
|
||||||
'//input[@name="password"]',
|
'//input[@name="password"]',
|
||||||
'//input[@id="form_item_password"]',
|
'//input[@id="form_item_password"]',
|
||||||
'//input[@id="password"]'
|
'//input[@id="password"]',
|
||||||
|
'//input[@type="password"]'
|
||||||
],
|
],
|
||||||
"captcha": [
|
"captcha": [
|
||||||
'//input[@name="imagestring"]',
|
'//input[@name="imagestring"]',
|
||||||
'//input[@name="captcha"]',
|
'//input[@name="captcha"]',
|
||||||
'//input[@id="form_item_captcha"]'
|
'//input[@id="form_item_captcha"]',
|
||||||
|
'//input[@placeholder="驗證碼"]'
|
||||||
],
|
],
|
||||||
"captcha_img": [
|
"captcha_img": [
|
||||||
|
'//img[@alt="captcha"]/@src',
|
||||||
'//img[@alt="CAPTCHA"]/@src',
|
'//img[@alt="CAPTCHA"]/@src',
|
||||||
'//img[@alt="SECURITY CODE"]/@src',
|
'//img[@alt="SECURITY CODE"]/@src',
|
||||||
'//img[@id="LAY-user-get-vercode"]/@src',
|
'//img[@id="LAY-user-get-vercode"]/@src',
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple, Union, Any, List, Generator
|
from typing import Optional, Tuple, Union, Any, List, Generator
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ class EmbyModule(_ModuleBase):
|
|||||||
# Emby认证
|
# Emby认证
|
||||||
return self.emby.authenticate(name, password)
|
return self.emby.authenticate(name, password)
|
||||||
|
|
||||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> WebhookEventInfo:
|
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
|
||||||
"""
|
"""
|
||||||
解析Webhook报文体
|
解析Webhook报文体
|
||||||
:param body: 请求体
|
:param body: 请求体
|
||||||
@@ -49,11 +48,7 @@ class EmbyModule(_ModuleBase):
|
|||||||
:param args: 请求参数
|
:param args: 请求参数
|
||||||
:return: 字典,解析为消息时需要包含:title、text、image
|
:return: 字典,解析为消息时需要包含:title、text、image
|
||||||
"""
|
"""
|
||||||
if form and form.get("data"):
|
return self.emby.get_webhook_message(form, args)
|
||||||
result = form.get("data")
|
|
||||||
else:
|
|
||||||
result = json.dumps(dict(args))
|
|
||||||
return self.emby.get_webhook_message(result)
|
|
||||||
|
|
||||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
||||||
"""
|
"""
|
||||||
@@ -87,7 +82,7 @@ class EmbyModule(_ModuleBase):
|
|||||||
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
|
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
|
||||||
return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
|
return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
|
||||||
|
|
||||||
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]:
|
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
刷新媒体库
|
刷新媒体库
|
||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
@@ -103,25 +98,27 @@ class EmbyModule(_ModuleBase):
|
|||||||
target_path=file_path
|
target_path=file_path
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
return self.emby.refresh_library_by_items(items)
|
self.emby.refresh_library_by_items(items)
|
||||||
|
|
||||||
def media_statistic(self) -> schemas.Statistic:
|
def media_statistic(self) -> List[schemas.Statistic]:
|
||||||
"""
|
"""
|
||||||
媒体数量统计
|
媒体数量统计
|
||||||
"""
|
"""
|
||||||
media_statistic = self.emby.get_medias_count()
|
media_statistic = self.emby.get_medias_count()
|
||||||
user_count = self.emby.get_user_count()
|
user_count = self.emby.get_user_count()
|
||||||
return schemas.Statistic(
|
return [schemas.Statistic(
|
||||||
movie_count=media_statistic.get("MovieCount") or 0,
|
movie_count=media_statistic.get("MovieCount") or 0,
|
||||||
tv_count=media_statistic.get("SeriesCount") or 0,
|
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||||
user_count=user_count or 0
|
user_count=user_count or 0
|
||||||
)
|
)]
|
||||||
|
|
||||||
def mediaserver_librarys(self) -> List[schemas.MediaServerLibrary]:
|
def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||||
"""
|
"""
|
||||||
媒体库列表
|
媒体库列表
|
||||||
"""
|
"""
|
||||||
|
if server != "emby":
|
||||||
|
return None
|
||||||
librarys = self.emby.get_librarys()
|
librarys = self.emby.get_librarys()
|
||||||
if not librarys:
|
if not librarys:
|
||||||
return []
|
return []
|
||||||
@@ -133,10 +130,12 @@ class EmbyModule(_ModuleBase):
|
|||||||
path=library.get("path")
|
path=library.get("path")
|
||||||
) for library in librarys]
|
) for library in librarys]
|
||||||
|
|
||||||
def mediaserver_items(self, library_id: str) -> Generator:
|
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
|
||||||
"""
|
"""
|
||||||
媒体库项目列表
|
媒体库项目列表
|
||||||
"""
|
"""
|
||||||
|
if server != "emby":
|
||||||
|
return None
|
||||||
items = self.emby.get_items(library_id)
|
items = self.emby.get_items(library_id)
|
||||||
for item in items:
|
for item in items:
|
||||||
yield schemas.MediaServerItem(
|
yield schemas.MediaServerItem(
|
||||||
@@ -153,10 +152,13 @@ class EmbyModule(_ModuleBase):
|
|||||||
path=item.get("path"),
|
path=item.get("path"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def mediaserver_tv_episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
def mediaserver_tv_episodes(self, server: str,
|
||||||
|
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
|
||||||
"""
|
"""
|
||||||
获取剧集信息
|
获取剧集信息
|
||||||
"""
|
"""
|
||||||
|
if server != "emby":
|
||||||
|
return None
|
||||||
seasoninfo = self.emby.get_tv_episodes(item_id=item_id)
|
seasoninfo = self.emby.get_tv_episodes(item_id=item_id)
|
||||||
if not seasoninfo:
|
if not seasoninfo:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -545,7 +545,7 @@ class Emby(metaclass=Singleton):
|
|||||||
logger.error(f"连接Users/Items出错:" + str(e))
|
logger.error(f"连接Users/Items出错:" + str(e))
|
||||||
yield {}
|
yield {}
|
||||||
|
|
||||||
def get_webhook_message(self, message_str: str) -> WebhookEventInfo:
|
def get_webhook_message(self, form: any, args: dict) -> Optional[WebhookEventInfo]:
|
||||||
"""
|
"""
|
||||||
解析Emby Webhook报文
|
解析Emby Webhook报文
|
||||||
电影:
|
电影:
|
||||||
@@ -783,9 +783,22 @@ class Emby(metaclass=Singleton):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
message = json.loads(message_str)
|
if not form and not args:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
if form and form.get("data"):
|
||||||
|
result = form.get("data")
|
||||||
|
else:
|
||||||
|
result = json.dumps(dict(args))
|
||||||
|
message = json.loads(result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"解析emby webhook报文出错:" + str(e))
|
||||||
|
return None
|
||||||
|
eventType = message.get('Event')
|
||||||
|
if not eventType:
|
||||||
|
return None
|
||||||
logger.info(f"接收到emby webhook:{message}")
|
logger.info(f"接收到emby webhook:{message}")
|
||||||
eventItem = WebhookEventInfo(event=message.get('Event', ''), channel="emby")
|
eventItem = WebhookEventInfo(event=eventType, channel="emby")
|
||||||
if message.get('Item'):
|
if message.get('Item'):
|
||||||
if message.get('Item', {}).get('Type') == 'Episode':
|
if message.get('Item', {}).get('Type') == 'Episode':
|
||||||
eventItem.item_type = "TV"
|
eventItem.item_type = "TV"
|
||||||
|
|||||||
@@ -536,16 +536,14 @@ class FileTransferModule(_ModuleBase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_library_path(path: Path):
|
def get_library_path(path: Path):
|
||||||
"""
|
"""
|
||||||
根据目录查询其所在的媒体库目录
|
根据目录查询其所在的媒体库目录,查询不到的返回输入目录
|
||||||
"""
|
"""
|
||||||
if not path:
|
if not path:
|
||||||
return None
|
return None
|
||||||
if not settings.LIBRARY_PATHS:
|
if not settings.LIBRARY_PATHS:
|
||||||
return None
|
return path
|
||||||
# 目的路径,多路径以,分隔
|
# 目的路径,多路径以,分隔
|
||||||
dest_paths = settings.LIBRARY_PATHS
|
dest_paths = settings.LIBRARY_PATHS
|
||||||
if len(dest_paths) == 1:
|
|
||||||
return dest_paths[0]
|
|
||||||
for libpath in dest_paths:
|
for libpath in dest_paths:
|
||||||
try:
|
try:
|
||||||
if path.is_relative_to(libpath):
|
if path.is_relative_to(libpath):
|
||||||
@@ -553,7 +551,7 @@ class FileTransferModule(_ModuleBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"计算媒体库路径时出错:{e}")
|
logger.debug(f"计算媒体库路径时出错:{e}")
|
||||||
continue
|
continue
|
||||||
return None
|
return path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_target_path(in_path: Path = None) -> Optional[Path]:
|
def get_target_path(in_path: Path = None) -> Optional[Path]:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from ruamel.yaml import CommentedMap
|
|||||||
from app.core.context import MediaInfo, TorrentInfo
|
from app.core.context import MediaInfo, TorrentInfo
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
|
from app.modules.indexer.mtorrent import MTorrentSpider
|
||||||
from app.modules.indexer.spider import TorrentSpider
|
from app.modules.indexer.spider import TorrentSpider
|
||||||
from app.modules.indexer.tnode import TNodeSpider
|
from app.modules.indexer.tnode import TNodeSpider
|
||||||
from app.modules.indexer.torrentleech import TorrentLeech
|
from app.modules.indexer.torrentleech import TorrentLeech
|
||||||
@@ -74,6 +75,12 @@ class IndexerModule(_ModuleBase):
|
|||||||
keyword=search_word,
|
keyword=search_word,
|
||||||
page=page
|
page=page
|
||||||
)
|
)
|
||||||
|
elif site.get('parser') == "mTorrent":
|
||||||
|
error_flag, result_array = MTorrentSpider(site).search(
|
||||||
|
keyword=search_word,
|
||||||
|
mtype=mediainfo.type if mediainfo else None,
|
||||||
|
page=page
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
error_flag, result_array = self.__spider_search(
|
error_flag, result_array = self.__spider_search(
|
||||||
keyword=search_word,
|
keyword=search_word,
|
||||||
|
|||||||
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认证
|
# Jellyfin认证
|
||||||
return self.jellyfin.authenticate(name, password)
|
return self.jellyfin.authenticate(name, password)
|
||||||
|
|
||||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> WebhookEventInfo:
|
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
|
||||||
"""
|
"""
|
||||||
解析Webhook报文体
|
解析Webhook报文体
|
||||||
:param body: 请求体
|
:param body: 请求体
|
||||||
@@ -49,7 +49,7 @@ class JellyfinModule(_ModuleBase):
|
|||||||
:param args: 请求参数
|
:param args: 请求参数
|
||||||
:return: 字典,解析为消息时需要包含:title、text、image
|
:return: 字典,解析为消息时需要包含:title、text、image
|
||||||
"""
|
"""
|
||||||
return self.jellyfin.get_webhook_message(json.loads(body))
|
return self.jellyfin.get_webhook_message(body)
|
||||||
|
|
||||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
||||||
"""
|
"""
|
||||||
@@ -83,32 +83,34 @@ class JellyfinModule(_ModuleBase):
|
|||||||
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
|
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
|
||||||
return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
|
return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
|
||||||
|
|
||||||
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]:
|
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
刷新媒体库
|
刷新媒体库
|
||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
:param file_path: 文件路径
|
:param file_path: 文件路径
|
||||||
:return: 成功或失败
|
:return: 成功或失败
|
||||||
"""
|
"""
|
||||||
return self.jellyfin.refresh_root_library()
|
self.jellyfin.refresh_root_library()
|
||||||
|
|
||||||
def media_statistic(self) -> schemas.Statistic:
|
def media_statistic(self) -> List[schemas.Statistic]:
|
||||||
"""
|
"""
|
||||||
媒体数量统计
|
媒体数量统计
|
||||||
"""
|
"""
|
||||||
media_statistic = self.jellyfin.get_medias_count()
|
media_statistic = self.jellyfin.get_medias_count()
|
||||||
user_count = self.jellyfin.get_user_count()
|
user_count = self.jellyfin.get_user_count()
|
||||||
return schemas.Statistic(
|
return [schemas.Statistic(
|
||||||
movie_count=media_statistic.get("MovieCount") or 0,
|
movie_count=media_statistic.get("MovieCount") or 0,
|
||||||
tv_count=media_statistic.get("SeriesCount") or 0,
|
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||||
user_count=user_count or 0
|
user_count=user_count or 0
|
||||||
)
|
)]
|
||||||
|
|
||||||
def mediaserver_librarys(self) -> List[schemas.MediaServerLibrary]:
|
def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||||
"""
|
"""
|
||||||
媒体库列表
|
媒体库列表
|
||||||
"""
|
"""
|
||||||
|
if server != "jellyfin":
|
||||||
|
return None
|
||||||
librarys = self.jellyfin.get_librarys()
|
librarys = self.jellyfin.get_librarys()
|
||||||
if not librarys:
|
if not librarys:
|
||||||
return []
|
return []
|
||||||
@@ -120,10 +122,12 @@ class JellyfinModule(_ModuleBase):
|
|||||||
path=library.get("path")
|
path=library.get("path")
|
||||||
) for library in librarys]
|
) for library in librarys]
|
||||||
|
|
||||||
def mediaserver_items(self, library_id: str) -> Generator:
|
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
|
||||||
"""
|
"""
|
||||||
媒体库项目列表
|
媒体库项目列表
|
||||||
"""
|
"""
|
||||||
|
if server != "jellyfin":
|
||||||
|
return None
|
||||||
items = self.jellyfin.get_items(library_id)
|
items = self.jellyfin.get_items(library_id)
|
||||||
for item in items:
|
for item in items:
|
||||||
yield schemas.MediaServerItem(
|
yield schemas.MediaServerItem(
|
||||||
@@ -140,10 +144,13 @@ class JellyfinModule(_ModuleBase):
|
|||||||
path=item.get("path"),
|
path=item.get("path"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def mediaserver_tv_episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
def mediaserver_tv_episodes(self, server: str,
|
||||||
|
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
|
||||||
"""
|
"""
|
||||||
获取剧集信息
|
获取剧集信息
|
||||||
"""
|
"""
|
||||||
|
if server != "jellyfin":
|
||||||
|
return None
|
||||||
seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id)
|
seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id)
|
||||||
if not seasoninfo:
|
if not seasoninfo:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
logger.error(f"连接Library/Refresh出错:" + str(e))
|
logger.error(f"连接Library/Refresh出错:" + str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_webhook_message(self, message: dict) -> WebhookEventInfo:
|
def get_webhook_message(self, body: any) -> Optional[WebhookEventInfo]:
|
||||||
"""
|
"""
|
||||||
解析Jellyfin报文
|
解析Jellyfin报文
|
||||||
{
|
{
|
||||||
@@ -450,9 +450,21 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
"UserId": "9783d2432b0d40a8a716b6aa46xxxxx"
|
"UserId": "9783d2432b0d40a8a716b6aa46xxxxx"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
if not body:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
message = json.loads(body)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"解析Jellyfin Webhook报文出错:" + str(e))
|
||||||
|
return None
|
||||||
|
if not message:
|
||||||
|
return None
|
||||||
logger.info(f"接收到jellyfin webhook:{message}")
|
logger.info(f"接收到jellyfin webhook:{message}")
|
||||||
|
eventType = message.get('NotificationType')
|
||||||
|
if not eventType:
|
||||||
|
return None
|
||||||
eventItem = WebhookEventInfo(
|
eventItem = WebhookEventInfo(
|
||||||
event=message.get('NotificationType', ''),
|
event=eventType,
|
||||||
channel="jellyfin"
|
channel="jellyfin"
|
||||||
)
|
)
|
||||||
eventItem.item_id = message.get('ItemId')
|
eventItem.item_id = message.get('ItemId')
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class PlexModule(_ModuleBase):
|
|||||||
if not self.plex.is_inactive():
|
if not self.plex.is_inactive():
|
||||||
self.plex = Plex()
|
self.plex = Plex()
|
||||||
|
|
||||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> WebhookEventInfo:
|
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
|
||||||
"""
|
"""
|
||||||
解析Webhook报文体
|
解析Webhook报文体
|
||||||
:param body: 请求体
|
:param body: 请求体
|
||||||
@@ -39,7 +39,7 @@ class PlexModule(_ModuleBase):
|
|||||||
:param args: 请求参数
|
:param args: 请求参数
|
||||||
:return: 字典,解析为消息时需要包含:title、text、image
|
:return: 字典,解析为消息时需要包含:title、text、image
|
||||||
"""
|
"""
|
||||||
return self.plex.get_webhook_message(form.get("payload"))
|
return self.plex.get_webhook_message(form)
|
||||||
|
|
||||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
||||||
"""
|
"""
|
||||||
@@ -77,7 +77,7 @@ class PlexModule(_ModuleBase):
|
|||||||
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
|
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
|
||||||
return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
|
return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
|
||||||
|
|
||||||
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> Optional[bool]:
|
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
刷新媒体库
|
刷新媒体库
|
||||||
:param mediainfo: 识别的媒体信息
|
:param mediainfo: 识别的媒体信息
|
||||||
@@ -93,24 +93,26 @@ class PlexModule(_ModuleBase):
|
|||||||
target_path=file_path
|
target_path=file_path
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
return self.plex.refresh_library_by_items(items)
|
self.plex.refresh_library_by_items(items)
|
||||||
|
|
||||||
def media_statistic(self) -> schemas.Statistic:
|
def media_statistic(self) -> List[schemas.Statistic]:
|
||||||
"""
|
"""
|
||||||
媒体数量统计
|
媒体数量统计
|
||||||
"""
|
"""
|
||||||
media_statistic = self.plex.get_medias_count()
|
media_statistic = self.plex.get_medias_count()
|
||||||
return schemas.Statistic(
|
return [schemas.Statistic(
|
||||||
movie_count=media_statistic.get("MovieCount") or 0,
|
movie_count=media_statistic.get("MovieCount") or 0,
|
||||||
tv_count=media_statistic.get("SeriesCount") or 0,
|
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||||
user_count=1
|
user_count=1
|
||||||
)
|
)]
|
||||||
|
|
||||||
def mediaserver_librarys(self) -> List[schemas.MediaServerLibrary]:
|
def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||||
"""
|
"""
|
||||||
媒体库列表
|
媒体库列表
|
||||||
"""
|
"""
|
||||||
|
if server != "plex":
|
||||||
|
return None
|
||||||
librarys = self.plex.get_librarys()
|
librarys = self.plex.get_librarys()
|
||||||
if not librarys:
|
if not librarys:
|
||||||
return []
|
return []
|
||||||
@@ -122,10 +124,12 @@ class PlexModule(_ModuleBase):
|
|||||||
path=library.get("path")
|
path=library.get("path")
|
||||||
) for library in librarys]
|
) for library in librarys]
|
||||||
|
|
||||||
def mediaserver_items(self, library_id: str) -> Generator:
|
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
|
||||||
"""
|
"""
|
||||||
媒体库项目列表
|
媒体库项目列表
|
||||||
"""
|
"""
|
||||||
|
if server != "plex":
|
||||||
|
return None
|
||||||
items = self.plex.get_items(library_id)
|
items = self.plex.get_items(library_id)
|
||||||
for item in items:
|
for item in items:
|
||||||
yield schemas.MediaServerItem(
|
yield schemas.MediaServerItem(
|
||||||
@@ -142,10 +146,13 @@ class PlexModule(_ModuleBase):
|
|||||||
path=item.get("path"),
|
path=item.get("path"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def mediaserver_tv_episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
def mediaserver_tv_episodes(self, server: str,
|
||||||
|
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
|
||||||
"""
|
"""
|
||||||
获取剧集信息
|
获取剧集信息
|
||||||
"""
|
"""
|
||||||
|
if server != "plex":
|
||||||
|
return None
|
||||||
seasoninfo = self.plex.get_tv_episodes(item_id=item_id)
|
seasoninfo = self.plex.get_tv_episodes(item_id=item_id)
|
||||||
if not seasoninfo:
|
if not seasoninfo:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ class Plex(metaclass=Singleton):
|
|||||||
logger.error(f"获取媒体库列表出错:{err}")
|
logger.error(f"获取媒体库列表出错:{err}")
|
||||||
yield {}
|
yield {}
|
||||||
|
|
||||||
def get_webhook_message(self, message_str: str) -> WebhookEventInfo:
|
def get_webhook_message(self, form: any) -> Optional[WebhookEventInfo]:
|
||||||
"""
|
"""
|
||||||
解析Plex报文
|
解析Plex报文
|
||||||
eventItem 字段的含义
|
eventItem 字段的含义
|
||||||
@@ -457,9 +457,21 @@ class Plex(metaclass=Singleton):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
message = json.loads(message_str)
|
if not form:
|
||||||
|
return None
|
||||||
|
payload = form.get("payload")
|
||||||
|
if not payload:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
message = json.loads(payload)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"解析plex webhook出错:{str(e)}")
|
||||||
|
return None
|
||||||
|
eventType = message.get('event')
|
||||||
|
if not eventType:
|
||||||
|
return None
|
||||||
logger.info(f"接收到plex webhook:{message}")
|
logger.info(f"接收到plex webhook:{message}")
|
||||||
eventItem = WebhookEventInfo(event=message.get('event', ''), channel="plex")
|
eventItem = WebhookEventInfo(event=eventType, channel="plex")
|
||||||
if message.get('Metadata'):
|
if message.get('Metadata'):
|
||||||
if message.get('Metadata', {}).get('type') == 'episode':
|
if message.get('Metadata', {}).get('type') == 'episode':
|
||||||
eventItem.item_type = "TV"
|
eventItem.item_type = "TV"
|
||||||
|
|||||||
@@ -131,130 +131,130 @@ class BestFilmVersion(_PluginBase):
|
|||||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'component': 'VForm',
|
'component': 'VForm',
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VRow',
|
'component': 'VRow',
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VCol',
|
'component': 'VCol',
|
||||||
'props': {
|
'props': {
|
||||||
'cols': 12,
|
'cols': 12,
|
||||||
'md': 3
|
'md': 3
|
||||||
},
|
},
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VSwitch',
|
'component': 'VSwitch',
|
||||||
'props': {
|
'props': {
|
||||||
'model': 'enabled',
|
'model': 'enabled',
|
||||||
'label': '启用插件',
|
'label': '启用插件',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'component': 'VCol',
|
'component': 'VCol',
|
||||||
'props': {
|
'props': {
|
||||||
'cols': 12,
|
'cols': 12,
|
||||||
'md': 3
|
'md': 3
|
||||||
},
|
},
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VSwitch',
|
'component': 'VSwitch',
|
||||||
'props': {
|
'props': {
|
||||||
'model': 'notify',
|
'model': 'notify',
|
||||||
'label': '发送通知',
|
'label': '发送通知',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'component': 'VCol',
|
'component': 'VCol',
|
||||||
'props': {
|
'props': {
|
||||||
'cols': 12,
|
'cols': 12,
|
||||||
'md': 3
|
'md': 3
|
||||||
},
|
},
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VSwitch',
|
'component': 'VSwitch',
|
||||||
'props': {
|
'props': {
|
||||||
'model': 'only_once',
|
'model': 'only_once',
|
||||||
'label': '立即运行一次',
|
'label': '立即运行一次',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'component': 'VCol',
|
'component': 'VCol',
|
||||||
'props': {
|
'props': {
|
||||||
'cols': 12,
|
'cols': 12,
|
||||||
'md': 3
|
'md': 3
|
||||||
},
|
},
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VSwitch',
|
'component': 'VSwitch',
|
||||||
'props': {
|
'props': {
|
||||||
'model': 'webhook_enabled',
|
'model': 'webhook_enabled',
|
||||||
'label': 'Webhook',
|
'label': 'Webhook',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'component': 'VRow',
|
'component': 'VRow',
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VCol',
|
'component': 'VCol',
|
||||||
'props': {
|
'props': {
|
||||||
'cols': 12,
|
'cols': 12,
|
||||||
},
|
},
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VTextField',
|
'component': 'VTextField',
|
||||||
'props': {
|
'props': {
|
||||||
'model': 'cron',
|
'model': 'cron',
|
||||||
'label': '执行周期',
|
'label': '执行周期',
|
||||||
'placeholder': '5位cron表达式,留空自动'
|
'placeholder': '5位cron表达式,留空自动'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'component': 'VRow',
|
'component': 'VRow',
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VCol',
|
'component': 'VCol',
|
||||||
'props': {
|
'props': {
|
||||||
'cols': 12,
|
'cols': 12,
|
||||||
},
|
},
|
||||||
'content': [
|
'content': [
|
||||||
{
|
{
|
||||||
'component': 'VAlert',
|
'component': 'VAlert',
|
||||||
'props': {
|
'props': {
|
||||||
'text': '支持主动定时获取媒体库数据和Webhook实时触发两种方式,两者只能选其一,'
|
'text': '支持主动定时获取媒体库数据和Webhook实时触发两种方式,两者只能选其一,'
|
||||||
'Webhook需要在媒体服务器设置发送Webhook报文。'
|
'Webhook需要在媒体服务器设置发送Webhook报文。'
|
||||||
'Plex使用主动获取时,建议执行周期设置大于1小时,'
|
'Plex使用主动获取时,建议执行周期设置大于1小时,'
|
||||||
'收藏Api调用Plex官网接口,有频率限制。'
|
'收藏Api调用Plex官网接口,有频率限制。'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
], {
|
], {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
"notify": False,
|
"notify": False,
|
||||||
"cron": "*/30 * * * *",
|
"cron": "*/30 * * * *",
|
||||||
"webhook_enabled": False,
|
"webhook_enabled": False,
|
||||||
"only_once": False
|
"only_once": False
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_page(self) -> List[dict]:
|
def get_page(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
@@ -386,81 +386,85 @@ class BestFilmVersion(_PluginBase):
|
|||||||
# 读取历史记录
|
# 读取历史记录
|
||||||
history = self.get_data('history') or []
|
history = self.get_data('history') or []
|
||||||
|
|
||||||
all_item = []
|
# 媒体服务器类型,多个以,分隔
|
||||||
|
if not settings.MEDIASERVER:
|
||||||
|
return
|
||||||
|
media_servers = settings.MEDIASERVER.split(',')
|
||||||
|
|
||||||
# 读取收藏
|
# 读取收藏
|
||||||
if settings.MEDIASERVER == 'jellyfin':
|
all_items = {}
|
||||||
self.jellyfin_get_items(all_item)
|
for media_server in media_servers:
|
||||||
elif settings.MEDIASERVER == 'emby':
|
if media_server == 'jellyfin':
|
||||||
self.emby_get_items(all_item)
|
all_items['jellyfin'] = self.jellyfin_get_items()
|
||||||
else:
|
elif media_server == 'emby':
|
||||||
resp = self.plex_get_watchlist()
|
all_items['emby'] = self.emby_get_items()
|
||||||
if not resp:
|
else:
|
||||||
return
|
all_items['plex'] = self.plex_get_watchlist()
|
||||||
all_item.extend(resp)
|
|
||||||
|
|
||||||
def function(y, x):
|
def function(y, x):
|
||||||
return y if (x['Name'] in [i['Name'] for i in y]) else (lambda z, u: (z.append(u), z))(y, x)[1]
|
return y if (x['Name'] in [i['Name'] for i in y]) else (lambda z, u: (z.append(u), z))(y, x)[1]
|
||||||
|
|
||||||
# all_item 根据电影名去重
|
# 处理所有结果
|
||||||
result = reduce(function, all_item, [])
|
for server, all_item in all_items.items():
|
||||||
|
# all_item 根据电影名去重
|
||||||
for data in result:
|
result = reduce(function, all_item, [])
|
||||||
# 检查缓存
|
for data in result:
|
||||||
if data.get('Name') in caches:
|
# 检查缓存
|
||||||
continue
|
if data.get('Name') in caches:
|
||||||
|
|
||||||
# 获取详情
|
|
||||||
if settings.MEDIASERVER == 'jellyfin':
|
|
||||||
item_info_resp = Jellyfin().get_iteminfo(itemid=data.get('Id'))
|
|
||||||
elif settings.MEDIASERVER == 'emby':
|
|
||||||
item_info_resp = Emby().get_iteminfo(itemid=data.get('Id'))
|
|
||||||
else:
|
|
||||||
item_info_resp = self.plex_get_iteminfo(itemid=data.get('Id'))
|
|
||||||
|
|
||||||
logger.info(f'BestFilmVersion插件 item打印 {item_info_resp}')
|
|
||||||
if not item_info_resp:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 只接受Movie类型
|
|
||||||
if data.get('Type') != 'Movie':
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 获取tmdb_id
|
|
||||||
media_info_ids = item_info_resp.get('ExternalUrls')
|
|
||||||
if not media_info_ids:
|
|
||||||
continue
|
|
||||||
for media_info_id in media_info_ids:
|
|
||||||
if 'TheMovieDb' != media_info_id.get('Name'):
|
|
||||||
continue
|
continue
|
||||||
tmdb_find_id = str(media_info_id.get('Url')).split('/')
|
|
||||||
tmdb_find_id.reverse()
|
# 获取详情
|
||||||
tmdb_id = tmdb_find_id[0]
|
if server == 'jellyfin':
|
||||||
# 识别媒体信息
|
item_info_resp = Jellyfin().get_iteminfo(itemid=data.get('Id'))
|
||||||
mediainfo: MediaInfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE)
|
elif server == 'emby':
|
||||||
if not mediainfo:
|
item_info_resp = Emby().get_iteminfo(itemid=data.get('Id'))
|
||||||
logger.warn(f'未识别到媒体信息,标题:{data.get("Name")},tmdbID:{tmdb_id}')
|
else:
|
||||||
|
item_info_resp = self.plex_get_iteminfo(itemid=data.get('Id'))
|
||||||
|
|
||||||
|
logger.info(f'BestFilmVersion插件 item打印 {item_info_resp}')
|
||||||
|
if not item_info_resp:
|
||||||
continue
|
continue
|
||||||
# 添加订阅
|
|
||||||
self.subscribechain.add(mtype=MediaType.MOVIE,
|
# 只接受Movie类型
|
||||||
title=mediainfo.title,
|
if data.get('Type') != 'Movie':
|
||||||
year=mediainfo.year,
|
continue
|
||||||
tmdbid=mediainfo.tmdb_id,
|
|
||||||
best_version=True,
|
# 获取tmdb_id
|
||||||
username="收藏洗版",
|
media_info_ids = item_info_resp.get('ExternalUrls')
|
||||||
exist_ok=True)
|
if not media_info_ids:
|
||||||
# 加入缓存
|
continue
|
||||||
caches.append(data.get('Name'))
|
for media_info_id in media_info_ids:
|
||||||
# 存储历史记录
|
if 'TheMovieDb' != media_info_id.get('Name'):
|
||||||
if mediainfo.tmdb_id not in [h.get("tmdbid") for h in history]:
|
continue
|
||||||
history.append({
|
tmdb_find_id = str(media_info_id.get('Url')).split('/')
|
||||||
"title": mediainfo.title,
|
tmdb_find_id.reverse()
|
||||||
"type": mediainfo.type.value,
|
tmdb_id = tmdb_find_id[0]
|
||||||
"year": mediainfo.year,
|
# 识别媒体信息
|
||||||
"poster": mediainfo.get_poster_image(),
|
mediainfo: MediaInfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE)
|
||||||
"overview": mediainfo.overview,
|
if not mediainfo:
|
||||||
"tmdbid": mediainfo.tmdb_id,
|
logger.warn(f'未识别到媒体信息,标题:{data.get("Name")},tmdbID:{tmdb_id}')
|
||||||
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
continue
|
||||||
})
|
# 添加订阅
|
||||||
|
self.subscribechain.add(mtype=MediaType.MOVIE,
|
||||||
|
title=mediainfo.title,
|
||||||
|
year=mediainfo.year,
|
||||||
|
tmdbid=mediainfo.tmdb_id,
|
||||||
|
best_version=True,
|
||||||
|
username="收藏洗版",
|
||||||
|
exist_ok=True)
|
||||||
|
# 加入缓存
|
||||||
|
caches.append(data.get('Name'))
|
||||||
|
# 存储历史记录
|
||||||
|
if mediainfo.tmdb_id not in [h.get("tmdbid") for h in history]:
|
||||||
|
history.append({
|
||||||
|
"title": mediainfo.title,
|
||||||
|
"type": mediainfo.type.value,
|
||||||
|
"year": mediainfo.year,
|
||||||
|
"poster": mediainfo.get_poster_image(),
|
||||||
|
"overview": mediainfo.overview,
|
||||||
|
"tmdbid": mediainfo.tmdb_id,
|
||||||
|
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
})
|
||||||
# 保存历史记录
|
# 保存历史记录
|
||||||
self.save_data('history', history)
|
self.save_data('history', history)
|
||||||
# 保存缓存
|
# 保存缓存
|
||||||
@@ -468,13 +472,14 @@ class BestFilmVersion(_PluginBase):
|
|||||||
finally:
|
finally:
|
||||||
lock.release()
|
lock.release()
|
||||||
|
|
||||||
def jellyfin_get_items(self, all_item):
|
def jellyfin_get_items(self) -> List[dict]:
|
||||||
# 获取所有user
|
# 获取所有user
|
||||||
users_url = "{HOST}Users?&apikey={APIKEY}"
|
users_url = "{HOST}Users?&apikey={APIKEY}"
|
||||||
users = self.get_users(Jellyfin().get_data(users_url))
|
users = self.get_users(Jellyfin().get_data(users_url))
|
||||||
if not users:
|
if not users:
|
||||||
logger.info(f"bestfilmversion/users_url: {users_url}")
|
logger.info(f"bestfilmversion/users_url: {users_url}")
|
||||||
return
|
return []
|
||||||
|
all_items = []
|
||||||
for user in users:
|
for user in users:
|
||||||
# 根据加入日期 降序排序
|
# 根据加入日期 降序排序
|
||||||
url = "{HOST}Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \
|
url = "{HOST}Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \
|
||||||
@@ -490,14 +495,16 @@ class BestFilmVersion(_PluginBase):
|
|||||||
resp = self.get_items(Jellyfin().get_data(url))
|
resp = self.get_items(Jellyfin().get_data(url))
|
||||||
if not resp:
|
if not resp:
|
||||||
continue
|
continue
|
||||||
all_item.extend(resp)
|
all_items.extend(resp)
|
||||||
|
return all_items
|
||||||
|
|
||||||
def emby_get_items(self, all_item):
|
def emby_get_items(self) -> List[dict]:
|
||||||
# 获取所有user
|
# 获取所有user
|
||||||
get_users_url = "{HOST}Users?&api_key={APIKEY}"
|
get_users_url = "{HOST}Users?&api_key={APIKEY}"
|
||||||
users = self.get_users(Emby().get_data(get_users_url))
|
users = self.get_users(Emby().get_data(get_users_url))
|
||||||
if not users:
|
if not users:
|
||||||
return
|
return []
|
||||||
|
all_items = []
|
||||||
for user in users:
|
for user in users:
|
||||||
# 根据加入日期 降序排序
|
# 根据加入日期 降序排序
|
||||||
url = "{HOST}emby/Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \
|
url = "{HOST}emby/Users/" + user + "/Items?SortBy=DateCreated%2CSortName" \
|
||||||
@@ -512,7 +519,8 @@ class BestFilmVersion(_PluginBase):
|
|||||||
resp = self.get_items(Emby().get_data(url))
|
resp = self.get_items(Emby().get_data(url))
|
||||||
if not resp:
|
if not resp:
|
||||||
continue
|
continue
|
||||||
all_item.extend(resp)
|
all_items.extend(resp)
|
||||||
|
return all_items
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_items(resp: Response):
|
def get_items(resp: Response):
|
||||||
@@ -538,7 +546,7 @@ class BestFilmVersion(_PluginBase):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def plex_get_watchlist():
|
def plex_get_watchlist() -> List[dict]:
|
||||||
# 根据加入日期 降序排序
|
# 根据加入日期 降序排序
|
||||||
url = f"https://metadata.provider.plex.tv/library/sections/watchlist/all?type=1&sort=addedAt%3Adesc" \
|
url = f"https://metadata.provider.plex.tv/library/sections/watchlist/all?type=1&sort=addedAt%3Adesc" \
|
||||||
f"&X-Plex-Container-Start=0&X-Plex-Container-Size=50" \
|
f"&X-Plex-Container-Start=0&X-Plex-Container-Size=50" \
|
||||||
|
|||||||
@@ -598,7 +598,7 @@ class DirMonitor(_PluginBase):
|
|||||||
'rows': 5,
|
'rows': 5,
|
||||||
'placeholder': '每一行一个目录,支持两种配置方式:\n'
|
'placeholder': '每一行一个目录,支持两种配置方式:\n'
|
||||||
'监控目录\n'
|
'监控目录\n'
|
||||||
'监控目录:转移目的目录'
|
'监控目录:转移目的目录(需同时在媒体库目录中配置该目的目录)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -718,19 +718,21 @@ class MediaSyncDel(_PluginBase):
|
|||||||
"""
|
"""
|
||||||
# 读取历史记录
|
# 读取历史记录
|
||||||
history = self.get_data('history') or []
|
history = self.get_data('history') or []
|
||||||
|
|
||||||
# 媒体服务器类型
|
|
||||||
media_server = settings.MEDIASERVER
|
|
||||||
|
|
||||||
last_time = self.get_data("last_time")
|
last_time = self.get_data("last_time")
|
||||||
del_medias = []
|
del_medias = []
|
||||||
if media_server == 'emby':
|
|
||||||
del_medias = self.parse_emby_log(last_time)
|
# 媒体服务器类型,多个以,分隔
|
||||||
elif media_server == 'jellyfin':
|
if not settings.MEDIASERVER:
|
||||||
del_medias = self.parse_jellyfin_log(last_time)
|
|
||||||
elif media_server == 'plex':
|
|
||||||
# TODO plex解析日志
|
|
||||||
return
|
return
|
||||||
|
media_servers = settings.MEDIASERVER.split(',')
|
||||||
|
for media_server in media_servers:
|
||||||
|
if media_server == 'emby':
|
||||||
|
del_medias.extend(self.parse_emby_log(last_time))
|
||||||
|
elif media_server == 'jellyfin':
|
||||||
|
del_medias.extend(self.parse_jellyfin_log(last_time))
|
||||||
|
elif media_server == 'plex':
|
||||||
|
# TODO plex解析日志
|
||||||
|
return
|
||||||
|
|
||||||
if not del_medias:
|
if not del_medias:
|
||||||
logger.error("未解析到已删除媒体信息")
|
logger.error("未解析到已删除媒体信息")
|
||||||
|
|||||||
@@ -383,78 +383,86 @@ class SpeedLimiter(_PluginBase):
|
|||||||
return
|
return
|
||||||
# 当前播放的总比特率
|
# 当前播放的总比特率
|
||||||
total_bit_rate = 0
|
total_bit_rate = 0
|
||||||
# 查询播放中会话
|
# 媒体服务器类型,多个以,分隔
|
||||||
playing_sessions = []
|
if not settings.MEDIASERVER:
|
||||||
if settings.MEDIASERVER == "emby":
|
return
|
||||||
req_url = "{HOST}emby/Sessions?api_key={APIKEY}"
|
media_servers = settings.MEDIASERVER.split(',')
|
||||||
try:
|
# 查询所有媒体服务器状态
|
||||||
res = Emby().get_data(req_url)
|
for media_server in media_servers:
|
||||||
if res and res.status_code == 200:
|
# 查询播放中会话
|
||||||
sessions = res.json()
|
playing_sessions = []
|
||||||
for session in sessions:
|
if media_server == "emby":
|
||||||
if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"):
|
req_url = "{HOST}emby/Sessions?api_key={APIKEY}"
|
||||||
playing_sessions.append(session)
|
try:
|
||||||
except Exception as e:
|
res = Emby().get_data(req_url)
|
||||||
logger.error(f"获取Emby播放会话失败:{str(e)}")
|
if res and res.status_code == 200:
|
||||||
# 计算有效比特率
|
sessions = res.json()
|
||||||
for session in playing_sessions:
|
for session in sessions:
|
||||||
# 设置了不限速范围则判断session ip是否在不限速范围内
|
if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"):
|
||||||
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
|
playing_sessions.append(session)
|
||||||
if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \
|
except Exception as e:
|
||||||
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
|
logger.error(f"获取Emby播放会话失败:{str(e)}")
|
||||||
total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0)
|
continue
|
||||||
# 未设置不限速范围,则默认不限速内网ip
|
|
||||||
elif not IpUtils.is_private_ip(session.get("RemoteEndPoint")) \
|
|
||||||
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
|
|
||||||
total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0)
|
|
||||||
elif settings.MEDIASERVER == "jellyfin":
|
|
||||||
req_url = "{HOST}Sessions?api_key={APIKEY}"
|
|
||||||
try:
|
|
||||||
res = Jellyfin().get_data(req_url)
|
|
||||||
if res and res.status_code == 200:
|
|
||||||
sessions = res.json()
|
|
||||||
for session in sessions:
|
|
||||||
if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"):
|
|
||||||
playing_sessions.append(session)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取Jellyfin播放会话失败:{str(e)}")
|
|
||||||
# 计算有效比特率
|
|
||||||
for session in playing_sessions:
|
|
||||||
# 设置了不限速范围则判断session ip是否在不限速范围内
|
|
||||||
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
|
|
||||||
if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \
|
|
||||||
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
|
|
||||||
media_streams = session.get("NowPlayingItem", {}).get("MediaStreams") or []
|
|
||||||
for media_stream in media_streams:
|
|
||||||
total_bit_rate += int(media_stream.get("BitRate") or 0)
|
|
||||||
# 未设置不限速范围,则默认不限速内网ip
|
|
||||||
elif not IpUtils.is_private_ip(session.get("RemoteEndPoint")) \
|
|
||||||
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
|
|
||||||
media_streams = session.get("NowPlayingItem", {}).get("MediaStreams") or []
|
|
||||||
for media_stream in media_streams:
|
|
||||||
total_bit_rate += int(media_stream.get("BitRate") or 0)
|
|
||||||
elif settings.MEDIASERVER == "plex":
|
|
||||||
_plex = Plex().get_plex()
|
|
||||||
if _plex:
|
|
||||||
sessions = _plex.sessions()
|
|
||||||
for session in sessions:
|
|
||||||
bitrate = sum([m.bitrate or 0 for m in session.media])
|
|
||||||
playing_sessions.append({
|
|
||||||
"type": session.TAG,
|
|
||||||
"bitrate": bitrate,
|
|
||||||
"address": session.player.address
|
|
||||||
})
|
|
||||||
# 计算有效比特率
|
# 计算有效比特率
|
||||||
for session in playing_sessions:
|
for session in playing_sessions:
|
||||||
# 设置了不限速范围则判断session ip是否在不限速范围内
|
# 设置了不限速范围则判断session ip是否在不限速范围内
|
||||||
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
|
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
|
||||||
if not self.__allow_access(self._unlimited_ips, session.get("address")) \
|
if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \
|
||||||
|
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
|
||||||
|
total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0)
|
||||||
|
# 未设置不限速范围,则默认不限速内网ip
|
||||||
|
elif not IpUtils.is_private_ip(session.get("RemoteEndPoint")) \
|
||||||
|
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
|
||||||
|
total_bit_rate += int(session.get("NowPlayingItem", {}).get("Bitrate") or 0)
|
||||||
|
elif media_server == "jellyfin":
|
||||||
|
req_url = "{HOST}Sessions?api_key={APIKEY}"
|
||||||
|
try:
|
||||||
|
res = Jellyfin().get_data(req_url)
|
||||||
|
if res and res.status_code == 200:
|
||||||
|
sessions = res.json()
|
||||||
|
for session in sessions:
|
||||||
|
if session.get("NowPlayingItem") and not session.get("PlayState", {}).get("IsPaused"):
|
||||||
|
playing_sessions.append(session)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取Jellyfin播放会话失败:{str(e)}")
|
||||||
|
continue
|
||||||
|
# 计算有效比特率
|
||||||
|
for session in playing_sessions:
|
||||||
|
# 设置了不限速范围则判断session ip是否在不限速范围内
|
||||||
|
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
|
||||||
|
if not self.__allow_access(self._unlimited_ips, session.get("RemoteEndPoint")) \
|
||||||
|
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
|
||||||
|
media_streams = session.get("NowPlayingItem", {}).get("MediaStreams") or []
|
||||||
|
for media_stream in media_streams:
|
||||||
|
total_bit_rate += int(media_stream.get("BitRate") or 0)
|
||||||
|
# 未设置不限速范围,则默认不限速内网ip
|
||||||
|
elif not IpUtils.is_private_ip(session.get("RemoteEndPoint")) \
|
||||||
|
and session.get("NowPlayingItem", {}).get("MediaType") == "Video":
|
||||||
|
media_streams = session.get("NowPlayingItem", {}).get("MediaStreams") or []
|
||||||
|
for media_stream in media_streams:
|
||||||
|
total_bit_rate += int(media_stream.get("BitRate") or 0)
|
||||||
|
elif media_server == "plex":
|
||||||
|
_plex = Plex().get_plex()
|
||||||
|
if _plex:
|
||||||
|
sessions = _plex.sessions()
|
||||||
|
for session in sessions:
|
||||||
|
bitrate = sum([m.bitrate or 0 for m in session.media])
|
||||||
|
playing_sessions.append({
|
||||||
|
"type": session.TAG,
|
||||||
|
"bitrate": bitrate,
|
||||||
|
"address": session.player.address
|
||||||
|
})
|
||||||
|
# 计算有效比特率
|
||||||
|
for session in playing_sessions:
|
||||||
|
# 设置了不限速范围则判断session ip是否在不限速范围内
|
||||||
|
if self._unlimited_ips["ipv4"] or self._unlimited_ips["ipv6"]:
|
||||||
|
if not self.__allow_access(self._unlimited_ips, session.get("address")) \
|
||||||
|
and session.get("type") == "Video":
|
||||||
|
total_bit_rate += int(session.get("bitrate") or 0)
|
||||||
|
# 未设置不限速范围,则默认不限速内网ip
|
||||||
|
elif not IpUtils.is_private_ip(session.get("address")) \
|
||||||
and session.get("type") == "Video":
|
and session.get("type") == "Video":
|
||||||
total_bit_rate += int(session.get("bitrate") or 0)
|
total_bit_rate += int(session.get("bitrate") or 0)
|
||||||
# 未设置不限速范围,则默认不限速内网ip
|
|
||||||
elif not IpUtils.is_private_ip(session.get("address")) \
|
|
||||||
and session.get("type") == "Video":
|
|
||||||
total_bit_rate += int(session.get("bitrate") or 0)
|
|
||||||
|
|
||||||
if total_bit_rate:
|
if total_bit_rate:
|
||||||
# 开启智能限速计算上传限速
|
# 开启智能限速计算上传限速
|
||||||
|
|||||||
@@ -644,7 +644,7 @@ class TorrentRemover(_PluginBase):
|
|||||||
return None
|
return None
|
||||||
if self._torrentstates and torrent.state not in self._torrentstates:
|
if self._torrentstates and torrent.state not in self._torrentstates:
|
||||||
return None
|
return None
|
||||||
if self._torrentcategorys and torrent.category not in self._torrentcategorys:
|
if self._torrentcategorys and (not torrent.category or torrent.category not in self._torrentcategorys):
|
||||||
return None
|
return None
|
||||||
return {
|
return {
|
||||||
"id": torrent.hash,
|
"id": torrent.hash,
|
||||||
|
|||||||
185
app/scheduler.py
185
app/scheduler.py
@@ -1,10 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from apscheduler.executors.pool import ThreadPoolExecutor
|
from apscheduler.executors.pool import ThreadPoolExecutor
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
from app.chain.cookiecloud import CookieCloudChain
|
from app.chain.cookiecloud import CookieCloudChain
|
||||||
from app.chain.mediaserver import MediaServerChain
|
from app.chain.mediaserver import MediaServerChain
|
||||||
@@ -40,57 +42,153 @@ class Scheduler(metaclass=Singleton):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
# 数据库连接
|
# 数据库连接
|
||||||
self._db = SessionFactory()
|
self._db = SessionFactory()
|
||||||
|
# 各服务的运行状态
|
||||||
|
self._jobs = {
|
||||||
|
"cookiecloud": {
|
||||||
|
"func": CookieCloudChain(self._db).process,
|
||||||
|
"running": False,
|
||||||
|
},
|
||||||
|
"mediaserver_sync": {
|
||||||
|
"func": MediaServerChain(self._db).sync,
|
||||||
|
"running": False,
|
||||||
|
},
|
||||||
|
"subscribe_tmdb": {
|
||||||
|
"func": SubscribeChain(self._db).check,
|
||||||
|
"running": False,
|
||||||
|
},
|
||||||
|
"subscribe_search": {
|
||||||
|
"func": SubscribeChain(self._db).search,
|
||||||
|
"running": False,
|
||||||
|
},
|
||||||
|
"subscribe_refresh": {
|
||||||
|
"func": SubscribeChain(self._db).refresh,
|
||||||
|
"running": False,
|
||||||
|
},
|
||||||
|
"transfer": {
|
||||||
|
"func": TransferChain(self._db).process,
|
||||||
|
"running": False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# 调试模式不启动定时服务
|
# 调试模式不启动定时服务
|
||||||
if settings.DEV:
|
if settings.DEV:
|
||||||
return
|
return
|
||||||
|
|
||||||
# CookieCloud定时同步
|
# CookieCloud定时同步
|
||||||
if settings.COOKIECLOUD_INTERVAL:
|
if settings.COOKIECLOUD_INTERVAL:
|
||||||
self._scheduler.add_job(CookieCloudChain(self._db).process,
|
self._scheduler.add_job(
|
||||||
"interval",
|
self.start,
|
||||||
minutes=settings.COOKIECLOUD_INTERVAL,
|
"interval",
|
||||||
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=1),
|
id="cookiecloud",
|
||||||
name="同步CookieCloud站点")
|
name="同步CookieCloud站点",
|
||||||
|
minutes=settings.COOKIECLOUD_INTERVAL,
|
||||||
|
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=1),
|
||||||
|
kwargs={
|
||||||
|
'job_id': 'cookiecloud'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 媒体服务器同步
|
# 媒体服务器同步
|
||||||
if settings.MEDIASERVER_SYNC_INTERVAL:
|
if settings.MEDIASERVER_SYNC_INTERVAL:
|
||||||
self._scheduler.add_job(MediaServerChain(self._db).sync, "interval",
|
self._scheduler.add_job(
|
||||||
hours=settings.MEDIASERVER_SYNC_INTERVAL,
|
self.start,
|
||||||
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=5),
|
"interval",
|
||||||
name="同步媒体服务器")
|
id="mediaserver_sync",
|
||||||
|
name="同步媒体服务器",
|
||||||
|
hours=settings.MEDIASERVER_SYNC_INTERVAL,
|
||||||
|
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=5),
|
||||||
|
kwargs={
|
||||||
|
'job_id': 'mediaserver_sync'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 新增订阅时搜索(5分钟检查一次)
|
# 新增订阅时搜索(5分钟检查一次)
|
||||||
self._scheduler.add_job(SubscribeChain(self._db).search, "interval",
|
self._scheduler.add_job(
|
||||||
minutes=5, kwargs={'state': 'N'})
|
self.start,
|
||||||
|
"interval",
|
||||||
|
minutes=5,
|
||||||
|
kwargs={
|
||||||
|
'job_id': 'subscribe_search',
|
||||||
|
'state': 'N'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 检查更新订阅TMDB数据(每隔6小时)
|
# 检查更新订阅TMDB数据(每隔6小时)
|
||||||
self._scheduler.add_job(SubscribeChain(self._db).check, "interval", hours=6)
|
self._scheduler.add_job(
|
||||||
|
self.start,
|
||||||
|
"interval",
|
||||||
|
id="subscribe_tmdb",
|
||||||
|
name="订阅元数据更新",
|
||||||
|
hours=6,
|
||||||
|
kwargs={
|
||||||
|
'job_id': 'subscribe_tmdb'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 订阅状态每隔24小时搜索一次
|
# 订阅状态每隔24小时搜索一次
|
||||||
if settings.SUBSCRIBE_SEARCH:
|
if settings.SUBSCRIBE_SEARCH:
|
||||||
self._scheduler.add_job(SubscribeChain(self._db).search, "interval",
|
self._scheduler.add_job(
|
||||||
hours=24, kwargs={'state': 'R'}, name="订阅搜索")
|
self.start,
|
||||||
|
"interval",
|
||||||
|
id="subscribe_search",
|
||||||
|
name="订阅搜索",
|
||||||
|
hours=24,
|
||||||
|
kwargs={
|
||||||
|
'job_id': 'subscribe_search',
|
||||||
|
'state': 'R'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if settings.SUBSCRIBE_MODE == "spider":
|
if settings.SUBSCRIBE_MODE == "spider":
|
||||||
# 站点首页种子定时刷新模式
|
# 站点首页种子定时刷新模式
|
||||||
triggers = TimerUtils.random_scheduler(num_executions=30)
|
triggers = TimerUtils.random_scheduler(num_executions=30)
|
||||||
for trigger in triggers:
|
for trigger in triggers:
|
||||||
self._scheduler.add_job(SubscribeChain(self._db).refresh, "cron",
|
self._scheduler.add_job(
|
||||||
hour=trigger.hour, minute=trigger.minute, name="订阅刷新")
|
self.start,
|
||||||
|
"cron",
|
||||||
|
id=f"subscribe_refresh|{trigger.hour}:{trigger.minute}",
|
||||||
|
name="订阅刷新",
|
||||||
|
hour=trigger.hour,
|
||||||
|
minute=trigger.minute,
|
||||||
|
kwargs={
|
||||||
|
'job_id': 'subscribe_refresh'
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
# RSS订阅模式
|
# RSS订阅模式
|
||||||
if not settings.SUBSCRIBE_RSS_INTERVAL:
|
if not settings.SUBSCRIBE_RSS_INTERVAL:
|
||||||
settings.SUBSCRIBE_RSS_INTERVAL = 30
|
settings.SUBSCRIBE_RSS_INTERVAL = 30
|
||||||
elif settings.SUBSCRIBE_RSS_INTERVAL < 5:
|
elif settings.SUBSCRIBE_RSS_INTERVAL < 5:
|
||||||
settings.SUBSCRIBE_RSS_INTERVAL = 5
|
settings.SUBSCRIBE_RSS_INTERVAL = 5
|
||||||
self._scheduler.add_job(SubscribeChain(self._db).refresh, "interval",
|
self._scheduler.add_job(
|
||||||
minutes=settings.SUBSCRIBE_RSS_INTERVAL, name="订阅刷新")
|
self.start,
|
||||||
|
"interval",
|
||||||
|
id="subscribe_refresh",
|
||||||
|
name="RSS订阅刷新",
|
||||||
|
minutes=settings.SUBSCRIBE_RSS_INTERVAL,
|
||||||
|
kwargs={
|
||||||
|
'job_id': 'subscribe_refresh'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 下载器文件转移(每5分钟)
|
# 下载器文件转移(每5分钟)
|
||||||
if settings.DOWNLOADER_MONITOR:
|
if settings.DOWNLOADER_MONITOR:
|
||||||
self._scheduler.add_job(TransferChain(self._db).process, "interval", minutes=5, name="下载文件整理")
|
self._scheduler.add_job(
|
||||||
|
self.start,
|
||||||
|
"interval",
|
||||||
|
id="transfer",
|
||||||
|
name="下载文件整理",
|
||||||
|
minutes=5,
|
||||||
|
kwargs={
|
||||||
|
'job_id': 'transfer'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 公共定时服务
|
# 公共定时服务
|
||||||
self._scheduler.add_job(SchedulerChain(self._db).scheduler_job, "interval", minutes=10)
|
self._scheduler.add_job(
|
||||||
|
SchedulerChain(self._db).scheduler_job,
|
||||||
|
"interval",
|
||||||
|
minutes=10
|
||||||
|
)
|
||||||
|
|
||||||
# 打印服务
|
# 打印服务
|
||||||
logger.debug(self._scheduler.print_jobs())
|
logger.debug(self._scheduler.print_jobs())
|
||||||
@@ -98,11 +196,54 @@ class Scheduler(metaclass=Singleton):
|
|||||||
# 启动定时服务
|
# 启动定时服务
|
||||||
self._scheduler.start()
|
self._scheduler.start()
|
||||||
|
|
||||||
def list(self):
|
def start(self, job_id: str, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
启动定时服务
|
||||||
|
"""
|
||||||
|
# 处理job_id格式
|
||||||
|
job = self._jobs.get(job_id)
|
||||||
|
if not job:
|
||||||
|
return
|
||||||
|
if job.get("running"):
|
||||||
|
logger.warning(f"定时任务 {job_id} 正在运行 ...")
|
||||||
|
return
|
||||||
|
self._jobs[job_id]["running"] = True
|
||||||
|
try:
|
||||||
|
job["func"](*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"定时任务 {job_id} 执行失败:{e}")
|
||||||
|
self._jobs[job_id]["running"] = False
|
||||||
|
|
||||||
|
def list(self) -> List[schemas.ScheduleInfo]:
|
||||||
"""
|
"""
|
||||||
当前所有任务
|
当前所有任务
|
||||||
"""
|
"""
|
||||||
return self._scheduler.get_jobs()
|
# 返回计时任务
|
||||||
|
schedulers = []
|
||||||
|
# 去重
|
||||||
|
added = []
|
||||||
|
jobs = self._scheduler.get_jobs()
|
||||||
|
# 按照下次运行时间排序
|
||||||
|
jobs.sort(key=lambda x: x.next_run_time)
|
||||||
|
for job in jobs:
|
||||||
|
if job.name not in added:
|
||||||
|
added.append(job.name)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
job_id = job.id.split("|")[0]
|
||||||
|
if not self._jobs.get(job_id):
|
||||||
|
continue
|
||||||
|
# 任务状态
|
||||||
|
status = "正在运行" if self._jobs[job_id].get("running") else "等待"
|
||||||
|
# 下次运行时间
|
||||||
|
next_run = TimerUtils.time_difference(job.next_run_time)
|
||||||
|
schedulers.append(schemas.ScheduleInfo(
|
||||||
|
id=job_id,
|
||||||
|
name=job.name,
|
||||||
|
status=status,
|
||||||
|
next_run=next_run
|
||||||
|
))
|
||||||
|
return schedulers
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ class SystemConfigKey(Enum):
|
|||||||
SubscribeFilterRules = "SubscribeFilterRules"
|
SubscribeFilterRules = "SubscribeFilterRules"
|
||||||
# 洗版规则
|
# 洗版规则
|
||||||
BestVersionFilterRules = "BestVersionFilterRules"
|
BestVersionFilterRules = "BestVersionFilterRules"
|
||||||
# 默认包含与排除规则
|
# 默认过滤规则
|
||||||
DefaultIncludeExcludeFilter = "DefaultIncludeExcludeFilter"
|
DefaultFilterRules = "DefaultFilterRules"
|
||||||
# 转移屏蔽词
|
# 转移屏蔽词
|
||||||
TransferExcludeWords = "TransferExcludeWords"
|
TransferExcludeWords = "TransferExcludeWords"
|
||||||
|
|
||||||
|
|||||||
@@ -17,19 +17,18 @@ class SiteUtils:
|
|||||||
if html.xpath("//input[@type='password']"):
|
if html.xpath("//input[@type='password']"):
|
||||||
return False
|
return False
|
||||||
# 是否存在登出和用户面板等链接
|
# 是否存在登出和用户面板等链接
|
||||||
xpaths = ['//a[contains(@href, "logout")'
|
xpaths = [
|
||||||
' or contains(@data-url, "logout")'
|
'//a[contains(@href, "logout")'
|
||||||
' or contains(@href, "mybonus") '
|
' or contains(@data-url, "logout")'
|
||||||
' or contains(@onclick, "logout")'
|
' or contains(@href, "mybonus") '
|
||||||
' or contains(@href, "usercp")]',
|
' or contains(@onclick, "logout")'
|
||||||
'//form[contains(@action, "logout")]']
|
' or contains(@href, "usercp")]',
|
||||||
|
'//form[contains(@action, "logout")]',
|
||||||
|
'//div[@class="user-info-side"]'
|
||||||
|
]
|
||||||
for xpath in xpaths:
|
for xpath in xpaths:
|
||||||
if html.xpath(xpath):
|
if html.xpath(xpath):
|
||||||
return True
|
return True
|
||||||
user_info_div = html.xpath('//div[@class="user-info-side"]')
|
|
||||||
if user_info_div:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -343,9 +343,9 @@ class SystemUtils:
|
|||||||
# 获取当前容器的 ID
|
# 获取当前容器的 ID
|
||||||
with open('/proc/self/mountinfo', 'r') as f:
|
with open('/proc/self/mountinfo', 'r') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
index_resolv_conf = data.find("resolv.conf")
|
index_resolv_conf = data.find("/sys/fs/cgroup/devices")
|
||||||
if index_resolv_conf != -1:
|
if index_resolv_conf != -1:
|
||||||
index_second_slash = data.rfind("/", 0, index_resolv_conf)
|
index_second_slash = data.rfind(" ", 0, index_resolv_conf)
|
||||||
index_first_slash = data.rfind("/", 0, index_second_slash) + 1
|
index_first_slash = data.rfind("/", 0, index_second_slash) + 1
|
||||||
container_id = data[index_first_slash:index_second_slash]
|
container_id = data[index_first_slash:index_second_slash]
|
||||||
if not container_id:
|
if not container_id:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
APP_VERSION = 'v1.2.4'
|
APP_VERSION = 'v1.2.5'
|
||||||
|
|||||||
Reference in New Issue
Block a user