豆瓣想看移植为插件
This commit is contained in:
parent
3b5863befa
commit
478c0cb5b6
@ -26,8 +26,7 @@ ENV LANG="C.UTF-8" \
|
||||
QB_PASSWORD="adminadmin" \
|
||||
MEDIASERVER="emby" \
|
||||
EMBY_HOST="http://127.0.0.1:8096" \
|
||||
EMBY_API_KEY="" \
|
||||
DOUBAN_USER_IDS=""
|
||||
EMBY_API_KEY=""
|
||||
WORKDIR "/app"
|
||||
COPY . .
|
||||
RUN apt-get update \
|
||||
|
@ -55,7 +55,6 @@ docker pull jxxghp/moviepilot:latest
|
||||
- **TORRENT_TAG:** 种子标签,默认为`MOVIEPILOT`,设置后只有MoviePilot添加的下载才会处理,留空所有下载器中的任务均会处理
|
||||
- **LIBRARY_PATH:** 媒体库目录,**注意:需要将`moviepilot`的映射路径与宿主机`真实路径`保持一致**,多个目录使用`,`分隔
|
||||
- **LIBRARY_CATEGORY:** 媒体库二级分类开关,`true`/`false`,默认`false`,开启后会根据配置`category.yaml`自动在媒体库目录下建立二级目录分类
|
||||
- **DOUBAN_USER_IDS:** 豆瓣用户ID,用于同步豆瓣标记的`想看`数据,自动添加订阅,多个用户使用,分隔
|
||||
- **TRANSFER_TYPE:** 转移方式,支持`link`/`copy`/`move`/`softlink`
|
||||
- **COOKIECLOUD_HOST:** CookieCloud服务器地址,格式:`http://ip:port`,必须配置,否则无法添加站点
|
||||
- **COOKIECLOUD_KEY:** CookieCloud用户KEY
|
||||
|
@ -1,18 +1,11 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from typing import Union
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.search import SearchChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import Context
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.helper.rss import RssHelper
|
||||
from app.log import logger
|
||||
from app.schemas import MediaType, Notification, MessageChannel
|
||||
from app.schemas import MediaType
|
||||
|
||||
|
||||
class DoubanChain(ChainBase):
|
||||
@ -20,17 +13,6 @@ class DoubanChain(ChainBase):
|
||||
豆瓣处理链
|
||||
"""
|
||||
|
||||
_interests_url: str = "https://www.douban.com/feed/people/%s/interests"
|
||||
|
||||
_cache_path: Path = settings.TEMP_PATH / "__doubansync_cache__"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.rsshelper = RssHelper()
|
||||
self.downloadchain = DownloadChain()
|
||||
self.searchchain = SearchChain()
|
||||
self.subscribechain = SubscribeChain()
|
||||
|
||||
def recognize_by_doubanid(self, doubanid: str) -> Optional[Context]:
|
||||
"""
|
||||
根据豆瓣ID识别媒体信息
|
||||
@ -96,99 +78,3 @@ class DoubanChain(ChainBase):
|
||||
"""
|
||||
return self.run_module("douban_discover", mtype=mtype, sort=sort, tags=tags,
|
||||
page=page, count=count)
|
||||
|
||||
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):
|
||||
"""
|
||||
通过用户RSS同步豆瓣想看数据
|
||||
"""
|
||||
if not settings.DOUBAN_USER_IDS:
|
||||
return
|
||||
# 读取缓存
|
||||
caches = self._cache_path.read_text().split("\n") if self._cache_path.exists() else []
|
||||
for user_id in settings.DOUBAN_USER_IDS.split(","):
|
||||
# 同步每个用户的豆瓣数据
|
||||
if not user_id:
|
||||
continue
|
||||
logger.info(f"开始同步用户 {user_id} 的豆瓣想看数据 ...")
|
||||
url = self._interests_url % user_id
|
||||
results = self.rsshelper.parse(url)
|
||||
if not results:
|
||||
logger.error(f"未获取到用户 {user_id} 豆瓣RSS数据:{url}")
|
||||
return
|
||||
# 解析数据
|
||||
for result in results:
|
||||
dtype = result.get("title", "")[:2]
|
||||
title = result.get("title", "")[2:]
|
||||
if dtype not in ["想看"]:
|
||||
continue
|
||||
if not result.get("link"):
|
||||
continue
|
||||
douban_id = result.get("link", "").split("/")[-2]
|
||||
if not douban_id or douban_id in caches:
|
||||
continue
|
||||
# 根据豆瓣ID获取豆瓣数据
|
||||
doubaninfo: Optional[dict] = self.douban_info(doubanid=douban_id)
|
||||
if not doubaninfo:
|
||||
logger.warn(f'未获取到豆瓣信息,标题:{title},豆瓣ID:{douban_id}')
|
||||
continue
|
||||
logger.info(f'获取到豆瓣信息,标题:{title},豆瓣ID:{douban_id}')
|
||||
# 识别媒体信息
|
||||
meta = MetaInfo(doubaninfo.get("original_title") or doubaninfo.get("title"))
|
||||
if doubaninfo.get("year"):
|
||||
meta.year = doubaninfo.get("year")
|
||||
mediainfo: MediaInfo = self.recognize_media(meta=meta)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{title},豆瓣ID:{douban_id}')
|
||||
continue
|
||||
# 加入缓存
|
||||
caches.append(douban_id)
|
||||
# 查询缺失的媒体信息
|
||||
exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=meta, mediainfo=mediainfo)
|
||||
if exist_flag:
|
||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||
continue
|
||||
logger.info(f'{mediainfo.title_year} 媒体库中不存在,开始搜索 ...')
|
||||
# 搜索
|
||||
contexts = self.searchchain.process(mediainfo=mediainfo,
|
||||
no_exists=no_exists)
|
||||
if not contexts:
|
||||
logger.warn(f'{mediainfo.title_year} 未搜索到资源')
|
||||
# 添加订阅
|
||||
self.subscribechain.add(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
mtype=mediainfo.type,
|
||||
tmdbid=mediainfo.tmdb_id,
|
||||
season=meta.begin_season,
|
||||
exist_ok=True,
|
||||
username="豆瓣想看")
|
||||
continue
|
||||
# 自动下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=contexts, no_exists=no_exists)
|
||||
if downloads and not lefts:
|
||||
# 全部下载完成
|
||||
logger.info(f'{mediainfo.title_year} 下载完成')
|
||||
else:
|
||||
# 未完成下载
|
||||
logger.info(f'{mediainfo.title_year} 未下载未完整,添加订阅 ...')
|
||||
# 添加订阅
|
||||
self.subscribechain.add(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
mtype=mediainfo.type,
|
||||
tmdbid=mediainfo.tmdb_id,
|
||||
season=meta.begin_season,
|
||||
exist_ok=True,
|
||||
username="豆瓣想看")
|
||||
|
||||
logger.info(f"用户 {user_id} 豆瓣想看同步完成")
|
||||
# 保存缓存
|
||||
self._cache_path.write_text("\n".join(caches))
|
||||
|
@ -4,19 +4,18 @@ from typing import Any, Union
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.chain.cookiecloud import CookieCloudChain
|
||||
from app.chain.douban import DoubanChain
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.site import SiteChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.event import Event as ManagerEvent
|
||||
from app.core.event import eventmanager, EventManager
|
||||
from app.core.plugin import PluginManager
|
||||
from app.core.event import Event as ManagerEvent
|
||||
from app.log import logger
|
||||
from app.schemas.types import EventType, MessageChannel
|
||||
from app.utils.object import ObjectUtils
|
||||
from app.utils.singleton import Singleton
|
||||
from app.schemas.types import EventType, MessageChannel
|
||||
|
||||
|
||||
class CommandChian(ChainBase):
|
||||
@ -70,11 +69,6 @@ class Command(metaclass=Singleton):
|
||||
"description": "禁用站点",
|
||||
"data": {}
|
||||
},
|
||||
"/douban_sync": {
|
||||
"func": DoubanChain().remote_sync,
|
||||
"description": "同步豆瓣想看",
|
||||
"data": {}
|
||||
},
|
||||
"/mediaserver_sync": {
|
||||
"func": MediaServerChain().remote_sync,
|
||||
"description": "同步媒体服务器",
|
||||
|
@ -143,8 +143,6 @@ class Settings(BaseSettings):
|
||||
LIBRARY_PATH: str = None
|
||||
# 二级分类
|
||||
LIBRARY_CATEGORY: bool = True
|
||||
# 豆瓣用户ID,用于同步豆瓣数据,使用,分隔
|
||||
DOUBAN_USER_IDS: str = ""
|
||||
# 电影重命名格式
|
||||
MOVIE_RENAME_FORMAT: str = "{{title}}{% if year %} ({{year}}){% endif %}" \
|
||||
"/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}" \
|
||||
|
@ -7,6 +7,8 @@ from app.core.config import settings
|
||||
from app.db.models import Base
|
||||
from app.db.plugindata_oper import PluginDataOper
|
||||
from app.db.systemconfig_oper import SystemConfigOper
|
||||
from app.helper.message import MessageHelper
|
||||
from app.schemas import Notification, NotificationType, MessageChannel
|
||||
|
||||
|
||||
class PluginChian(ChainBase):
|
||||
@ -34,9 +36,14 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
plugin_desc: str = ""
|
||||
|
||||
def __init__(self):
|
||||
# 插件数据
|
||||
self.plugindata = PluginDataOper()
|
||||
# 处理链
|
||||
self.chain = PluginChian()
|
||||
# 系统配置
|
||||
self.systemconfig = SystemConfigOper()
|
||||
# 系统消息
|
||||
self.systemmessage = MessageHelper()
|
||||
|
||||
@abstractmethod
|
||||
def init_plugin(self, config: dict = None):
|
||||
@ -139,3 +146,13 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
:param key: 数据key
|
||||
"""
|
||||
return self.plugindata.get_data(key)
|
||||
|
||||
def post_message(self, channel: MessageChannel = None, mtype: NotificationType = None, title: str = None,
|
||||
text: str = None, image: str = None, link: str = None, userid: str = None):
|
||||
"""
|
||||
发送消息
|
||||
"""
|
||||
self.chain.post_message(Notification(
|
||||
channel=channel, mtype=mtype, title=title, text=text,
|
||||
image=image, link=link, userid=userid
|
||||
))
|
||||
|
@ -2,8 +2,7 @@ import traceback
|
||||
from datetime import datetime
|
||||
from multiprocessing.dummy import Pool as ThreadPool
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from threading import Event
|
||||
from typing import Any, List, Dict, Tuple
|
||||
from typing import Any, List, Dict, Tuple, Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
@ -11,20 +10,19 @@ from apscheduler.triggers.cron import CronTrigger
|
||||
from ruamel.yaml import CommentedMap
|
||||
|
||||
from app import schemas
|
||||
from app.core.event import EventManager, eventmanager
|
||||
from app.core.config import settings
|
||||
from app.core.event import EventManager, eventmanager, Event
|
||||
from app.helper.browser import PlaywrightHelper
|
||||
from app.helper.cloudflare import under_challenge
|
||||
from app.helper.module import ModuleHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas import Notification
|
||||
from app.schemas.types import EventType, NotificationType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.site import SiteUtils
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.timer import TimerUtils
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
class AutoSignIn(_PluginBase):
|
||||
@ -54,7 +52,7 @@ class AutoSignIn(_PluginBase):
|
||||
# 事件管理器
|
||||
event: EventManager = None
|
||||
# 定时器
|
||||
_scheduler = None
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
# 加载的模块
|
||||
_site_schema: list = []
|
||||
|
||||
@ -92,9 +90,12 @@ class AutoSignIn(_PluginBase):
|
||||
if self._cron:
|
||||
try:
|
||||
self._scheduler.add_job(func=self.sign_in,
|
||||
trigger=CronTrigger.from_crontab(self._cron))
|
||||
trigger=CronTrigger.from_crontab(self._cron),
|
||||
name="站点自动签到")
|
||||
except Exception as err:
|
||||
logger.error(f"定时任务配置错误:{err}")
|
||||
# 推送实时消息
|
||||
self.systemmessage.put(f"执行周期配置错误:{err}")
|
||||
else:
|
||||
# 随机时间
|
||||
triggers = TimerUtils.random_scheduler(num_executions=2,
|
||||
@ -104,7 +105,8 @@ class AutoSignIn(_PluginBase):
|
||||
min_interval=6 * 60)
|
||||
for trigger in triggers:
|
||||
self._scheduler.add_job(self.sign_in, "cron",
|
||||
hour=trigger.hour, minute=trigger.minute)
|
||||
hour=trigger.hour, minute=trigger.minute,
|
||||
name="站点自动签到")
|
||||
|
||||
# 启动任务
|
||||
if self._scheduler.get_jobs():
|
||||
@ -183,7 +185,7 @@ class AutoSignIn(_PluginBase):
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '签到通知',
|
||||
'label': '发送通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -205,7 +207,7 @@ class AutoSignIn(_PluginBase):
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '0 9,18 * * *'
|
||||
'placeholder': '5位cron表达式,留空自动'
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -253,7 +255,7 @@ class AutoSignIn(_PluginBase):
|
||||
], {
|
||||
"enabled": False,
|
||||
"notify": True,
|
||||
"cron": "1 9,18 * * *",
|
||||
"cron": "",
|
||||
"queue_cnt": 5,
|
||||
"sign_sites": []
|
||||
}
|
||||
@ -270,7 +272,10 @@ class AutoSignIn(_PluginBase):
|
||||
自动签到
|
||||
"""
|
||||
if event:
|
||||
logger.info("收到远程签到命令,开始执行签到任务 ...")
|
||||
logger.info("收到命令,开始站点签到 ...")
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="开始站点签到 ...",
|
||||
userid=event.event_data.get("user"))
|
||||
# 查询签到站点
|
||||
sign_sites = [site for site in self.sites.get_indexers() if not site.get("public")]
|
||||
# 过滤掉没有选中的站点
|
||||
@ -297,10 +302,17 @@ class AutoSignIn(_PluginBase):
|
||||
} for s in status])
|
||||
# 发送通知
|
||||
if self._notify:
|
||||
self.chain.post_message(Notification(title="站点自动签到",
|
||||
text="\n".join([f'【{s[0]}】{s[1]}' for s in status if s])))
|
||||
self.post_message(title="站点自动签到",
|
||||
mtype=NotificationType.SiteMessage,
|
||||
text="\n".join([f'【{s[0]}】{s[1]}' for s in status if s]))
|
||||
if event:
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="站点签到完成!", userid=event.event_data.get("user"))
|
||||
else:
|
||||
logger.error("站点签到任务失败!")
|
||||
if event:
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="站点签到任务失败!", userid=event.event_data.get("user"))
|
||||
|
||||
def __build_class(self, url) -> Any:
|
||||
for site_schema in self._site_schema:
|
||||
|
356
app/plugins/doubansync/__init__.py
Normal file
356
app/plugins/doubansync/__init__.py
Normal file
@ -0,0 +1,356 @@
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Optional, Any, List, Dict, Tuple
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.search import SearchChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import Event
|
||||
from app.core.event import eventmanager
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.helper.rss import RssHelper
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas.types import EventType
|
||||
|
||||
lock = Lock()
|
||||
|
||||
|
||||
class DoubanSync(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "豆瓣想看"
|
||||
# 插件描述
|
||||
plugin_desc = "同步豆瓣想看数据,自动添加订阅。"
|
||||
# 插件图标
|
||||
plugin_icon = "douban.png"
|
||||
# 主题色
|
||||
plugin_color = "#05B711"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/jxxghp"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "doubansync_"
|
||||
# 加载顺序
|
||||
plugin_order = 3
|
||||
# 可使用的用户级别
|
||||
auth_level = 2
|
||||
|
||||
# 私有变量
|
||||
_interests_url: str = "https://www.douban.com/feed/people/%s/interests"
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
_cache_path: Optional[Path] = None
|
||||
rsshelper = None
|
||||
downloadchain = None
|
||||
searchchain = None
|
||||
subscribechain = None
|
||||
|
||||
# 配置属性
|
||||
_enabled: bool = False
|
||||
_cron: str = ""
|
||||
_notify: bool = False
|
||||
_days: int = 7
|
||||
_users: str = ""
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
self._cache_path = settings.TEMP_PATH / "__doubansync_cache__"
|
||||
self.rsshelper = RssHelper()
|
||||
self.downloadchain = DownloadChain()
|
||||
self.searchchain = SearchChain()
|
||||
self.subscribechain = SubscribeChain()
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
# 配置
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._cron = config.get("cron")
|
||||
self._notify = config.get("notify")
|
||||
self._days = config.get("days")
|
||||
self._users = config.get("users")
|
||||
|
||||
if self._enabled:
|
||||
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
if self._cron:
|
||||
try:
|
||||
self._scheduler.add_job(func=self.sync,
|
||||
trigger=CronTrigger.from_crontab(self._cron),
|
||||
name="豆瓣想看")
|
||||
except Exception as err:
|
||||
logger.error(f"定时任务配置错误:{err}")
|
||||
# 推送实时消息
|
||||
self.systemmessage.put(f"执行周期配置错误:{err}")
|
||||
else:
|
||||
self._scheduler.add_job(self.sync, "interval", minutes=30, name="豆瓣想看")
|
||||
|
||||
# 启动任务
|
||||
if self._scheduler.get_jobs():
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
定义远程控制命令
|
||||
:return: 命令关键字、事件、描述、附带数据
|
||||
"""
|
||||
return [{
|
||||
"cmd": "/douban_sync",
|
||||
"event": EventType.DoubanSync,
|
||||
"desc": "同步豆瓣想看",
|
||||
"data": {}
|
||||
}]
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取插件API
|
||||
[{
|
||||
"path": "/xx",
|
||||
"endpoint": self.xxx,
|
||||
"methods": ["GET", "POST"],
|
||||
"summary": "API说明"
|
||||
}]
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
|
||||
"""
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '发送通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '5位cron表达式,留空自动'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'days',
|
||||
'label': '同步天数'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'users',
|
||||
'label': '用户列表',
|
||||
'placeholder': '豆瓣用户ID,多个用英文逗号分隔'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"notify": True,
|
||||
"cron": "*/30 * * * *",
|
||||
"days": 7,
|
||||
"users": "",
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
"""
|
||||
拼装插件详情页面,需要返回页面配置,同时附带数据
|
||||
"""
|
||||
pass
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
try:
|
||||
if self._scheduler:
|
||||
self._scheduler.remove_all_jobs()
|
||||
if self._scheduler.running:
|
||||
self._scheduler.shutdown()
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
logger.error("退出插件失败:%s" % str(e))
|
||||
|
||||
def sync(self):
|
||||
"""
|
||||
通过用户RSS同步豆瓣想看数据
|
||||
"""
|
||||
if not self._users:
|
||||
return
|
||||
# 读取缓存
|
||||
caches = self._cache_path.read_text().split("\n") if self._cache_path.exists() else []
|
||||
for user_id in self._users.split(","):
|
||||
# 同步每个用户的豆瓣数据
|
||||
if not user_id:
|
||||
continue
|
||||
logger.info(f"开始同步用户 {user_id} 的豆瓣想看数据 ...")
|
||||
url = self._interests_url % user_id
|
||||
results = self.rsshelper.parse(url)
|
||||
if not results:
|
||||
logger.error(f"未获取到用户 {user_id} 豆瓣RSS数据:{url}")
|
||||
return
|
||||
# 解析数据
|
||||
for result in results:
|
||||
dtype = result.get("title", "")[:2]
|
||||
title = result.get("title", "")[2:]
|
||||
if dtype not in ["想看"]:
|
||||
continue
|
||||
if not result.get("link"):
|
||||
continue
|
||||
# TODO 判断是否在天数范围
|
||||
douban_id = result.get("link", "").split("/")[-2]
|
||||
if not douban_id or douban_id in caches:
|
||||
continue
|
||||
# 根据豆瓣ID获取豆瓣数据
|
||||
doubaninfo: Optional[dict] = self.chain.douban_info(doubanid=douban_id)
|
||||
if not doubaninfo:
|
||||
logger.warn(f'未获取到豆瓣信息,标题:{title},豆瓣ID:{douban_id}')
|
||||
continue
|
||||
logger.info(f'获取到豆瓣信息,标题:{title},豆瓣ID:{douban_id}')
|
||||
# 识别媒体信息
|
||||
meta = MetaInfo(doubaninfo.get("original_title") or doubaninfo.get("title"))
|
||||
if doubaninfo.get("year"):
|
||||
meta.year = doubaninfo.get("year")
|
||||
mediainfo: MediaInfo = self.chain.recognize_media(meta=meta)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{title},豆瓣ID:{douban_id}')
|
||||
continue
|
||||
# 加入缓存
|
||||
caches.append(douban_id)
|
||||
# 查询缺失的媒体信息
|
||||
exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=meta, mediainfo=mediainfo)
|
||||
if exist_flag:
|
||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||
continue
|
||||
logger.info(f'{mediainfo.title_year} 媒体库中不存在,开始搜索 ...')
|
||||
# 搜索
|
||||
contexts = self.searchchain.process(mediainfo=mediainfo,
|
||||
no_exists=no_exists)
|
||||
if not contexts:
|
||||
logger.warn(f'{mediainfo.title_year} 未搜索到资源')
|
||||
# 添加订阅
|
||||
self.subscribechain.add(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
mtype=mediainfo.type,
|
||||
tmdbid=mediainfo.tmdb_id,
|
||||
season=meta.begin_season,
|
||||
exist_ok=True,
|
||||
username="豆瓣想看")
|
||||
continue
|
||||
# 自动下载
|
||||
downloads, lefts = self.downloadchain.batch_download(contexts=contexts, no_exists=no_exists)
|
||||
if downloads and not lefts:
|
||||
# 全部下载完成
|
||||
logger.info(f'{mediainfo.title_year} 下载完成')
|
||||
else:
|
||||
# 未完成下载
|
||||
logger.info(f'{mediainfo.title_year} 未下载未完整,添加订阅 ...')
|
||||
# 添加订阅
|
||||
self.subscribechain.add(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
mtype=mediainfo.type,
|
||||
tmdbid=mediainfo.tmdb_id,
|
||||
season=meta.begin_season,
|
||||
exist_ok=True,
|
||||
username="豆瓣想看")
|
||||
|
||||
logger.info(f"用户 {user_id} 豆瓣想看同步完成")
|
||||
# 保存缓存
|
||||
self._cache_path.write_text("\n".join(caches))
|
||||
|
||||
@eventmanager.register(EventType.DoubanSync)
|
||||
def remote_sync(self, event: Event):
|
||||
"""
|
||||
刷新站点数据
|
||||
"""
|
||||
if event:
|
||||
logger.info("收到命令,开始执行豆瓣想看同步 ...")
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="开始同步豆瓣想看 ...",
|
||||
userid=event.event_data.get("user"))
|
||||
self.sync()
|
||||
|
||||
if event:
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="同步豆瓣想看数据完成!", userid=event.event_data.get("user"))
|
@ -1,3 +1,4 @@
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from multiprocessing.dummy import Pool as ThreadPool
|
||||
from threading import Lock
|
||||
@ -10,23 +11,19 @@ from ruamel.yaml import CommentedMap
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager
|
||||
from app.core.event import Event
|
||||
from app.core.event import eventmanager
|
||||
from app.helper.browser import PlaywrightHelper
|
||||
from app.helper.module import ModuleHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.plugins.sitestatistic.siteuserinfo import ISiteUserInfo
|
||||
from app.schemas import Notification
|
||||
from app.schemas.types import EventType, NotificationType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
from app.utils.timer import TimerUtils
|
||||
|
||||
import warnings
|
||||
|
||||
from app.schemas.types import EventType
|
||||
|
||||
warnings.filterwarnings("ignore", category=FutureWarning)
|
||||
|
||||
lock = Lock()
|
||||
@ -56,7 +53,7 @@ class SiteStatistic(_PluginBase):
|
||||
|
||||
# 私有属性
|
||||
sites = None
|
||||
_scheduler = None
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
_last_update_time: Optional[datetime] = None
|
||||
_sites_data: dict = {}
|
||||
_site_schema: List[ISiteUserInfo] = None
|
||||
@ -95,9 +92,12 @@ class SiteStatistic(_PluginBase):
|
||||
if self._cron:
|
||||
try:
|
||||
self._scheduler.add_job(func=self.refresh_all_site_data,
|
||||
trigger=CronTrigger.from_crontab(self._cron))
|
||||
trigger=CronTrigger.from_crontab(self._cron),
|
||||
name="站点数据统计")
|
||||
except Exception as err:
|
||||
logger.error(f"定时任务配置错误:{err}")
|
||||
# 推送实时消息
|
||||
self.systemmessage.put(f"执行周期配置错误:{err}")
|
||||
else:
|
||||
triggers = TimerUtils.random_scheduler(num_executions=1,
|
||||
begin_hour=0,
|
||||
@ -106,7 +106,8 @@ class SiteStatistic(_PluginBase):
|
||||
max_interval=60)
|
||||
for trigger in triggers:
|
||||
self._scheduler.add_job(self.refresh_all_site_data, "cron",
|
||||
hour=trigger.hour, minute=trigger.minute)
|
||||
hour=trigger.hour, minute=trigger.minute,
|
||||
name="站点数据统计")
|
||||
|
||||
# 启动任务
|
||||
if self._scheduler.get_jobs():
|
||||
@ -207,7 +208,7 @@ class SiteStatistic(_PluginBase):
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '0 9,18 * * *'
|
||||
'placeholder': '5位cron表达式,留空自动'
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -467,10 +468,11 @@ class SiteStatistic(_PluginBase):
|
||||
for head, date, content in site_user_info.message_unread_contents:
|
||||
msg_title = f"【站点 {site_user_info.site_name} 消息】"
|
||||
msg_text = f"时间:{date}\n标题:{head}\n内容:\n{content}"
|
||||
self.chain.post_message(Notification(title=msg_title, text=msg_text))
|
||||
self.post_message(mtype=NotificationType.SiteMessage, title=msg_title, text=msg_text)
|
||||
else:
|
||||
self.chain.post_message(Notification(title=f"站点 {site_user_info.site_name} 收到 "
|
||||
f"{site_user_info.message_unread} 条新消息,请登陆查看"))
|
||||
self.post_message(mtype=NotificationType.SiteMessage,
|
||||
title=f"站点 {site_user_info.site_name} 收到 "
|
||||
f"{site_user_info.message_unread} 条新消息,请登陆查看")
|
||||
|
||||
@eventmanager.register(EventType.SiteStatistic)
|
||||
def refresh(self, event: Event):
|
||||
@ -478,8 +480,14 @@ class SiteStatistic(_PluginBase):
|
||||
刷新站点数据
|
||||
"""
|
||||
if event:
|
||||
logger.info("收到命令,开始执行站点数据刷新 ...")
|
||||
logger.info("收到命令,开始刷新站点数据 ...")
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="开始刷新站点数据 ...",
|
||||
userid=event.event_data.get("user"))
|
||||
self.refresh_all_site_data(force=True)
|
||||
if event:
|
||||
self.post_message(channel=event.event_data.get("channel"),
|
||||
title="站点数据刷新完成!", userid=event.event_data.get("user"))
|
||||
|
||||
def refresh_all_site_data(self, force: bool = False, specify_sites: list = None):
|
||||
"""
|
||||
@ -555,6 +563,7 @@ class SiteStatistic(_PluginBase):
|
||||
f"总上传:{StringUtils.str_filesize(incUploads)}\n"
|
||||
f"总下载:{StringUtils.str_filesize(incDownloads)}\n"
|
||||
f"————————————")
|
||||
self.chain.post_message(Notification(title="站点数据统计", text="\n".join(messages)))
|
||||
self.post_message(mtype=NotificationType.SiteMessage,
|
||||
title="站点数据统计", text="\n".join(messages))
|
||||
|
||||
logger.info("站点数据刷新完成")
|
||||
|
@ -7,7 +7,6 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.chain.cookiecloud import CookieCloudChain
|
||||
from app.chain.douban import DoubanChain
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.chain.transfer import TransferChain
|
||||
@ -70,9 +69,6 @@ class Scheduler(metaclass=Singleton):
|
||||
self._scheduler.add_job(SubscribeChain().refresh, "cron",
|
||||
hour=trigger.hour, minute=trigger.minute, name="订阅刷新")
|
||||
|
||||
# 豆瓣同步(每30分钟)
|
||||
self._scheduler.add_job(DoubanChain().sync, "interval", minutes=30, name="同步豆瓣想看")
|
||||
|
||||
# 下载器文件转移(每5分钟)
|
||||
self._scheduler.add_job(TransferChain().process, "interval", minutes=5, name="下载文件整理")
|
||||
|
||||
|
@ -22,6 +22,8 @@ class EventType(Enum):
|
||||
SiteSignin = "site.signin"
|
||||
# 站点数据统计
|
||||
SiteStatistic = "site.statistic"
|
||||
# 豆瓣想看
|
||||
DoubanSync = "douban.sync"
|
||||
# Webhook消息
|
||||
WebhookMessage = "webhook.message"
|
||||
# 转移完成
|
||||
|
Loading…
x
Reference in New Issue
Block a user