mirror of
https://github.com/d0zingcat/MoviePilot-Plugins.git
synced 2026-05-23 15:09:42 +00:00
Merge branch 'jxxghp:main' into main
This commit is contained in:
@@ -79,7 +79,6 @@
|
||||
"icon": "douban.png",
|
||||
"author": "jxxghp",
|
||||
"level": 2,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.9.1": "修复版本兼容问题",
|
||||
"v1.9": "请求豆瓣RSS时增加请求头",
|
||||
@@ -123,7 +122,6 @@
|
||||
"icon": "movie.jpg",
|
||||
"author": "jxxghp",
|
||||
"level": 2,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.9.1": "优化媒体类型的判断处理",
|
||||
"v1.9": "增强API安全性",
|
||||
@@ -491,11 +489,14 @@
|
||||
"name": "Bark消息推送",
|
||||
"description": "支持使用Bark发送消息通知。",
|
||||
"labels": "消息通知",
|
||||
"version": "1.1",
|
||||
"version": "1.2",
|
||||
"icon": "Bark_A.png",
|
||||
"author": "jxxghp",
|
||||
"level": 1,
|
||||
"v2": true
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.2": "支持多人消息发送"
|
||||
}
|
||||
},
|
||||
"PushDeerMsg": {
|
||||
"name": "PushDeer消息推送",
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
"name": "站点数据统计",
|
||||
"description": "站点统计数据图表。",
|
||||
"labels": "站点,仪表板",
|
||||
"version": "1.5",
|
||||
"version": "1.6",
|
||||
"icon": "statistic.png",
|
||||
"author": "lightolly,jxxghp",
|
||||
"level": 2,
|
||||
"history": {
|
||||
"v1.6": "优化了站点数据获取失败时的回退逻辑",
|
||||
"v1.5": "修复了发送增量通知失败等一些问题",
|
||||
"v1.4.1": "支持数据刷新时发送消息通知",
|
||||
"v1.3": "远程刷新命令移植到主程序",
|
||||
@@ -20,11 +21,13 @@
|
||||
"name": "站点刷流",
|
||||
"description": "自动托管刷流,将会提高对应站点的访问频率。",
|
||||
"labels": "刷流,仪表板",
|
||||
"version": "4.2",
|
||||
"version": "4.3.1",
|
||||
"icon": "brush.jpg",
|
||||
"author": "jxxghp,InfinityPacer",
|
||||
"level": 2,
|
||||
"history": {
|
||||
"v4.3.1": "修复了一些细节问题",
|
||||
"v4.3": "支持带宽采样并计算平均值,以优化刷流效率",
|
||||
"v4.2": "优化执行周期输入,需要MoviePilot v2.2.1+",
|
||||
"v4.1": "支持通过CRON表达式配置开启时间,固定10分钟为执行周期",
|
||||
"v4.0": "站点独立配置项支持配置NexusPHP 站点自动跳过下载提示页",
|
||||
@@ -317,5 +320,29 @@
|
||||
"v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+",
|
||||
"v2.0": "兼容MoviePilot V2"
|
||||
}
|
||||
},
|
||||
"DoubanRank": {
|
||||
"name": "豆瓣榜单订阅",
|
||||
"description": "监控豆瓣热门榜单,自动添加订阅。",
|
||||
"labels": "订阅",
|
||||
"version": "2.0.0",
|
||||
"icon": "movie.jpg",
|
||||
"author": "jxxghp",
|
||||
"level": 2,
|
||||
"history": {
|
||||
"v2.0.0": "优化cron表达式输入"
|
||||
}
|
||||
},
|
||||
"DoubanSync": {
|
||||
"name": "豆瓣想看",
|
||||
"description": "同步豆瓣想看数据,自动添加订阅。",
|
||||
"labels": "订阅",
|
||||
"version": "2.0.0",
|
||||
"icon": "douban.png",
|
||||
"author": "jxxghp",
|
||||
"level": 2,
|
||||
"history": {
|
||||
"v2.0.0": "优化cron表达式输入"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ class BrushFlow(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "brush.jpg"
|
||||
# 插件版本
|
||||
plugin_version = "4.2"
|
||||
plugin_version = "4.3.1"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp,InfinityPacer"
|
||||
# 作者主页
|
||||
@@ -435,7 +435,7 @@ class BrushFlow(_PluginBase):
|
||||
if self._task_brush_enable:
|
||||
if brush_config.cron:
|
||||
values = brush_config.cron.split()
|
||||
values[0] = f"{datetime.now().minute % 10}/5"
|
||||
values[0] = f"{datetime.now().minute % 10}/10"
|
||||
cron = " ".join(values)
|
||||
logger.info(f"站点刷流定时服务启动,执行周期 {cron}")
|
||||
cron_trigger = CronTrigger.from_crontab(cron)
|
||||
@@ -2142,16 +2142,15 @@ class BrushFlow(_PluginBase):
|
||||
]
|
||||
|
||||
if include_network_conditions:
|
||||
downloader_info = self.__get_downloader_info()
|
||||
if downloader_info:
|
||||
current_upload_speed = downloader_info.upload_speed or 0
|
||||
current_download_speed = downloader_info.download_speed or 0
|
||||
# 获取平均带宽
|
||||
avg_upload_speed, avg_download_speed = self.__get_average_bandwidth()
|
||||
if avg_upload_speed is not None and avg_download_speed is not None:
|
||||
reasons.extend([
|
||||
("maxupspeed", lambda config: current_upload_speed >= float(config) * 1024,
|
||||
lambda config: f"当前总上传带宽 {StringUtils.str_filesize(current_upload_speed)},"
|
||||
("maxupspeed", lambda config: avg_upload_speed >= float(config) * 1024,
|
||||
lambda config: f"当前总上传带宽 {StringUtils.str_filesize(avg_upload_speed)},"
|
||||
f"已达到最大值 {config} KB/s,暂时停止新增任务"),
|
||||
("maxdlspeed", lambda config: current_download_speed >= float(config) * 1024,
|
||||
lambda config: f"当前总下载带宽 {StringUtils.str_filesize(current_download_speed)},"
|
||||
("maxdlspeed", lambda config: avg_download_speed >= float(config) * 1024,
|
||||
lambda config: f"当前总下载带宽 {StringUtils.str_filesize(avg_download_speed)},"
|
||||
f"已达到最大值 {config} KB/s,暂时停止新增任务"),
|
||||
])
|
||||
|
||||
@@ -3508,6 +3507,32 @@ class BrushFlow(_PluginBase):
|
||||
total_size = sum([task.get("size") or 0 for task in task_info.values()])
|
||||
return total_size
|
||||
|
||||
def __get_average_bandwidth(self, sample_count: int = 5, interval: float = 3.0) \
|
||||
-> Tuple[Optional[float], Optional[float]]:
|
||||
"""
|
||||
多次采样上传和下载带宽,取平均值
|
||||
"""
|
||||
upload_speeds = []
|
||||
download_speeds = []
|
||||
start_time = time.time()
|
||||
for _ in range(sample_count):
|
||||
downloader_info = self.__get_downloader_info()
|
||||
if downloader_info:
|
||||
upload_speeds.append(downloader_info.upload_speed or 0)
|
||||
download_speeds.append(downloader_info.download_speed or 0)
|
||||
# 采样间隔
|
||||
time.sleep(interval)
|
||||
end_time = time.time()
|
||||
total_duration = end_time - start_time
|
||||
if not upload_speeds or not download_speeds:
|
||||
return None, None
|
||||
avg_upload_speed = sum(upload_speeds) / len(upload_speeds) if upload_speeds else 0
|
||||
avg_download_speed = sum(download_speeds) / len(download_speeds) if download_speeds else 0
|
||||
logger.debug(f"平均上传带宽 {StringUtils.str_filesize(avg_upload_speed)}, "
|
||||
f"平均下载带宽 {StringUtils.str_filesize(avg_download_speed)}, "
|
||||
f"采样次数={sample_count}, 时长={total_duration:.2f} 秒")
|
||||
return avg_upload_speed, avg_download_speed
|
||||
|
||||
def __get_downloader_info(self) -> schemas.DownloaderInfo:
|
||||
"""
|
||||
获取下载器实时信息(所有下载器)
|
||||
|
||||
693
plugins.v2/doubanrank/__init__.py
Normal file
693
plugins.v2/doubanrank/__init__.py
Normal file
@@ -0,0 +1,693 @@
|
||||
import datetime
|
||||
import re
|
||||
import xml.dom.minidom
|
||||
from threading import Event
|
||||
from typing import Tuple, List, Dict, Any
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app import schemas
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas import MediaType
|
||||
from app.utils.dom import DomUtils
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class DoubanRank(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "豆瓣榜单订阅"
|
||||
# 插件描述
|
||||
plugin_desc = "监控豆瓣热门榜单,自动添加订阅。"
|
||||
# 插件图标
|
||||
plugin_icon = "movie.jpg"
|
||||
# 插件版本
|
||||
plugin_version = "2.0.0"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/jxxghp"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "doubanrank_"
|
||||
# 加载顺序
|
||||
plugin_order = 6
|
||||
# 可使用的用户级别
|
||||
auth_level = 2
|
||||
|
||||
# 退出事件
|
||||
_event = Event()
|
||||
# 私有属性
|
||||
downloadchain: DownloadChain = None
|
||||
subscribechain: SubscribeChain = None
|
||||
mediachain: MediaChain = None
|
||||
_scheduler = None
|
||||
_douban_address = {
|
||||
'movie-ustop': 'https://rsshub.app/douban/movie/ustop',
|
||||
'movie-weekly': 'https://rsshub.app/douban/movie/weekly',
|
||||
'movie-real-time': 'https://rsshub.app/douban/movie/weekly/movie_real_time_hotest',
|
||||
'show-domestic': 'https://rsshub.app/douban/movie/weekly/show_domestic',
|
||||
'movie-hot-gaia': 'https://rsshub.app/douban/movie/weekly/movie_hot_gaia',
|
||||
'tv-hot': 'https://rsshub.app/douban/movie/weekly/tv_hot',
|
||||
'movie-top250': 'https://rsshub.app/douban/movie/weekly/movie_top250',
|
||||
'movie-top250-full': 'https://rsshub.app/douban/list/movie_top250',
|
||||
}
|
||||
_enabled = False
|
||||
_cron = ""
|
||||
_onlyonce = False
|
||||
_rss_addrs = []
|
||||
_ranks = []
|
||||
_vote = 0
|
||||
_clear = False
|
||||
_clearflag = False
|
||||
_proxy = False
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
self.downloadchain = DownloadChain()
|
||||
self.subscribechain = SubscribeChain()
|
||||
self.mediachain = MediaChain()
|
||||
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._cron = config.get("cron")
|
||||
self._proxy = config.get("proxy")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._vote = float(config.get("vote")) if config.get("vote") else 0
|
||||
rss_addrs = config.get("rss_addrs")
|
||||
if rss_addrs:
|
||||
if isinstance(rss_addrs, str):
|
||||
self._rss_addrs = rss_addrs.split('\n')
|
||||
else:
|
||||
self._rss_addrs = rss_addrs
|
||||
else:
|
||||
self._rss_addrs = []
|
||||
self._ranks = config.get("ranks") or []
|
||||
self._clear = config.get("clear")
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
# 启动服务
|
||||
if self._enabled or self._onlyonce:
|
||||
if self._onlyonce:
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
logger.info("豆瓣榜单订阅服务启动,立即运行一次")
|
||||
self._scheduler.add_job(func=self.__refresh_rss, trigger='date',
|
||||
run_date=datetime.datetime.now(
|
||||
tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3)
|
||||
)
|
||||
|
||||
if self._scheduler.get_jobs():
|
||||
# 启动服务
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
if self._onlyonce or self._clear:
|
||||
# 关闭一次性开关
|
||||
self._onlyonce = False
|
||||
# 记录缓存清理标志
|
||||
self._clearflag = self._clear
|
||||
# 关闭清理缓存
|
||||
self._clear = False
|
||||
# 保存配置
|
||||
self.__update_config()
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取插件API
|
||||
[{
|
||||
"path": "/xx",
|
||||
"endpoint": self.xxx,
|
||||
"methods": ["GET", "POST"],
|
||||
"summary": "API说明"
|
||||
}]
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"path": "/delete_history",
|
||||
"endpoint": self.delete_history,
|
||||
"methods": ["GET"],
|
||||
"summary": "删除豆瓣榜单订阅历史记录"
|
||||
}
|
||||
]
|
||||
|
||||
def get_service(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
注册插件公共服务
|
||||
[{
|
||||
"id": "服务ID",
|
||||
"name": "服务名称",
|
||||
"trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()",
|
||||
"func": self.xxx,
|
||||
"kwargs": {} # 定时器参数
|
||||
}]
|
||||
"""
|
||||
if self._enabled and self._cron:
|
||||
return [
|
||||
{
|
||||
"id": "DoubanRank",
|
||||
"name": "豆瓣榜单订阅服务",
|
||||
"trigger": CronTrigger.from_crontab(self._cron),
|
||||
"func": self.__refresh_rss,
|
||||
"kwargs": {}
|
||||
}
|
||||
]
|
||||
elif self._enabled:
|
||||
return [
|
||||
{
|
||||
"id": "DoubanRank",
|
||||
"name": "豆瓣榜单订阅服务",
|
||||
"trigger": CronTrigger.from_crontab("0 8 * * *"),
|
||||
"func": self.__refresh_rss,
|
||||
"kwargs": {}
|
||||
}
|
||||
]
|
||||
return []
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
'component': 'VForm',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'proxy',
|
||||
'label': '使用代理服务器',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'onlyonce',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCronField',
|
||||
'props': {
|
||||
'model': 'cron',
|
||||
'label': '执行周期',
|
||||
'placeholder': '5位cron表达式,留空自动'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'vote',
|
||||
'label': '评分',
|
||||
'placeholder': '评分大于等于该值才订阅'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSelect',
|
||||
'props': {
|
||||
'chips': True,
|
||||
'multiple': True,
|
||||
'model': 'ranks',
|
||||
'label': '热门榜单',
|
||||
'items': [
|
||||
{'title': '电影北美票房榜', 'value': 'movie-ustop'},
|
||||
{'title': '一周口碑电影榜', 'value': 'movie-weekly'},
|
||||
{'title': '实时热门电影', 'value': 'movie-real-time'},
|
||||
{'title': '热门综艺', 'value': 'show-domestic'},
|
||||
{'title': '热门电影', 'value': 'movie-hot-gaia'},
|
||||
{'title': '热门电视剧', 'value': 'tv-hot'},
|
||||
{'title': '电影TOP10', 'value': 'movie-top250'},
|
||||
{'title': '电影TOP250', 'value': 'movie-top250-full'},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextarea',
|
||||
'props': {
|
||||
'model': 'rss_addrs',
|
||||
'label': '自定义榜单地址',
|
||||
'placeholder': '每行一个地址,如:https://rsshub.app/douban/movie/ustop'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'clear',
|
||||
'label': '清理历史记录',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"cron": "",
|
||||
"proxy": False,
|
||||
"onlyonce": False,
|
||||
"vote": "",
|
||||
"ranks": [],
|
||||
"rss_addrs": "",
|
||||
"clear": False
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
"""
|
||||
拼装插件详情页面,需要返回页面配置,同时附带数据
|
||||
"""
|
||||
# 查询历史记录
|
||||
historys = self.get_data('history')
|
||||
if not historys:
|
||||
return [
|
||||
{
|
||||
'component': 'div',
|
||||
'text': '暂无数据',
|
||||
'props': {
|
||||
'class': 'text-center',
|
||||
}
|
||||
}
|
||||
]
|
||||
# 数据按时间降序排序
|
||||
historys = sorted(historys, key=lambda x: x.get('time'), reverse=True)
|
||||
# 拼装页面
|
||||
contents = []
|
||||
for history in historys:
|
||||
title = history.get("title")
|
||||
poster = history.get("poster")
|
||||
mtype = history.get("type")
|
||||
time_str = history.get("time")
|
||||
doubanid = history.get("doubanid")
|
||||
contents.append(
|
||||
{
|
||||
'component': 'VCard',
|
||||
'content': [
|
||||
{
|
||||
"component": "VDialogCloseBtn",
|
||||
"props": {
|
||||
'innerClass': 'absolute top-0 right-0',
|
||||
},
|
||||
'events': {
|
||||
'click': {
|
||||
'api': 'plugin/DoubanRank/delete_history',
|
||||
'method': 'get',
|
||||
'params': {
|
||||
'key': f"doubanrank: {title} (DB:{doubanid})",
|
||||
'apikey': settings.API_TOKEN
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
'component': 'div',
|
||||
'props': {
|
||||
'class': 'd-flex justify-space-start flex-nowrap flex-row',
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'div',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VImg',
|
||||
'props': {
|
||||
'src': poster,
|
||||
'height': 120,
|
||||
'width': 80,
|
||||
'aspect-ratio': '2/3',
|
||||
'class': 'object-cover shadow ring-gray-500',
|
||||
'cover': True
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'div',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCardTitle',
|
||||
'props': {
|
||||
'class': 'ps-1 pe-5 break-words whitespace-break-spaces'
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'a',
|
||||
'props': {
|
||||
'href': f"https://movie.douban.com/subject/{doubanid}",
|
||||
'target': '_blank'
|
||||
},
|
||||
'text': title
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'类型:{mtype}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'时间:{time_str}'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
'component': 'div',
|
||||
'props': {
|
||||
'class': 'grid gap-3 grid-info-card',
|
||||
},
|
||||
'content': contents
|
||||
}
|
||||
]
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
停止服务
|
||||
"""
|
||||
try:
|
||||
if self._scheduler:
|
||||
self._scheduler.remove_all_jobs()
|
||||
if self._scheduler.running:
|
||||
self._event.set()
|
||||
self._scheduler.shutdown()
|
||||
self._event.clear()
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
|
||||
def delete_history(self, key: str, apikey: str):
|
||||
"""
|
||||
删除同步历史记录
|
||||
"""
|
||||
if apikey != settings.API_TOKEN:
|
||||
return schemas.Response(success=False, message="API密钥错误")
|
||||
# 历史记录
|
||||
historys = self.get_data('history')
|
||||
if not historys:
|
||||
return schemas.Response(success=False, message="未找到历史记录")
|
||||
# 删除指定记录
|
||||
historys = [h for h in historys if h.get("unique") != key]
|
||||
self.save_data('history', historys)
|
||||
return schemas.Response(success=True, message="删除成功")
|
||||
|
||||
def __update_config(self):
|
||||
"""
|
||||
列新配置
|
||||
"""
|
||||
self.update_config({
|
||||
"enabled": self._enabled,
|
||||
"cron": self._cron,
|
||||
"onlyonce": self._onlyonce,
|
||||
"vote": self._vote,
|
||||
"ranks": self._ranks,
|
||||
"rss_addrs": '\n'.join(map(str, self._rss_addrs)),
|
||||
"clear": self._clear
|
||||
})
|
||||
|
||||
def __refresh_rss(self):
|
||||
"""
|
||||
刷新RSS
|
||||
"""
|
||||
logger.info(f"开始刷新豆瓣榜单 ...")
|
||||
addr_list = self._rss_addrs + [self._douban_address.get(rank) for rank in self._ranks]
|
||||
if not addr_list:
|
||||
logger.info(f"未设置榜单RSS地址")
|
||||
return
|
||||
else:
|
||||
logger.info(f"共 {len(addr_list)} 个榜单RSS地址需要刷新")
|
||||
|
||||
# 读取历史记录
|
||||
if self._clearflag:
|
||||
history = []
|
||||
else:
|
||||
history: List[dict] = self.get_data('history') or []
|
||||
|
||||
for addr in addr_list:
|
||||
if not addr:
|
||||
continue
|
||||
try:
|
||||
logger.info(f"获取RSS:{addr} ...")
|
||||
rss_infos = self.__get_rss_info(addr)
|
||||
if not rss_infos:
|
||||
logger.error(f"RSS地址:{addr} ,未查询到数据")
|
||||
continue
|
||||
else:
|
||||
logger.info(f"RSS地址:{addr} ,共 {len(rss_infos)} 条数据")
|
||||
for rss_info in rss_infos:
|
||||
if self._event.is_set():
|
||||
logger.info(f"订阅服务停止")
|
||||
return
|
||||
mtype = None
|
||||
title = rss_info.get('title')
|
||||
douban_id = rss_info.get('doubanid')
|
||||
year = rss_info.get('year')
|
||||
type_str = rss_info.get('type')
|
||||
if type_str == "movie":
|
||||
mtype = MediaType.MOVIE
|
||||
elif type_str:
|
||||
mtype = MediaType.TV
|
||||
unique_flag = f"doubanrank: {title} (DB:{douban_id})"
|
||||
# 检查是否已处理过
|
||||
if unique_flag in [h.get("unique") for h in history]:
|
||||
continue
|
||||
# 元数据
|
||||
meta = MetaInfo(title)
|
||||
meta.year = year
|
||||
if mtype:
|
||||
meta.type = mtype
|
||||
# 识别媒体信息
|
||||
if douban_id:
|
||||
# 识别豆瓣信息
|
||||
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
||||
tmdbinfo = self.mediachain.get_tmdbinfo_by_doubanid(doubanid=douban_id, mtype=meta.type)
|
||||
if not tmdbinfo:
|
||||
logger.warn(f'未能通过豆瓣ID {douban_id} 获取到TMDB信息,标题:{title},豆瓣ID:{douban_id}')
|
||||
continue
|
||||
mediainfo = self.chain.recognize_media(meta=meta, tmdbid=tmdbinfo.get("id"))
|
||||
if not mediainfo:
|
||||
logger.warn(f'TMDBID {tmdbinfo.get("id")} 未识别到媒体信息')
|
||||
continue
|
||||
else:
|
||||
mediainfo = self.chain.recognize_media(meta=meta, doubanid=douban_id)
|
||||
if not mediainfo:
|
||||
logger.warn(f'豆瓣ID {douban_id} 未识别到媒体信息')
|
||||
continue
|
||||
else:
|
||||
# 匹配媒体信息
|
||||
mediainfo: MediaInfo = self.chain.recognize_media(meta=meta)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{title},豆瓣ID:{douban_id}')
|
||||
continue
|
||||
# 判断评分是否符合要求
|
||||
if self._vote and mediainfo.vote_average < self._vote:
|
||||
logger.info(f'{mediainfo.title_year} 评分不符合要求')
|
||||
continue
|
||||
# 查询缺失的媒体信息
|
||||
exist_flag, _ = self.downloadchain.get_no_exists_info(meta=meta, mediainfo=mediainfo)
|
||||
if exist_flag:
|
||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||
continue
|
||||
# 判断用户是否已经添加订阅
|
||||
if self.subscribechain.exists(mediainfo=mediainfo, meta=meta):
|
||||
logger.info(f'{mediainfo.title_year} 订阅已存在')
|
||||
continue
|
||||
# 添加订阅
|
||||
self.subscribechain.add(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
mtype=mediainfo.type,
|
||||
tmdbid=mediainfo.tmdb_id,
|
||||
season=meta.begin_season,
|
||||
exist_ok=True,
|
||||
username="豆瓣榜单")
|
||||
# 存储历史记录
|
||||
history.append({
|
||||
"title": title,
|
||||
"type": mediainfo.type.value,
|
||||
"year": mediainfo.year,
|
||||
"poster": mediainfo.get_poster_image(),
|
||||
"overview": mediainfo.overview,
|
||||
"tmdbid": mediainfo.tmdb_id,
|
||||
"doubanid": douban_id,
|
||||
"time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"unique": unique_flag
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
|
||||
# 保存历史记录
|
||||
self.save_data('history', history)
|
||||
# 缓存只清理一次
|
||||
self._clearflag = False
|
||||
logger.info(f"所有榜单RSS刷新完成")
|
||||
|
||||
def __get_rss_info(self, addr) -> List[dict]:
|
||||
"""
|
||||
获取RSS
|
||||
"""
|
||||
try:
|
||||
if self._proxy:
|
||||
ret = RequestUtils(proxies=settings.PROXY).get_res(addr)
|
||||
else:
|
||||
ret = RequestUtils().get_res(addr)
|
||||
if not ret:
|
||||
return []
|
||||
ret_xml = ret.text
|
||||
ret_array = []
|
||||
# 解析XML
|
||||
dom_tree = xml.dom.minidom.parseString(ret_xml)
|
||||
rootNode = dom_tree.documentElement
|
||||
items = rootNode.getElementsByTagName("item")
|
||||
for item in items:
|
||||
try:
|
||||
rss_info = {}
|
||||
|
||||
# 标题
|
||||
title = DomUtils.tag_value(item, "title", default="")
|
||||
# 链接
|
||||
link = DomUtils.tag_value(item, "link", default="")
|
||||
# 年份
|
||||
description = DomUtils.tag_value(item, "description", default="")
|
||||
|
||||
if not title and not link:
|
||||
logger.warn(f"条目标题和链接均为空,无法处理")
|
||||
continue
|
||||
rss_info['title'] = title
|
||||
rss_info['link'] = link
|
||||
|
||||
doubanid = re.findall(r"/(\d+)/", link)
|
||||
if doubanid:
|
||||
doubanid = doubanid[0]
|
||||
if doubanid and not str(doubanid).isdigit():
|
||||
logger.warn(f"解析的豆瓣ID格式不正确:{doubanid}")
|
||||
continue
|
||||
rss_info['doubanid'] = doubanid
|
||||
|
||||
# 匹配4位独立数字1900-2099年
|
||||
year = re.findall(r"\b(19\d{2}|20\d{2})\b", description)
|
||||
if year:
|
||||
rss_info['year'] = year[0]
|
||||
|
||||
# 返回对象
|
||||
ret_array.append(rss_info)
|
||||
except Exception as e1:
|
||||
logger.error("解析RSS条目失败:" + str(e1))
|
||||
continue
|
||||
return ret_array
|
||||
except Exception as e:
|
||||
logger.error("获取RSS失败:" + str(e))
|
||||
return []
|
||||
622
plugins.v2/doubansync/__init__.py
Normal file
622
plugins.v2/doubansync/__init__.py
Normal file
@@ -0,0 +1,622 @@
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Optional, Any, List, Dict, Tuple
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app import schemas
|
||||
from app.chain.media import MediaChain
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
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.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_version = "2.0.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
|
||||
mediachain = None
|
||||
|
||||
# 配置属性
|
||||
_enabled: bool = False
|
||||
_onlyonce: bool = False
|
||||
_cron: str = ""
|
||||
_notify: bool = False
|
||||
_days: int = 7
|
||||
_users: str = ""
|
||||
_clear: bool = False
|
||||
_clearflag: bool = False
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
self.rsshelper = RssHelper()
|
||||
self.downloadchain = DownloadChain()
|
||||
self.searchchain = SearchChain()
|
||||
self.subscribechain = SubscribeChain()
|
||||
self.mediachain = MediaChain()
|
||||
|
||||
# 停止现有任务
|
||||
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")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._clear = config.get("clear")
|
||||
|
||||
if self._enabled or self._onlyonce:
|
||||
if self._onlyonce:
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
logger.info(f"豆瓣想看服务启动,立即运行一次")
|
||||
self._scheduler.add_job(func=self.sync, trigger='date',
|
||||
run_date=datetime.datetime.now(
|
||||
tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3)
|
||||
)
|
||||
|
||||
# 启动任务
|
||||
if self._scheduler.get_jobs():
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
if self._onlyonce or self._clear:
|
||||
# 关闭一次性开关
|
||||
self._onlyonce = False
|
||||
# 记录缓存清理标志
|
||||
self._clearflag = self._clear
|
||||
# 关闭清理缓存
|
||||
self._clear = False
|
||||
# 保存配置
|
||||
self.__update_config()
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
定义远程控制命令
|
||||
:return: 命令关键字、事件、描述、附带数据
|
||||
"""
|
||||
return [{
|
||||
"cmd": "/douban_sync",
|
||||
"event": EventType.PluginAction,
|
||||
"desc": "同步豆瓣想看",
|
||||
"category": "订阅",
|
||||
"data": {
|
||||
"action": "douban_sync"
|
||||
}
|
||||
}]
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取插件API
|
||||
[{
|
||||
"path": "/xx",
|
||||
"endpoint": self.xxx,
|
||||
"methods": ["GET", "POST"],
|
||||
"summary": "API说明"
|
||||
}]
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"path": "/delete_history",
|
||||
"endpoint": self.delete_history,
|
||||
"methods": ["GET"],
|
||||
"summary": "删除豆瓣同步历史记录"
|
||||
}
|
||||
]
|
||||
|
||||
def get_service(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
注册插件公共服务
|
||||
[{
|
||||
"id": "服务ID",
|
||||
"name": "服务名称",
|
||||
"trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()",
|
||||
"func": self.xxx,
|
||||
"kwargs": {} # 定时器参数
|
||||
}]
|
||||
"""
|
||||
if self._enabled and self._cron:
|
||||
return [
|
||||
{
|
||||
"id": "DoubanSync",
|
||||
"name": "豆瓣想看同步服务",
|
||||
"trigger": CronTrigger.from_crontab(self._cron),
|
||||
"func": self.sync,
|
||||
"kwargs": {}
|
||||
}
|
||||
]
|
||||
elif self._enabled:
|
||||
return [
|
||||
{
|
||||
"id": "DoubanSync",
|
||||
"name": "豆瓣想看同步服务",
|
||||
"trigger": "interval",
|
||||
"func": self.sync,
|
||||
"kwargs": {"minutes": 30}
|
||||
}
|
||||
]
|
||||
return []
|
||||
|
||||
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': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'notify',
|
||||
'label': '发送通知',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'onlyonce',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCronField',
|
||||
'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,多个用英文逗号分隔'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'clear',
|
||||
'label': '清理历史记录',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"notify": True,
|
||||
"onlyonce": False,
|
||||
"cron": "*/30 * * * *",
|
||||
"days": 7,
|
||||
"users": "",
|
||||
"clear": False
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
"""
|
||||
拼装插件详情页面,需要返回页面配置,同时附带数据
|
||||
"""
|
||||
# 查询同步详情
|
||||
historys = self.get_data('history')
|
||||
if not historys:
|
||||
return [
|
||||
{
|
||||
'component': 'div',
|
||||
'text': '暂无数据',
|
||||
'props': {
|
||||
'class': 'text-center',
|
||||
}
|
||||
}
|
||||
]
|
||||
# 数据按时间降序排序
|
||||
historys = sorted(historys, key=lambda x: x.get('time'), reverse=True)
|
||||
# 拼装页面
|
||||
contents = []
|
||||
for history in historys:
|
||||
title = history.get("title")
|
||||
poster = history.get("poster")
|
||||
mtype = history.get("type")
|
||||
time_str = history.get("time")
|
||||
doubanid = history.get("doubanid")
|
||||
contents.append(
|
||||
{
|
||||
'component': 'VCard',
|
||||
'content': [
|
||||
{
|
||||
"component": "VDialogCloseBtn",
|
||||
"props": {
|
||||
'innerClass': 'absolute top-0 right-0',
|
||||
},
|
||||
'events': {
|
||||
'click': {
|
||||
'api': 'plugin/DoubanSync/delete_history',
|
||||
'method': 'get',
|
||||
'params': {
|
||||
'doubanid': doubanid,
|
||||
'apikey': settings.API_TOKEN
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
'component': 'div',
|
||||
'props': {
|
||||
'class': 'd-flex justify-space-start flex-nowrap flex-row',
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'div',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VImg',
|
||||
'props': {
|
||||
'src': poster,
|
||||
'height': 120,
|
||||
'width': 80,
|
||||
'aspect-ratio': '2/3',
|
||||
'class': 'object-cover shadow ring-gray-500',
|
||||
'cover': True
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'div',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCardTitle',
|
||||
'props': {
|
||||
'class': 'ps-1 pe-5 break-words whitespace-break-spaces'
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'a',
|
||||
'props': {
|
||||
'href': f"https://movie.douban.com/subject/{doubanid}",
|
||||
'target': '_blank'
|
||||
},
|
||||
'text': title
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'类型:{mtype}'
|
||||
},
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'pa-0 px-2'
|
||||
},
|
||||
'text': f'时间:{time_str}'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
'component': 'div',
|
||||
'props': {
|
||||
'class': 'grid gap-3 grid-info-card',
|
||||
},
|
||||
'content': contents
|
||||
}
|
||||
]
|
||||
|
||||
def __update_config(self):
|
||||
"""
|
||||
更新配置
|
||||
"""
|
||||
self.update_config({
|
||||
"enabled": self._enabled,
|
||||
"notify": self._notify,
|
||||
"onlyonce": self._onlyonce,
|
||||
"cron": self._cron,
|
||||
"days": self._days,
|
||||
"users": self._users,
|
||||
"clear": self._clear
|
||||
})
|
||||
|
||||
def delete_history(self, doubanid: str, apikey: str):
|
||||
"""
|
||||
删除同步历史记录
|
||||
"""
|
||||
if apikey != settings.API_TOKEN:
|
||||
return schemas.Response(success=False, message="API密钥错误")
|
||||
# 历史记录
|
||||
historys = self.get_data('history')
|
||||
if not historys:
|
||||
return schemas.Response(success=False, message="未找到历史记录")
|
||||
# 删除指定记录
|
||||
historys = [h for h in historys if h.get("doubanid") != doubanid]
|
||||
self.save_data('history', historys)
|
||||
return schemas.Response(success=True, message="删除成功")
|
||||
|
||||
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
|
||||
# 版本
|
||||
if hasattr(settings, 'VERSION_FLAG'):
|
||||
version = settings.VERSION_FLAG # V2
|
||||
else:
|
||||
version = "v1"
|
||||
# 读取历史记录
|
||||
if self._clearflag:
|
||||
history = []
|
||||
else:
|
||||
history: List[dict] = self.get_data('history') or []
|
||||
for user_id in self._users.split(","):
|
||||
# 同步每个用户的豆瓣数据
|
||||
if not user_id:
|
||||
continue
|
||||
logger.info(f"开始同步用户 {user_id} 的豆瓣想看数据 ...")
|
||||
url = self._interests_url % user_id
|
||||
if version == "v2":
|
||||
results = self.rsshelper.parse(url, headers={
|
||||
"User-Agent": settings.USER_AGENT
|
||||
})
|
||||
else:
|
||||
results = self.rsshelper.parse(url)
|
||||
if not results:
|
||||
logger.warn(f"未获取到用户 {user_id} 豆瓣RSS数据:{url}")
|
||||
continue
|
||||
else:
|
||||
logger.info(f"获取到用户 {user_id} 豆瓣RSS数据:{len(results)}")
|
||||
# 解析数据
|
||||
for result in results:
|
||||
try:
|
||||
dtype = result.get("title", "")[:2]
|
||||
title = result.get("title", "")[2:]
|
||||
if dtype not in ["想看"]:
|
||||
logger.info(f'标题:{title},非想看数据,跳过')
|
||||
continue
|
||||
if not result.get("link"):
|
||||
logger.warn(f'标题:{title},未获取到链接,跳过')
|
||||
continue
|
||||
# 判断是否在天数范围
|
||||
pubdate: Optional[datetime.datetime] = result.get("pubdate")
|
||||
if pubdate:
|
||||
if (datetime.datetime.now(datetime.timezone.utc) - pubdate).days > float(self._days):
|
||||
logger.info(f'已超过同步天数,标题:{title},发布时间:{pubdate}')
|
||||
continue
|
||||
douban_id = result.get("link", "").split("/")[-2]
|
||||
# 检查是否处理过
|
||||
if not douban_id or douban_id in [h.get("doubanid") for h in history]:
|
||||
logger.info(f'标题:{title},豆瓣ID:{douban_id} 已处理过')
|
||||
continue
|
||||
# 识别媒体信息
|
||||
meta = MetaInfo(title=title)
|
||||
douban_info = self.chain.douban_info(doubanid=douban_id)
|
||||
meta.type = MediaType.MOVIE if douban_info.get("type") == "movie" else MediaType.TV
|
||||
if settings.RECOGNIZE_SOURCE == "themoviedb":
|
||||
tmdbinfo = self.mediachain.get_tmdbinfo_by_doubanid(doubanid=douban_id, mtype=meta.type)
|
||||
if not tmdbinfo:
|
||||
logger.warn(f'未能通过豆瓣ID {douban_id} 获取到TMDB信息,标题:{title},豆瓣ID:{douban_id}')
|
||||
continue
|
||||
mediainfo = self.chain.recognize_media(meta=meta, tmdbid=tmdbinfo.get("id"))
|
||||
if not mediainfo:
|
||||
logger.warn(f'TMDBID {tmdbinfo.get("id")} 未识别到媒体信息')
|
||||
continue
|
||||
else:
|
||||
mediainfo = self.chain.recognize_media(meta=meta, doubanid=douban_id)
|
||||
if not mediainfo:
|
||||
logger.warn(f'豆瓣ID {douban_id} 未识别到媒体信息')
|
||||
continue
|
||||
# 查询缺失的媒体信息
|
||||
exist_flag, no_exists = self.downloadchain.get_no_exists_info(meta=meta, mediainfo=mediainfo)
|
||||
if exist_flag:
|
||||
logger.info(f'{mediainfo.title_year} 媒体库中已存在')
|
||||
action = "exist"
|
||||
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="豆瓣想看")
|
||||
action = "subscribe"
|
||||
# 存储历史记录
|
||||
history.append({
|
||||
"action": action,
|
||||
"title": title,
|
||||
"type": mediainfo.type.value,
|
||||
"year": mediainfo.year,
|
||||
"poster": mediainfo.get_poster_image(),
|
||||
"overview": mediainfo.overview,
|
||||
"tmdbid": mediainfo.tmdb_id,
|
||||
"doubanid": douban_id,
|
||||
"time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
})
|
||||
except Exception as err:
|
||||
logger.error(f'同步用户 {user_id} 豆瓣想看数据出错:{str(err)}')
|
||||
logger.info(f"用户 {user_id} 豆瓣想看同步完成")
|
||||
# 保存历史记录
|
||||
self.save_data('history', history)
|
||||
# 缓存只清理一次
|
||||
self._clearflag = False
|
||||
|
||||
@eventmanager.register(EventType.PluginAction)
|
||||
def remote_sync(self, event: Event):
|
||||
"""
|
||||
豆瓣想看同步
|
||||
"""
|
||||
if event:
|
||||
event_data = event.event_data
|
||||
if not event_data or event_data.get("action") != "douban_sync":
|
||||
return
|
||||
|
||||
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,4 +1,5 @@
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Lock
|
||||
from typing import Optional, Any, List, Dict, Tuple
|
||||
@@ -31,7 +32,7 @@ class SiteStatistic(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "statistic.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.5"
|
||||
plugin_version = "1.6"
|
||||
# 插件作者
|
||||
plugin_author = "lightolly,jxxghp"
|
||||
# 作者主页
|
||||
@@ -264,28 +265,67 @@ class SiteStatistic(_PluginBase):
|
||||
|
||||
def __get_data(self) -> Tuple[str, List[SiteUserData], List[SiteUserData]]:
|
||||
"""
|
||||
获取今天的日期、今天的站点数据、昨天的站点数据
|
||||
获取最近一次统计的日期、最近一次统计的站点数据、上一次的站点数据
|
||||
如果上一次某个站点数据缺失,则 fallback 到该站点之前最近有数据的日期
|
||||
"""
|
||||
# 获取最近所有数据
|
||||
data_list: List[SiteUserData] = self.siteoper.get_userdata()
|
||||
if not data_list:
|
||||
# 获取所有原始数据
|
||||
raw_data_list: List[SiteUserData] = self.siteoper.get_userdata()
|
||||
if not raw_data_list:
|
||||
return "", [], []
|
||||
|
||||
# 每个日期、每个站点只保留最后一条数据
|
||||
data_list = list({f"{data.updated_day}_{data.name}": data for data in data_list}.values())
|
||||
data_list = list({f"{data.updated_day}_{data.name}": data for data in raw_data_list}.values())
|
||||
|
||||
# 按日期倒序排序
|
||||
data_list.sort(key=lambda x: x.updated_day, reverse=True)
|
||||
# 获取今天的日期
|
||||
today = data_list[0].updated_day
|
||||
# 获取昨天的日期
|
||||
yestoday = (datetime.strptime(today, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
# 今天的数据
|
||||
stattistic_data = [data for data in data_list if data.updated_day == today]
|
||||
# 今日数据按数据量降序排序
|
||||
stattistic_data.sort(key=lambda x: x.upload, reverse=True)
|
||||
# 昨天的数据
|
||||
yesterday_sites_data = [data for data in data_list if data.updated_day == yestoday]
|
||||
|
||||
return today, stattistic_data, yesterday_sites_data
|
||||
# 按日期分组数据
|
||||
data_by_day = defaultdict(list)
|
||||
for data in data_list:
|
||||
data_by_day[data.updated_day].append(data)
|
||||
|
||||
# 获取最近一次统计的日期
|
||||
latest_day = data_list[0].updated_day
|
||||
|
||||
# 筛选最近一次统计的数据(可能为空)
|
||||
latest_data = [data for data in data_list if data.updated_day == latest_day]
|
||||
# 最近一次统计按上传量降序排序
|
||||
latest_data.sort(key=lambda x: x.upload, reverse=True)
|
||||
|
||||
# 获取所有日期倒序排序后的列表
|
||||
sorted_dates = sorted(data_by_day.keys(), reverse=True)
|
||||
|
||||
# 计算前一天的日期字符串(相对于最近一次日期)
|
||||
previous_day_str = (datetime.strptime(latest_day, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
# 获取前一天的站点数据
|
||||
previous_day_sites = data_by_day.get(previous_day_str, [])
|
||||
# 构建前一天站点到数据的映射
|
||||
previous_by_site = {data.name: data for data in previous_day_sites}
|
||||
|
||||
# 准备查找早于前一天的日期列表,用于 fallback
|
||||
fallback_dates = [d for d in sorted_dates if d < previous_day_str]
|
||||
|
||||
# 按站点细化进行上一次数据的 fallback 处理
|
||||
previous_data = []
|
||||
for current_site in latest_data:
|
||||
site_name = current_site.name
|
||||
# 优先尝试获取前一天的同一站点数据
|
||||
site_prev = previous_by_site.get(site_name)
|
||||
|
||||
# 如果前一天没有该站点的数据,则进行逐日回退查找
|
||||
if site_prev is None or site_prev.err_msg:
|
||||
for d in fallback_dates:
|
||||
# 在每个候选日期中查找对应站点数据
|
||||
candidate = next((x for x in data_by_day[d] if x.name == site_name), None)
|
||||
if candidate:
|
||||
site_prev = candidate
|
||||
break
|
||||
|
||||
# 如果找到了上一次的数据,加入结果列表
|
||||
if site_prev:
|
||||
previous_data.append(site_prev)
|
||||
|
||||
return latest_day, latest_data, previous_data
|
||||
|
||||
@staticmethod
|
||||
def __get_total_elements(today: str, stattistic_data: List[SiteUserData], yesterday_sites_data: List[SiteUserData],
|
||||
|
||||
@@ -16,7 +16,7 @@ class BarkMsg(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Bark_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.1"
|
||||
plugin_version = "1.2"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
@@ -109,23 +109,7 @@ class BarkMsg(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'apikey',
|
||||
'label': '密钥',
|
||||
'placeholder': '',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
@@ -142,6 +126,23 @@ class BarkMsg(_PluginBase):
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextarea',
|
||||
'props': {
|
||||
'model': 'apikey',
|
||||
'label': '密钥',
|
||||
'placeholder': '每行一个用户密钥',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -216,22 +217,23 @@ class BarkMsg(_PluginBase):
|
||||
try:
|
||||
if not self._server or not self._apikey:
|
||||
return False, "参数未配置"
|
||||
sc_url = "%s/%s/%s/%s" % (self._server, self._apikey, quote_plus(title), quote_plus(text))
|
||||
if self._params:
|
||||
sc_url = "%s?%s" % (sc_url, self._params)
|
||||
res = RequestUtils().post_res(sc_url)
|
||||
if res and res.status_code == 200:
|
||||
ret_json = res.json()
|
||||
code = ret_json['code']
|
||||
message = ret_json['message']
|
||||
if code == 200:
|
||||
logger.info("Bark消息发送成功")
|
||||
for apikey in self._apikey.split():
|
||||
sc_url = "%s/%s/%s/%s" % (self._server, apikey, quote_plus(title), quote_plus(text))
|
||||
if self._params:
|
||||
sc_url = "%s?%s" % (sc_url, self._params)
|
||||
res = RequestUtils().post_res(sc_url)
|
||||
if res and res.status_code == 200:
|
||||
ret_json = res.json()
|
||||
code = ret_json['code']
|
||||
message = ret_json['message']
|
||||
if code == 200:
|
||||
logger.info(f"{apikey} Bark消息发送成功")
|
||||
else:
|
||||
logger.warn(f"{apikey} Bark消息发送失败:{message}")
|
||||
elif res is not None:
|
||||
logger.warn(f"{apikey} Bark消息发送失败,错误码:{res.status_code},错误原因:{res.reason}")
|
||||
else:
|
||||
logger.warn(f"Bark消息发送失败:{message}")
|
||||
elif res is not None:
|
||||
logger.warn(f"Bark消息发送失败,错误码:{res.status_code},错误原因:{res.reason}")
|
||||
else:
|
||||
logger.warn(f"Bark消息发送失败:未获取到返回信息")
|
||||
logger.warn(f"{apikey} Bark消息发送失败:未获取到返回信息")
|
||||
except Exception as msg_e:
|
||||
logger.error(f"Bark消息发送失败:{str(msg_e)}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user