mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-06-14 07:26:48 +00:00
Compare commits
25 Commits
AutoSignIn
...
AutoSignIn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ae826cf14 | ||
|
|
f438490ca5 | ||
|
|
b938ca5bf3 | ||
|
|
028103b900 | ||
|
|
bb1f159198 | ||
|
|
6fa42abc17 | ||
|
|
95b952c27f | ||
|
|
6631d06a04 | ||
|
|
1afce8c607 | ||
|
|
82c825e349 | ||
|
|
ff7d7b1fa4 | ||
|
|
328ed9884a | ||
|
|
4d1b90abc8 | ||
|
|
c5afdfc2da | ||
|
|
fdbd5ad501 | ||
|
|
d66605ae99 | ||
|
|
145e9747a9 | ||
|
|
87e4dcd211 | ||
|
|
633c8bad97 | ||
|
|
0927d0388a | ||
|
|
323289aa74 | ||
|
|
1f80e3b078 | ||
|
|
0ac725383e | ||
|
|
659f4f2b0d | ||
|
|
d65979323e |
@@ -26,7 +26,7 @@
|
||||
"name": "AI字幕自动生成(v2)",
|
||||
"description": "使用whisper自动生成视频文件字幕,使用大模型翻译字幕成中文。",
|
||||
"labels": "字幕",
|
||||
"version": "2.3",
|
||||
"version": "2.5",
|
||||
"icon": "autosubtitles.jpeg",
|
||||
"author": "TimoYoung",
|
||||
"level": 1,
|
||||
@@ -38,7 +38,8 @@
|
||||
"v2.0": "1.引入任务队列 2.支持监听媒体入库自动生成字幕 3.增加任务状态展示界面",
|
||||
"v2.1": "支持清除历史记录",
|
||||
"v2.2": "fix",
|
||||
"v2.3": "支持独立的大模型调用配置"
|
||||
"v2.3": "支持独立的大模型调用配置",
|
||||
"v2.5": "适配openai api v1"
|
||||
}
|
||||
},
|
||||
"CustomSites": {
|
||||
@@ -498,11 +499,12 @@
|
||||
"name": "MoviePilot更新推送",
|
||||
"description": "MoviePilot推送release更新通知、自动重启。",
|
||||
"labels": "消息通知,自动更新",
|
||||
"version": "1.5",
|
||||
"version": "1.5.1",
|
||||
"icon": "Moviepilot_A.png",
|
||||
"author": "thsrite",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.5.1": "修复版本号比较逻辑",
|
||||
"v1.5": "修复版本描述为空时的报错",
|
||||
"v1.4": "兼容更新内容带版本号的情况",
|
||||
"v1.3": "增加前端版本更新检查,需要主程序升级至v1.8.4+版本"
|
||||
|
||||
@@ -44,12 +44,14 @@
|
||||
"name": "站点自动签到",
|
||||
"description": "自动模拟登录、签到站点。",
|
||||
"labels": "站点",
|
||||
"version": "2.8",
|
||||
"version": "2.8.2",
|
||||
"icon": "signin.png",
|
||||
"author": "thsrite",
|
||||
"level": 2,
|
||||
"release": true,
|
||||
"history": {
|
||||
"v2.8.2": "优化站点 Rousi Pro 签到失败提示信息",
|
||||
"v2.8.1": "更新站点 Rousi Pro 签到接口",
|
||||
"v2.8": "适配站点 Rousi Pro",
|
||||
"v2.7": "站点请求使用站点设置的超时时间",
|
||||
"v2.6": "感谢madrays佬提供的UI!",
|
||||
@@ -95,11 +97,12 @@
|
||||
"name": "媒体库服务器通知",
|
||||
"description": "发送Emby/Jellyfin/Plex服务器的播放、入库等通知消息。",
|
||||
"labels": "消息通知,媒体库",
|
||||
"version": "1.8.2.1",
|
||||
"version": "1.8.2.2",
|
||||
"icon": "mediaplay.png",
|
||||
"author": "jxxghp",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.8.2.2": "修复emby多条相同新入库消息推送多次的问题",
|
||||
"v1.8.2.1": "修复多集时有概率图片获取失败的问题;修复emby测试通知类型接收失败的问题",
|
||||
"v1.8.1": "修复单集剧情信息有概率获取失败的问题",
|
||||
"v1.8": "当整理路径中没有tmdbid时,会尝试从媒体服务器中获取",
|
||||
@@ -383,11 +386,12 @@
|
||||
"name": "MoviePilot更新推送",
|
||||
"description": "MoviePilot推送release更新通知、自动重启。",
|
||||
"labels": "消息通知,自动更新",
|
||||
"version": "2.3",
|
||||
"version": "2.3.1",
|
||||
"icon": "Moviepilot_A.png",
|
||||
"author": "thsrite",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v2.3.1": "修复版本号比较逻辑",
|
||||
"v2.3": "修复版本描述为空时的报错",
|
||||
"v2.2": "支持 MoviePilot v2.5.0+",
|
||||
"v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+",
|
||||
@@ -471,11 +475,12 @@
|
||||
"name": "IMDb源",
|
||||
"description": "让探索,推荐和媒体识别支持IMDb数据源。",
|
||||
"labels": "探索",
|
||||
"version": "1.6.6",
|
||||
"version": "1.6.7",
|
||||
"icon": "IMDb_IOS-OSX_App.png",
|
||||
"author": "wumode",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.6.7": "优化界面显示; 增加榜单排名显示; 添加制作公司过滤项",
|
||||
"v1.6.6": "优化主页组件链接跳转",
|
||||
"v1.6.5": "仪表盘组件支持图片缓存",
|
||||
"v1.6.4": "为元数据增加背景图",
|
||||
@@ -495,7 +500,7 @@
|
||||
"v1.4.3": "为仪表盘组件添加缓存",
|
||||
"v1.4.2": "优化小屏幕组件显示",
|
||||
"v1.4.1": "优化亮色主题显示",
|
||||
"v1.4.0":"添加仪表盘组件: IMDb 编辑精选",
|
||||
"v1.4.0": "添加仪表盘组件: IMDb 编辑精选",
|
||||
"v1.3.3": "修复依赖问题",
|
||||
"v1.3.2": "更新 API query hash",
|
||||
"v1.3.1": "修复按日期排序错误",
|
||||
@@ -553,11 +558,12 @@
|
||||
"name": "美剧生词标注",
|
||||
"description": "根据CEFR等级,为英语影视剧标注高级词汇。",
|
||||
"labels": "英语",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.4",
|
||||
"icon": "LexiAnnot.png",
|
||||
"author": "wumode",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.2.4": "增强数据校验",
|
||||
"v1.2.3": "优化提示词",
|
||||
"v1.2.1": "改进字幕样式获取方法",
|
||||
"v1.2.0": "引入大模型候选词决策和词义丰富处理链; 支持读取系统智能体配置; 添加智能体工具; 优化通知样式; 改进 UI",
|
||||
@@ -609,5 +615,17 @@
|
||||
"v1.4.2": "适配MoviePilot v2.8.8+",
|
||||
"v1.4.1": "MoviePilot V2 版本登录壁纸本地化插件"
|
||||
}
|
||||
},
|
||||
"DailySummary": {
|
||||
"name": "活动总结",
|
||||
"description": "定时发送每日/每周/每月活动总结通知,支持自定义报告模块、历史记录查看",
|
||||
"labels": "通知",
|
||||
"version": "2.0.0",
|
||||
"icon": "Bark_A.png",
|
||||
"author": "yuhoye",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v2.0.0": "首次发布:可配置报告模块、历史记录页面、下载器通用化、存储路径可配置"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class AutoSignIn(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "signin.png"
|
||||
# 插件版本
|
||||
plugin_version = "2.8"
|
||||
plugin_version = "2.8.2"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
|
||||
@@ -57,15 +57,15 @@ class RousiPro(_ISiteSigninHandler):
|
||||
json=body
|
||||
)
|
||||
|
||||
if res is not None and res.status_code == 200 and "签到成功" in res.json().get("message", ""):
|
||||
if res is not None and res.status_code == 200 and res.json().get("code", -1) == 0:
|
||||
logger.info(f"{site} 签到成功")
|
||||
return True, "签到成功"
|
||||
elif res is not None and res.status_code == 400 and res.json().get("error", "") == "今日已签到":
|
||||
elif res is not None and res.status_code == 400 and res.json().get("code", -1) == 1:
|
||||
logger.info(f"{site} 今日已签到")
|
||||
return True, "今日已签到"
|
||||
elif res is not None and res.status_code == 401:
|
||||
logger.error(f"{site} 签到失败,登录状态无效")
|
||||
return False, "签到失败,登录状态无效"
|
||||
logger.error(f"{site} 签到失败,Authorization 已失效")
|
||||
return False, "签到失败,Authorization 已失效"
|
||||
elif res is not None:
|
||||
logger.error(f"{site} 签到失败,状态码:{res.status_code}")
|
||||
return False, f"签到失败,状态码:{res.status_code}"
|
||||
@@ -100,12 +100,12 @@ class RousiPro(_ISiteSigninHandler):
|
||||
url="https://rousi.pro/api/points/attendance/stats"
|
||||
)
|
||||
|
||||
if res is not None and res.status_code == 200 and "attended_dates" in res.json():
|
||||
if res is not None and res.status_code == 200 and res.json().get("code", -1) == 0:
|
||||
logger.info(f"{site} 模拟登录成功")
|
||||
return True, "模拟登录成功"
|
||||
elif res is not None and res.status_code == 401:
|
||||
logger.error(f"{site} 模拟登录失败,登录状态无效")
|
||||
return False, "模拟登录失败,登录状态无效"
|
||||
logger.error(f"{site} 模拟登录失败,Authorization 已失效")
|
||||
return False, "模拟登录失败,Authorization 已失效"
|
||||
elif res is not None:
|
||||
logger.error(f"{site} 模拟登录失败,状态码:{res.status_code}")
|
||||
return False, f"模拟登录失败,状态码:{res.status_code}"
|
||||
|
||||
@@ -6007,13 +6007,28 @@ const _sfc_main$q = /* @__PURE__ */ _defineComponent$q({
|
||||
}, {
|
||||
default: _withCtx$q(() => [
|
||||
_createVNode$q(_component_v_btn_group, {
|
||||
class: "d-sm-none",
|
||||
variant: "outlined",
|
||||
rounded: "",
|
||||
divided: ""
|
||||
}, {
|
||||
default: _withCtx$q(() => [
|
||||
_createVNode$q(_component_v_btn, {
|
||||
icon: "mdi-plus",
|
||||
disabled: loading.value,
|
||||
onClick: openAddRuleDialog
|
||||
}, null, 8, ["disabled"])
|
||||
]),
|
||||
_: 1
|
||||
}),
|
||||
_createVNode$q(_component_v_btn_group, {
|
||||
class: "d-none d-sm-flex",
|
||||
variant: "outlined",
|
||||
rounded: "",
|
||||
divided: ""
|
||||
}, {
|
||||
default: _withCtx$q(() => [
|
||||
_createVNode$q(_component_v_btn, {
|
||||
class: "d-none d-sm-flex",
|
||||
icon: group.value ? "mdi-format-list-bulleted" : "mdi-format-list-group",
|
||||
disabled: loading.value,
|
||||
onClick: _cache[2] || (_cache[2] = ($event) => group.value = !group.value)
|
||||
@@ -3,7 +3,7 @@ const currentImports = {};
|
||||
let moduleMap = {
|
||||
"./Page":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Page-CJILOVp4.css"], false, './Page');
|
||||
return __federation_import('./__federation_expose_Page-DeAFYy3o.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
return __federation_import('./__federation_expose_Page-DhQfGEOD.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
"./Config":()=>{
|
||||
dynamicLoadingCss(["__federation_expose_Config-CwbjkOP2.css"], false, './Config');
|
||||
return __federation_import('./__federation_expose_Config-CY46uj5g.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},
|
||||
|
||||
@@ -191,7 +191,7 @@ class ClashRuleProviderService:
|
||||
except ValueError:
|
||||
final_action = action
|
||||
rules = self.state.ruleset_rules_manager.filter_rules_by_action(final_action)
|
||||
return [rule.rule.condition_string() for rule in rules]
|
||||
return [rule.rule.condition_string() for rule in rules if rule.meta.available()]
|
||||
|
||||
def sync_ruleset(self):
|
||||
outbounds = set()
|
||||
@@ -240,12 +240,12 @@ class ClashRuleProviderService:
|
||||
self.state.save_data(DataKey.TOP_RULES, self.state.top_rules_manager.export_rules())
|
||||
|
||||
def clash_outbound(self) -> list[str]:
|
||||
outbound = [pg_data.data.name for pg_data in self.state.proxy_groups_from_subs()]
|
||||
outbound = [pg.data.name for pg in self.state.proxy_groups]
|
||||
if self.state.clash_template:
|
||||
outbound.extend(pg.name for pg in self.state.clash_template.proxy_groups)
|
||||
if self.state.config.group_by_region or self.state.config.group_by_country:
|
||||
outbound.extend(pg.name for pg in self.proxy_groups_by_region())
|
||||
outbound.extend(pg.data.name for pg in self.state.proxy_groups)
|
||||
outbound.extend(pg_data.data.name for pg_data in self.state.proxy_groups_from_subs())
|
||||
outbound.extend(pg.data.name for pg in self.get_proxies())
|
||||
return outbound
|
||||
|
||||
|
||||
769
plugins.v2/dailysummary/__init__.py
Normal file
769
plugins.v2/dailysummary/__init__.py
Normal file
@@ -0,0 +1,769 @@
|
||||
"""MoviePilot 活动总结插件 — 定时发送每日/每周/每月活动总结通知"""
|
||||
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Dict, Tuple, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas import NotificationType
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.schemas.types import EventType
|
||||
from app.core.config import settings
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
from app.db.subscribe_oper import SubscribeOper
|
||||
from app.db.plugindata_oper import PluginDataOper
|
||||
from app.db.models.siteuserdata import SiteUserData
|
||||
from app.db import ScopedSession
|
||||
|
||||
|
||||
# ─── 模块注册表:key → 中文名,各报告按需选取 ───
|
||||
|
||||
MODULES = OrderedDict([
|
||||
("download", "下载记录"),
|
||||
("transfer", "入库记录"),
|
||||
("signin", "签到状态"),
|
||||
("brush", "刷流统计"),
|
||||
("downloader", "下载器概览"),
|
||||
("site_delta", "站点增量"),
|
||||
("site_current", "站点快照"),
|
||||
("subscribe", "订阅进度"),
|
||||
("storage", "存储空间"),
|
||||
])
|
||||
|
||||
MODULE_OPTIONS = [{"title": name, "value": key} for key, name in MODULES.items()]
|
||||
|
||||
# 各报告类型的默认模块
|
||||
DEFAULT_DAILY_MODULES = ["download", "transfer", "signin", "brush", "downloader", "site_delta"]
|
||||
DEFAULT_WEEKLY_MODULES = ["download", "transfer", "subscribe", "site_delta", "brush"]
|
||||
DEFAULT_MONTHLY_MODULES = [
|
||||
"download", "transfer", "subscribe", "site_current",
|
||||
"site_delta", "storage", "brush", "downloader",
|
||||
]
|
||||
|
||||
WEEKDAY_NAMES = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
||||
|
||||
# 历史记录上限
|
||||
MAX_HISTORY = 100
|
||||
|
||||
|
||||
@dataclass
|
||||
class TimeRange:
|
||||
"""报告的时间范围"""
|
||||
start: datetime
|
||||
end: datetime
|
||||
start_str: str # "YYYY-MM-DD HH:MM:SS" — 用于数据库查询
|
||||
start_date: str # "YYYY-MM-DD"
|
||||
end_date: str # "YYYY-MM-DD"
|
||||
report_type: str # "daily" / "weekly" / "monthly"
|
||||
prefix: str # "今日" / "本周" / "本月"
|
||||
|
||||
|
||||
class DailySummary(_PluginBase):
|
||||
plugin_name = "活动总结"
|
||||
plugin_desc = "定时发送每日/每周/每月活动总结通知,支持自定义报告模块、历史记录查看"
|
||||
plugin_icon = "Bark_A.png"
|
||||
plugin_version = "2.0.0"
|
||||
plugin_author = "yuhoye"
|
||||
author_url = "https://github.com/yuhoye"
|
||||
plugin_config_prefix = "dailysummary_"
|
||||
plugin_order = 30
|
||||
auth_level = 1
|
||||
|
||||
# ─── 配置字段 ───
|
||||
|
||||
_enabled: bool = False
|
||||
_notify: bool = True
|
||||
_daily_cron: str = "0 23 * * *"
|
||||
_weekly_cron: str = "0 23 * * 1"
|
||||
_monthly_cron: str = "0 23 1 * *"
|
||||
_onlyonce: bool = False
|
||||
_test_type: str = "daily"
|
||||
|
||||
_daily_modules: list = None
|
||||
_weekly_modules: list = None
|
||||
_monthly_modules: list = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
if config:
|
||||
self._enabled = config.get("enabled", False)
|
||||
self._notify = config.get("notify", True)
|
||||
self._daily_cron = config.get("daily_cron", "0 23 * * *")
|
||||
self._weekly_cron = config.get("weekly_cron", "0 23 * * 1")
|
||||
self._monthly_cron = config.get("monthly_cron", "0 23 1 * *")
|
||||
self._onlyonce = config.get("onlyonce", False)
|
||||
self._test_type = config.get("test_type", "daily")
|
||||
self._daily_modules = config.get("daily_modules") or DEFAULT_DAILY_MODULES
|
||||
self._weekly_modules = config.get("weekly_modules") or DEFAULT_WEEKLY_MODULES
|
||||
self._monthly_modules = config.get("monthly_modules") or DEFAULT_MONTHLY_MODULES
|
||||
else:
|
||||
self._daily_modules = DEFAULT_DAILY_MODULES
|
||||
self._weekly_modules = DEFAULT_WEEKLY_MODULES
|
||||
self._monthly_modules = DEFAULT_MONTHLY_MODULES
|
||||
|
||||
if self._onlyonce:
|
||||
self._onlyonce = False
|
||||
self._save_config()
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
test_func = {
|
||||
"daily": self.send_daily,
|
||||
"weekly": self.send_weekly,
|
||||
"monthly": self.send_monthly,
|
||||
}.get(self._test_type, self.send_daily)
|
||||
scheduler.add_job(
|
||||
func=test_func,
|
||||
trigger="date",
|
||||
run_date=datetime.now(tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3),
|
||||
name="立即测试",
|
||||
)
|
||||
scheduler.start()
|
||||
|
||||
def _save_config(self):
|
||||
self.update_config({
|
||||
"enabled": self._enabled,
|
||||
"notify": self._notify,
|
||||
"daily_cron": self._daily_cron,
|
||||
"weekly_cron": self._weekly_cron,
|
||||
"monthly_cron": self._monthly_cron,
|
||||
"onlyonce": False,
|
||||
"test_type": self._test_type,
|
||||
"daily_modules": self._daily_modules,
|
||||
"weekly_modules": self._weekly_modules,
|
||||
"monthly_modules": self._monthly_modules,
|
||||
})
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{"cmd": "/daily_summary", "event": EventType.PluginAction, "desc": "发送每日总结", "category": "工具", "data": {"action": "daily_summary"}},
|
||||
{"cmd": "/weekly_summary", "event": EventType.PluginAction, "desc": "发送每周总结", "category": "工具", "data": {"action": "weekly_summary"}},
|
||||
{"cmd": "/monthly_summary", "event": EventType.PluginAction, "desc": "发送每月总结", "category": "工具", "data": {"action": "monthly_summary"}},
|
||||
]
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
return [{
|
||||
"path": "/clear_history",
|
||||
"endpoint": self._api_clear_history,
|
||||
"methods": ["POST"],
|
||||
"summary": "清除历史记录",
|
||||
}]
|
||||
|
||||
def _api_clear_history(self) -> dict:
|
||||
self.save_data("history", [])
|
||||
logger.info("[DailySummary] 历史记录已清除")
|
||||
return {"success": True}
|
||||
|
||||
def get_service(self) -> List[Dict[str, Any]]:
|
||||
if not self._enabled:
|
||||
return []
|
||||
services = []
|
||||
if self._daily_cron:
|
||||
services.append({
|
||||
"id": "DailySummary_daily",
|
||||
"name": "每日总结",
|
||||
"trigger": CronTrigger.from_crontab(self._daily_cron),
|
||||
"func": self.send_daily,
|
||||
"kwargs": {},
|
||||
})
|
||||
if self._weekly_cron:
|
||||
services.append({
|
||||
"id": "DailySummary_weekly",
|
||||
"name": "每周总结",
|
||||
"trigger": CronTrigger.from_crontab(self._weekly_cron),
|
||||
"func": self.send_weekly,
|
||||
"kwargs": {},
|
||||
})
|
||||
if self._monthly_cron:
|
||||
services.append({
|
||||
"id": "DailySummary_monthly",
|
||||
"name": "每月总结",
|
||||
"trigger": CronTrigger.from_crontab(self._monthly_cron),
|
||||
"func": self.send_monthly,
|
||||
"kwargs": {},
|
||||
})
|
||||
return services
|
||||
|
||||
# ─── 配置表单:三 Tab 布局 ───
|
||||
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
test_options = [
|
||||
{"title": "每日总结", "value": "daily"},
|
||||
{"title": "每周总结", "value": "weekly"},
|
||||
{"title": "每月总结", "value": "monthly"},
|
||||
]
|
||||
return [
|
||||
{
|
||||
"component": "VForm",
|
||||
"content": [
|
||||
# ── 基本设置(VTabs 外面,避免弹出菜单被裁剪) ──
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{'component': 'VCol', 'props': {'cols': 12, 'md': 3},
|
||||
'content': [{'component': 'VSwitch', 'props': {'model': 'enabled', 'label': '启用插件'}}]},
|
||||
{'component': 'VCol', 'props': {'cols': 12, 'md': 3},
|
||||
'content': [{'component': 'VSwitch', 'props': {'model': 'notify', 'label': '发送通知'}}]},
|
||||
{'component': 'VCol', 'props': {'cols': 12, 'md': 3},
|
||||
'content': [{'component': 'VSwitch', 'props': {'model': 'onlyonce', 'label': '立即测试一次'}}]},
|
||||
{'component': 'VCol', 'props': {'cols': 12, 'md': 3},
|
||||
'content': [{'component': 'VSelect', 'props': {'model': 'test_type', 'label': '测试类型', 'items': test_options}}]},
|
||||
],
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{'component': 'VCol', 'props': {'cols': 12, 'md': 4},
|
||||
'content': [{'component': 'VCronField', 'props': {'model': 'daily_cron', 'label': '每日周期', 'placeholder': '5位cron表达式'}}]},
|
||||
{'component': 'VCol', 'props': {'cols': 12, 'md': 4},
|
||||
'content': [{'component': 'VCronField', 'props': {'model': 'weekly_cron', 'label': '每周周期', 'placeholder': '5位cron表达式'}}]},
|
||||
{'component': 'VCol', 'props': {'cols': 12, 'md': 4},
|
||||
'content': [{'component': 'VCronField', 'props': {'model': 'monthly_cron', 'label': '每月周期', 'placeholder': '5位cron表达式'}}]},
|
||||
],
|
||||
},
|
||||
# ── 报告内容 ──
|
||||
{
|
||||
"component": "VRow",
|
||||
"props": {"style": "margin-top: 8px;"},
|
||||
"content": [
|
||||
{"component": "VCol", "props": {"cols": 12},
|
||||
"content": [{"component": "VAlert", "props": {"type": "info", "variant": "tonal", "text": "选择各报告中包含的信息模块,模块按选择顺序显示在报告中"}}]},
|
||||
],
|
||||
},
|
||||
{
|
||||
"component": "VRow",
|
||||
"content": [
|
||||
{"component": "VCol", "props": {"cols": 12, "md": 4},
|
||||
"content": [{"component": "VSelect", "props": {
|
||||
"model": "daily_modules", "label": "日报模块",
|
||||
"items": MODULE_OPTIONS, "multiple": True, "chips": True, "closable-chips": True,
|
||||
}}]},
|
||||
{"component": "VCol", "props": {"cols": 12, "md": 4},
|
||||
"content": [{"component": "VSelect", "props": {
|
||||
"model": "weekly_modules", "label": "周报模块",
|
||||
"items": MODULE_OPTIONS, "multiple": True, "chips": True, "closable-chips": True,
|
||||
}}]},
|
||||
{"component": "VCol", "props": {"cols": 12, "md": 4},
|
||||
"content": [{"component": "VSelect", "props": {
|
||||
"model": "monthly_modules", "label": "月报模块",
|
||||
"items": MODULE_OPTIONS, "multiple": True, "chips": True, "closable-chips": True,
|
||||
}}]},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"notify": True,
|
||||
"daily_cron": "0 23 * * *",
|
||||
"weekly_cron": "0 23 * * 1",
|
||||
"monthly_cron": "0 23 1 * *",
|
||||
"onlyonce": False,
|
||||
"test_type": "daily",
|
||||
"daily_modules": DEFAULT_DAILY_MODULES,
|
||||
"weekly_modules": DEFAULT_WEEKLY_MODULES,
|
||||
"monthly_modules": DEFAULT_MONTHLY_MODULES,
|
||||
}
|
||||
|
||||
# ─── 历史记录页面 ───
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
history = self.get_data("history") or []
|
||||
|
||||
# 模块配置摘要
|
||||
def _module_names(modules):
|
||||
return "、".join(MODULES.get(m, m) for m in (modules or []))
|
||||
|
||||
config_cards = [
|
||||
self._config_card('📊 日报模块', _module_names(self._daily_modules), self._daily_cron),
|
||||
self._config_card('📈 周报模块', _module_names(self._weekly_modules), self._weekly_cron),
|
||||
self._config_card('📅 月报模块', _module_names(self._monthly_modules), self._monthly_cron),
|
||||
]
|
||||
|
||||
if not history:
|
||||
return [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': config_cards,
|
||||
},
|
||||
{
|
||||
'component': 'div',
|
||||
'text': '暂无发送记录',
|
||||
'props': {'class': 'text-center mt-4'},
|
||||
},
|
||||
]
|
||||
|
||||
daily_count = sum(1 for r in history if r.get("type") == "daily")
|
||||
weekly_count = sum(1 for r in history if r.get("type") == "weekly")
|
||||
monthly_count = sum(1 for r in history if r.get("type") == "monthly")
|
||||
|
||||
items = [
|
||||
{
|
||||
'time': r.get('time', ''),
|
||||
'type_label': {'daily': '日报', 'weekly': '周报', 'monthly': '月报'}.get(r.get('type'), ''),
|
||||
'title': r.get('title', ''),
|
||||
'preview': (r.get('text', '')[:80] + '...') if len(r.get('text', '')) > 80 else r.get('text', ''),
|
||||
}
|
||||
for r in reversed(history)
|
||||
]
|
||||
|
||||
return [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': config_cards + [
|
||||
# 发送统计
|
||||
self._stat_card('日报', f'{daily_count} 份'),
|
||||
self._stat_card('周报', f'{weekly_count} 份'),
|
||||
self._stat_card('月报', f'{monthly_count} 份'),
|
||||
# 历史记录表格
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {'cols': 12, 'class': 'd-none d-sm-block'},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VDataTableVirtual',
|
||||
'props': {
|
||||
'class': 'text-sm',
|
||||
'headers': [
|
||||
{'title': '时间', 'key': 'time', 'sortable': True},
|
||||
{'title': '类型', 'key': 'type_label', 'sortable': True},
|
||||
{'title': '标题', 'key': 'title', 'sortable': False},
|
||||
{'title': '预览', 'key': 'preview', 'sortable': False},
|
||||
],
|
||||
'items': items,
|
||||
'height': '30rem',
|
||||
'density': 'compact',
|
||||
'fixed-header': True,
|
||||
'hide-no-data': True,
|
||||
'hover': True,
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _config_card(title: str, modules_text: str, cron: str) -> dict:
|
||||
return {
|
||||
'component': 'VCol',
|
||||
'props': {'cols': 12, 'md': 4},
|
||||
'content': [{
|
||||
'component': 'VCard',
|
||||
'props': {'variant': 'tonal'},
|
||||
'content': [{
|
||||
'component': 'VCardText',
|
||||
'content': [
|
||||
{'component': 'div', 'props': {'class': 'text-subtitle-2 mb-1'}, 'text': f'{title} ⏰ {cron}'},
|
||||
{'component': 'span', 'props': {'class': 'text-caption'}, 'text': modules_text},
|
||||
],
|
||||
}],
|
||||
}],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _stat_card(title: str, value: str) -> dict:
|
||||
return {
|
||||
'component': 'VCol',
|
||||
'props': {'cols': 4, 'md': 4},
|
||||
'content': [{
|
||||
'component': 'VCard',
|
||||
'props': {'variant': 'tonal'},
|
||||
'content': [{
|
||||
'component': 'VCardText',
|
||||
'props': {'class': 'text-center pa-2'},
|
||||
'content': [
|
||||
{'component': 'div', 'props': {'class': 'text-caption'}, 'text': title},
|
||||
{'component': 'div', 'props': {'class': 'text-h6'}, 'text': value},
|
||||
],
|
||||
}],
|
||||
}],
|
||||
}
|
||||
|
||||
def stop_service(self):
|
||||
pass
|
||||
|
||||
# ─── 命令处理 ───
|
||||
|
||||
@eventmanager.register(EventType.PluginAction)
|
||||
def handle_command(self, event: Event = None):
|
||||
if not event:
|
||||
return
|
||||
action = (event.event_data or {}).get("action", "")
|
||||
handler = {
|
||||
"daily_summary": self.send_daily,
|
||||
"weekly_summary": self.send_weekly,
|
||||
"monthly_summary": self.send_monthly,
|
||||
}.get(action)
|
||||
if handler:
|
||||
handler()
|
||||
|
||||
# ─── 统一报告引擎 ───
|
||||
|
||||
def send_daily(self):
|
||||
header, text = self._build_report("daily")
|
||||
self._send(report_type="daily", title=header, text=text)
|
||||
|
||||
def send_weekly(self):
|
||||
header, text = self._build_report("weekly")
|
||||
self._send(report_type="weekly", title=header, text=text)
|
||||
|
||||
def send_monthly(self):
|
||||
header, text = self._build_report("monthly")
|
||||
self._send(report_type="monthly", title=header, text=text)
|
||||
|
||||
def _build_report(self, report_type: str) -> Tuple[str, str]:
|
||||
logger.info(f"[DailySummary] 开始生成 {report_type} 总结")
|
||||
tr = self._calc_time_range(report_type)
|
||||
modules = {
|
||||
"daily": self._daily_modules,
|
||||
"weekly": self._weekly_modules,
|
||||
"monthly": self._monthly_modules,
|
||||
}.get(report_type, self._daily_modules)
|
||||
|
||||
sections = []
|
||||
for mod in modules:
|
||||
result = self._run_section(mod, tr)
|
||||
if result:
|
||||
sections.append(result)
|
||||
|
||||
header = self._make_header(report_type, tr)
|
||||
text = "\n\n".join(sections) if sections else "无数据"
|
||||
return header, text
|
||||
|
||||
def _calc_time_range(self, report_type: str) -> TimeRange:
|
||||
tz = pytz.timezone(settings.TZ)
|
||||
now = datetime.now(tz)
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
if report_type == "daily":
|
||||
start = today_start
|
||||
prefix = "今日"
|
||||
elif report_type == "weekly":
|
||||
start = today_start - timedelta(days=now.weekday())
|
||||
prefix = "本周"
|
||||
else:
|
||||
start = today_start.replace(day=1)
|
||||
prefix = "本月"
|
||||
|
||||
return TimeRange(
|
||||
start=start,
|
||||
end=now,
|
||||
start_str=start.strftime("%Y-%m-%d 00:00:00"),
|
||||
start_date=start.strftime("%Y-%m-%d"),
|
||||
end_date=today_start.strftime("%Y-%m-%d"),
|
||||
report_type=report_type,
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
def _make_header(self, report_type: str, tr: TimeRange) -> str:
|
||||
now = tr.end
|
||||
if report_type == "daily":
|
||||
return f"📊 每日总结 ({now.strftime('%m-%d')} {WEEKDAY_NAMES[now.weekday()]})"
|
||||
elif report_type == "weekly":
|
||||
return f"📈 周报 ({tr.start.strftime('%m-%d')} ~ {now.strftime('%m-%d')})"
|
||||
else:
|
||||
return f"📅 月报 ({now.strftime('%Y年%m月')})"
|
||||
|
||||
def _run_section(self, module: str, tr: TimeRange) -> Optional[str]:
|
||||
handler = {
|
||||
"download": self._section_download,
|
||||
"transfer": self._section_transfer,
|
||||
"signin": self._section_signin,
|
||||
"brush": self._section_brush,
|
||||
"downloader": self._section_downloader,
|
||||
"site_delta": self._section_site_delta,
|
||||
"site_current": self._section_site_current,
|
||||
"subscribe": self._section_subscribe,
|
||||
"storage": self._section_storage,
|
||||
}.get(module)
|
||||
if not handler:
|
||||
return None
|
||||
try:
|
||||
return handler(tr)
|
||||
except Exception as e:
|
||||
logger.error(f"[DailySummary] 模块 {module} 执行失败: {e}")
|
||||
return f"【{MODULES.get(module, module)}】数据读取失败"
|
||||
|
||||
# ─── 各模块实现 ───
|
||||
|
||||
def _section_download(self, tr: TimeRange) -> Optional[str]:
|
||||
downloads = self._get_downloads(tr.start_str)
|
||||
if not downloads:
|
||||
return f"【{tr.prefix}下载】无"
|
||||
|
||||
# 日报:详细列表;周报/月报:分类汇总
|
||||
if tr.report_type == "daily":
|
||||
lines = [f"【{tr.prefix}下载 {len(downloads)} 部】"]
|
||||
for d in downloads:
|
||||
ep = f" {d.seasons}{d.episodes}" if d.episodes else (f" {d.seasons}" if d.seasons else "")
|
||||
site = f" - {d.torrent_site}" if d.torrent_site else ""
|
||||
lines.append(f" • {d.title}{ep}{site}")
|
||||
return "\n".join(lines)
|
||||
|
||||
type_count = {}
|
||||
for d in downloads:
|
||||
cat = d.media_category or d.type or "其他"
|
||||
type_count[cat] = type_count.get(cat, 0) + 1
|
||||
type_summary = " | ".join(f"{k} {v}" for k, v in sorted(type_count.items(), key=lambda x: -x[1]))
|
||||
return f"【{tr.prefix}下载】共 {len(downloads)} 部\n {type_summary}"
|
||||
|
||||
def _section_transfer(self, tr: TimeRange) -> Optional[str]:
|
||||
transfers = self._get_transfers(tr.start_str)
|
||||
success = [t for t in transfers if t.status]
|
||||
if not success:
|
||||
return f"【{tr.prefix}入库】无"
|
||||
|
||||
# 日报:详细列表;周报/月报:分类汇总
|
||||
if tr.report_type == "daily":
|
||||
lines = [f"【{tr.prefix}入库 {len(success)} 部】"]
|
||||
for t in success:
|
||||
ep = f" {t.seasons}{t.episodes}" if t.episodes else (f" {t.seasons}" if t.seasons else "")
|
||||
cat = f" → {t.category}" if t.category else ""
|
||||
lines.append(f" • {t.title}{ep}{cat}")
|
||||
return "\n".join(lines)
|
||||
|
||||
cat_count = {}
|
||||
for t in success:
|
||||
cat = t.category or t.type or "其他"
|
||||
cat_count[cat] = cat_count.get(cat, 0) + 1
|
||||
cat_summary = " | ".join(f"{k} {v}" for k, v in sorted(cat_count.items(), key=lambda x: -x[1]))
|
||||
return f"【{tr.prefix}入库】共 {len(success)} 部\n {cat_summary}"
|
||||
|
||||
def _section_signin(self, tr: TimeRange) -> str:
|
||||
pdo = PluginDataOper()
|
||||
plugin_id = "AutoSignIn"
|
||||
now = tr.end
|
||||
key = f"{now.month}月{now.day}日"
|
||||
data = pdo.get_data(plugin_id, key)
|
||||
if not data:
|
||||
return "【签到】今日无签到记录"
|
||||
|
||||
signin_records = [r for r in data if "模拟登录" not in r.get("status", "")]
|
||||
total = len(signin_records)
|
||||
success = sum(1 for r in signin_records if "成功" in r.get("status", ""))
|
||||
failed = [r for r in signin_records if "成功" not in r.get("status", "")]
|
||||
|
||||
if success == total:
|
||||
return f"【签到】✅ 全部成功 ({success}/{total})"
|
||||
|
||||
fail_sites = ", ".join(r["site"] for r in failed)
|
||||
return f"【签到】⚠️ {success}/{total} 成功\n 失败: {fail_sites}"
|
||||
|
||||
def _section_brush(self, tr: TimeRange) -> str:
|
||||
pdo = PluginDataOper()
|
||||
plugin_ids = ["BrushFlow"]
|
||||
|
||||
total_uploaded = 0
|
||||
total_downloaded = 0
|
||||
total_active = 0
|
||||
total_deleted = 0
|
||||
total_count = 0
|
||||
|
||||
for pid in plugin_ids:
|
||||
stat = pdo.get_data(pid, "statistic")
|
||||
if not stat:
|
||||
continue
|
||||
total_uploaded += stat.get("uploaded", 0) + stat.get("active_uploaded", 0)
|
||||
total_downloaded += stat.get("downloaded", 0)
|
||||
total_active += stat.get("active", 0)
|
||||
total_deleted += stat.get("deleted", 0)
|
||||
total_count += stat.get("count", 0)
|
||||
|
||||
return (
|
||||
f"【刷流】总种: {total_count} | 活跃: {total_active} | 已删: {total_deleted}\n"
|
||||
f" 总↑ {_human_size(total_uploaded)} | 总↓ {_human_size(total_downloaded)}"
|
||||
)
|
||||
|
||||
def _section_downloader(self, tr: TimeRange) -> Optional[str]:
|
||||
"""通过 DownloaderHelper 获取所有已配置下载器的概览"""
|
||||
try:
|
||||
from app.helper.downloader import DownloaderHelper
|
||||
except ImportError:
|
||||
logger.warning("[DailySummary] DownloaderHelper 不可用")
|
||||
return None
|
||||
|
||||
services = DownloaderHelper().get_services()
|
||||
if not services:
|
||||
return None
|
||||
|
||||
lines = ["【下载器概览】"]
|
||||
for name, svc in services.items():
|
||||
if not svc or not svc.instance:
|
||||
continue
|
||||
inst = svc.instance
|
||||
completed = inst.get_completed_torrents() or []
|
||||
downloading = inst.get_downloading_torrents() or []
|
||||
total = len(completed) + len(downloading)
|
||||
ti = inst.transfer_info()
|
||||
up_speed = ti.get("up_info_speed", 0) if ti else 0
|
||||
dl_speed = ti.get("dl_info_speed", 0) if ti else 0
|
||||
lines.append(f" {name}: 种子 {total} | ↑{_human_size(up_speed)}/s | ↓{_human_size(dl_speed)}/s")
|
||||
|
||||
return "\n".join(lines) if len(lines) > 1 else None
|
||||
|
||||
def _section_site_delta(self, tr: TimeRange) -> Optional[str]:
|
||||
with ScopedSession() as db:
|
||||
start_data = SiteUserData.get_by_date(db, tr.start_date)
|
||||
end_data = SiteUserData.get_by_date(db, tr.end_date)
|
||||
|
||||
if not start_data or not end_data:
|
||||
return None
|
||||
|
||||
start_map = {d.domain: d for d in start_data}
|
||||
end_map = {d.domain: d for d in end_data}
|
||||
|
||||
label = {"daily": "站点增量", "weekly": "站点周增量", "monthly": "站点月增量"}.get(tr.report_type, "站点增量")
|
||||
lines = [f"【{label}】", " 站点 ↑上传 ↓下载 魔力变化"]
|
||||
|
||||
has_data = False
|
||||
for domain, end in sorted(end_map.items(), key=lambda x: (x[1].upload or 0), reverse=True):
|
||||
start = start_map.get(domain)
|
||||
if not start:
|
||||
continue
|
||||
up_delta = (end.upload or 0) - (start.upload or 0)
|
||||
down_delta = (end.download or 0) - (start.download or 0)
|
||||
bonus_delta = (end.bonus or 0) - (start.bonus or 0)
|
||||
|
||||
no_change = up_delta == 0 and down_delta == 0 and bonus_delta == 0
|
||||
data_anomaly = up_delta < 0 or down_delta < 0
|
||||
if no_change or data_anomaly:
|
||||
continue
|
||||
|
||||
has_data = True
|
||||
name = (end.name or domain)[:6].ljust(6)
|
||||
bonus_str = f"+{bonus_delta:.0f}" if bonus_delta >= 0 else f"{bonus_delta:.0f}"
|
||||
lines.append(f" {name} {_human_size(up_delta):>10} {_human_size(down_delta):>10} {bonus_str:>8}")
|
||||
|
||||
return "\n".join(lines) if has_data else None
|
||||
|
||||
def _section_site_current(self, tr: TimeRange) -> Optional[str]:
|
||||
with ScopedSession() as db:
|
||||
data = SiteUserData.get_latest(db)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
lines = ["【站点数据】", " 站点 总↑ 总↓ 分享率 魔力"]
|
||||
|
||||
for d in sorted(data, key=lambda x: (x.upload or 0), reverse=True):
|
||||
if not d.upload and not d.download:
|
||||
continue
|
||||
name = (d.name or d.domain)[:6].ljust(6)
|
||||
ratio = f"{d.ratio:.2f}" if d.ratio else "∞"
|
||||
bonus = f"{d.bonus:.0f}" if d.bonus else "0"
|
||||
lines.append(f" {name} {_human_size(d.upload or 0):>10} {_human_size(d.download or 0):>10} {ratio:>6} {bonus:>8}")
|
||||
|
||||
return "\n".join(lines) if len(lines) > 2 else None
|
||||
|
||||
def _section_subscribe(self, tr: TimeRange) -> str:
|
||||
subs = SubscribeOper().list(state="R") or []
|
||||
if not subs:
|
||||
return "【订阅进度】无活跃订阅"
|
||||
|
||||
lines = [f"【订阅进度】{len(subs)} 部进行中"]
|
||||
for s in subs:
|
||||
total = s.total_episode or 0
|
||||
lack = s.lack_episode or 0
|
||||
done = total - lack
|
||||
season = f" S{s.season}" if s.season else ""
|
||||
progress = f" {done}/{total}" if total > 0 else ""
|
||||
lines.append(f" • {s.name}{season}{progress}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _section_storage(self, tr: TimeRange) -> Optional[str]:
|
||||
volumes = self._parse_storage_paths()
|
||||
if not volumes:
|
||||
return None
|
||||
|
||||
lines = ["【存储空间】"]
|
||||
has_data = False
|
||||
for path, label in volumes:
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
stat = os.statvfs(path)
|
||||
total = stat.f_blocks * stat.f_frsize
|
||||
used = (stat.f_blocks - stat.f_bfree) * stat.f_frsize
|
||||
if total == 0:
|
||||
continue
|
||||
has_data = True
|
||||
pct = used / total * 100
|
||||
lines.append(f" {label}: 已用 {_human_size(used)} / {_human_size(total)} ({pct:.0f}%)")
|
||||
return "\n".join(lines) if has_data else None
|
||||
|
||||
def _parse_storage_paths(self) -> List[Tuple[str, str]]:
|
||||
"""自动检测 MP 的 LIBRARY_PATH / DOWNLOAD_PATH"""
|
||||
paths = []
|
||||
if hasattr(settings, "LIBRARY_PATH") and settings.LIBRARY_PATH:
|
||||
paths.append((settings.LIBRARY_PATH, "媒体库"))
|
||||
if hasattr(settings, "DOWNLOAD_PATH") and settings.DOWNLOAD_PATH:
|
||||
paths.append((settings.DOWNLOAD_PATH, "下载目录"))
|
||||
return paths
|
||||
|
||||
# ─── 数据查询 ───
|
||||
|
||||
def _get_downloads(self, since: str) -> list:
|
||||
try:
|
||||
from app.db.models.downloadhistory import DownloadHistory
|
||||
with ScopedSession() as db:
|
||||
return db.query(DownloadHistory).filter(
|
||||
DownloadHistory.date > since
|
||||
).order_by(DownloadHistory.date.desc()).all()
|
||||
except Exception as e:
|
||||
logger.error(f"[DailySummary] 查询下载记录失败: {e}")
|
||||
return []
|
||||
|
||||
def _get_transfers(self, since: str) -> list:
|
||||
try:
|
||||
return TransferHistoryOper().list_by_date(since) or []
|
||||
except Exception as e:
|
||||
logger.error(f"[DailySummary] 查询入库记录失败: {e}")
|
||||
return []
|
||||
|
||||
# ─── 发送通知 + 保存历史 ───
|
||||
|
||||
def _send(self, report_type: str, title: str, text: str):
|
||||
logger.info(f"[DailySummary] {title}\n{text}")
|
||||
|
||||
if self._notify:
|
||||
self.post_message(mtype=NotificationType.Plugin, title=title, text=text)
|
||||
|
||||
# 保存历史记录
|
||||
tz = pytz.timezone(settings.TZ)
|
||||
now = datetime.now(tz)
|
||||
record = {
|
||||
"time": now.strftime("%Y-%m-%d %H:%M"),
|
||||
"type": report_type,
|
||||
"title": title,
|
||||
"text": text,
|
||||
}
|
||||
history = self.get_data("history") or []
|
||||
history.append(record)
|
||||
# 保留最近 MAX_HISTORY 条
|
||||
if len(history) > MAX_HISTORY:
|
||||
history = history[-MAX_HISTORY:]
|
||||
self.save_data("history", history)
|
||||
|
||||
|
||||
# ─── 工具函数 ───
|
||||
|
||||
def _human_size(size_bytes: float) -> str:
|
||||
if size_bytes is None or size_bytes == 0:
|
||||
return "0 B"
|
||||
negative = size_bytes < 0
|
||||
size_bytes = abs(size_bytes)
|
||||
for unit in ["B", "KB", "MB", "GB", "TB", "PB"]:
|
||||
if size_bytes < 1024:
|
||||
formatted = f"{size_bytes:.1f} {unit}" if size_bytes != int(size_bytes) else f"{int(size_bytes)} {unit}"
|
||||
return f"-{formatted}" if negative else formatted
|
||||
size_bytes /= 1024
|
||||
return f"{size_bytes:.1f} EB"
|
||||
@@ -34,7 +34,7 @@ class ImdbSource(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "IMDb_IOS-OSX_App.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.6.6"
|
||||
plugin_version = "1.6.7"
|
||||
# 插件作者
|
||||
plugin_author = "wumode"
|
||||
# 作者主页
|
||||
@@ -223,7 +223,7 @@ class ImdbSource(_PluginBase):
|
||||
height = 335
|
||||
is_mobile = ImdbSource.is_mobile(kwargs.get('user_agent'))
|
||||
if is_mobile:
|
||||
height *= 1.75
|
||||
height *= 1.80
|
||||
# 全局配置
|
||||
attrs = {
|
||||
"border": False
|
||||
@@ -320,7 +320,7 @@ class ImdbSource(_PluginBase):
|
||||
'href': f"https://www.imdb.com/name/{cs.id}",
|
||||
'target': '_blank',
|
||||
'rel': 'noopener noreferrer',
|
||||
'class': 'text-h4 font-weight-bold mb-2 d-flex align-center',
|
||||
'class': 'text-h4 font-weight-bold mb-1 d-flex align-center',
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
@@ -344,7 +344,6 @@ class ImdbSource(_PluginBase):
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
'component': 'span',
|
||||
'props': {
|
||||
@@ -361,6 +360,7 @@ class ImdbSource(_PluginBase):
|
||||
poster_url = next((f"{title.primary_image.url if title.primary_image else ''}" for title in titles if
|
||||
title.id == entry.ttconst), None)
|
||||
poster_url = f"{self._img_proxy_prefix}{quote(poster_url or '')}"
|
||||
meter_ranking_url = imdb_title.meter_ranking.url if imdb_title.meter_ranking else None
|
||||
poster_com = {
|
||||
'component': 'VImg',
|
||||
'props': {
|
||||
@@ -375,9 +375,9 @@ class ImdbSource(_PluginBase):
|
||||
}
|
||||
|
||||
poster_ui = {
|
||||
'component': 'div',
|
||||
'component': 'VRow',
|
||||
'props': {
|
||||
'class': 'd-flex justify-center mt-2'
|
||||
'align': 'center'
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
@@ -394,11 +394,39 @@ class ImdbSource(_PluginBase):
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
meta_chips = [
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"append-icon": "mdi-trending-up",
|
||||
"size": "small",
|
||||
"href": meter_ranking_url,
|
||||
"target": "_blank"
|
||||
},
|
||||
"text": imdb_title.meter_ranking_text
|
||||
},
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"size": "small",
|
||||
},
|
||||
"text": imdb_title.title_type.text
|
||||
},
|
||||
]
|
||||
if imdb_title.certificate_text:
|
||||
meta_chips.append(
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"size": "small"
|
||||
},
|
||||
"text": imdb_title.certificate_text
|
||||
}
|
||||
)
|
||||
rating_ui = {
|
||||
'component': 'div',
|
||||
'props': {
|
||||
'class': 'mb-2 d-flex align-center',
|
||||
'class': 'd-flex align-center mb-1',
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
@@ -418,21 +446,21 @@ class ImdbSource(_PluginBase):
|
||||
{
|
||||
'component': 'span',
|
||||
'props': {
|
||||
'class': 'text-body-2 ml-1',
|
||||
'class': 'text-truncate text-body-2 ml-1',
|
||||
'style': 'color: rgba(231, 227, 252, 0.8);'
|
||||
},
|
||||
'text': f"{imdb_title.rating_text}/10",
|
||||
'text': f"{imdb_title.rating_text}",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'span',
|
||||
'props': {
|
||||
'class': 'text-warning font-weight-bold ml-4',
|
||||
'class': 'text-truncate text-warning font-weight-bold ml-4',
|
||||
'color': 'warning'
|
||||
},
|
||||
'text': entry.detail,
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -455,7 +483,7 @@ class ImdbSource(_PluginBase):
|
||||
'component': 'span',
|
||||
'html': f"{entry.name}",
|
||||
'props': {
|
||||
'class': 'line-clamp-2 overflow-hidden',
|
||||
'class': 'text-truncate overflow-hidden',
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -468,6 +496,13 @@ class ImdbSource(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": 'div',
|
||||
"props": {
|
||||
"class": "d-flex align-center gap-1 mb-2",
|
||||
},
|
||||
"content": meta_chips
|
||||
},
|
||||
rating_ui,
|
||||
{
|
||||
'component': 'span',
|
||||
@@ -499,14 +534,16 @@ class ImdbSource(_PluginBase):
|
||||
{
|
||||
'component': 'VCardText',
|
||||
'props': {
|
||||
'class': 'd-flex flex-row absolute pa-4 text-white',
|
||||
'class': 'd-flex flex-row absolute pa-4 text-white h-100',
|
||||
'style': 'z-index: 2; bottom: 0; max-width: 100%;',
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VRow',
|
||||
'props': {
|
||||
'class': 'w-100'
|
||||
'class': 'w-100',
|
||||
'align': "end",
|
||||
'align-md': "center"
|
||||
},
|
||||
'content': [
|
||||
# 左图:海报
|
||||
@@ -514,7 +551,8 @@ class ImdbSource(_PluginBase):
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 3
|
||||
'md': 3,
|
||||
'class': 'd-flex justify-center align-center'
|
||||
},
|
||||
'content': [
|
||||
poster_ui
|
||||
@@ -1057,16 +1095,17 @@ class ImdbSource(_PluginBase):
|
||||
return res
|
||||
|
||||
async def imdb_discover(self, mtype: str = "series",
|
||||
country: str = None,
|
||||
lang: str = None,
|
||||
genre: str = None,
|
||||
country: str | None = None,
|
||||
lang: str | None = None,
|
||||
genre: str | None = None,
|
||||
company: str | None = None,
|
||||
sort_by: str = 'POPULARITY',
|
||||
sort_order: str = 'DESC',
|
||||
using_rating: bool = False,
|
||||
user_rating: list[int] = Query(None, alias="user_rating[]"),
|
||||
year: str = None,
|
||||
award: str = None,
|
||||
ranked_list: str = None,
|
||||
year: str | None = None,
|
||||
award: str | None = None,
|
||||
ranked_list: str | None = None,
|
||||
page: int = 1) -> List[schemas.MediaInfo]:
|
||||
|
||||
if not self._imdb_helper:
|
||||
@@ -1086,41 +1125,16 @@ class ImdbSource(_PluginBase):
|
||||
release_date_start = None
|
||||
release_date_end = None
|
||||
if year:
|
||||
if year == "2025":
|
||||
release_date_start = "2025-01-01"
|
||||
elif year == "2024":
|
||||
release_date_start = "2024-01-01"
|
||||
release_date_end = "2024-12-31"
|
||||
elif year == "2023":
|
||||
release_date_start = "2023-01-01"
|
||||
release_date_end = "2023-12-31"
|
||||
elif year == "2022":
|
||||
release_date_start = "2022-01-01"
|
||||
release_date_end = "2022-12-31"
|
||||
elif year == "2021":
|
||||
release_date_start = "2021-01-01"
|
||||
release_date_end = "2021-12-31"
|
||||
elif year == "2020":
|
||||
release_date_start = "2020-01-01"
|
||||
release_date_end = "2020-12-31"
|
||||
elif year == "2020s":
|
||||
release_date_start = "2020-01-01"
|
||||
release_date_end = "2029-12-31"
|
||||
elif year == "2010s":
|
||||
release_date_start = "2010-01-01"
|
||||
release_date_end = "2019-12-31"
|
||||
elif year == "2000s":
|
||||
release_date_start = "2000-01-01"
|
||||
release_date_end = "2009-12-31"
|
||||
elif year == "1990s":
|
||||
release_date_start = "1990-01-01"
|
||||
release_date_end = "1999-12-31"
|
||||
elif year == "1980s":
|
||||
release_date_start = "1980-01-01"
|
||||
release_date_end = "1989-12-31"
|
||||
elif year == "1970s":
|
||||
release_date_start = "1970-01-01"
|
||||
release_date_end = "1979-12-31"
|
||||
if year == f"{datetime.now().year}":
|
||||
release_date_start = f"{datetime.now().year}-01-01"
|
||||
elif year.endswith("s"):
|
||||
decade = int(year[:-2])
|
||||
release_date_start = f"{decade}0-01-01"
|
||||
release_date_end = f"{decade}9-12-31"
|
||||
else:
|
||||
release_date_start = f"{year}-01-01"
|
||||
release_date_end = f"{year}-12-31"
|
||||
|
||||
if not release_date_end:
|
||||
release_date_end = datetime.now().date().strftime("%Y-%m-%d")
|
||||
if sort_by == 'POPULARITY':
|
||||
@@ -1142,7 +1156,8 @@ class ImdbSource(_PluginBase):
|
||||
release_date_end=release_date_end,
|
||||
release_date_start=release_date_start,
|
||||
award_constraint=awards,
|
||||
ranked=ranked_lists
|
||||
ranked=ranked_lists,
|
||||
company=company
|
||||
)
|
||||
results = await self._imdb_helper.async_advanced_title_search(search_params, first_page=first_page)
|
||||
res: List[schemas.MediaInfo] = []
|
||||
@@ -1339,21 +1354,15 @@ class ImdbSource(_PluginBase):
|
||||
} for key, value in sort_order_dict.items()
|
||||
]
|
||||
|
||||
year_dict = {
|
||||
"2025": "2025",
|
||||
"2024": "2024",
|
||||
"2023": "2023",
|
||||
"2022": "2022",
|
||||
"2021": "2021",
|
||||
"2020": "2020",
|
||||
year_dict = {str(year): str(year) for year in range(datetime.now().year, 2019, -1)}
|
||||
year_dict.update({
|
||||
"2020s": "2020s",
|
||||
"2010s": "2010s",
|
||||
"2000s": "2000s",
|
||||
"1990s": "1990s",
|
||||
"1980s": "1980s",
|
||||
"1970s": "1970s",
|
||||
}
|
||||
|
||||
})
|
||||
year_ui = [
|
||||
{
|
||||
"component": "VChip",
|
||||
@@ -1394,12 +1403,12 @@ class ImdbSource(_PluginBase):
|
||||
]
|
||||
|
||||
ranked_list_dict = {
|
||||
"TOP_RATED_MOVIES-100": "IMDb Top 100",
|
||||
"TOP_RATED_MOVIES-250": "IMDb Top 250",
|
||||
"TOP_RATED_MOVIES-1000": "IMDb Top 1000",
|
||||
"LOWEST_RATED_MOVIES-100": "IMDb Bottom 100",
|
||||
"LOWEST_RATED_MOVIES-250": "IMDb Bottom 250",
|
||||
"LOWEST_RATED_MOVIES-1000": "IMDb Bottom 1000",
|
||||
"TOP_RATED_MOVIES-100": "Top 100",
|
||||
"TOP_RATED_MOVIES-250": "Top 250",
|
||||
"TOP_RATED_MOVIES-1000": "Top 1000",
|
||||
"LOWEST_RATED_MOVIES-100": "Bottom 100",
|
||||
"LOWEST_RATED_MOVIES-250": "Bottom 250",
|
||||
"LOWEST_RATED_MOVIES-1000": "Bottom 1000",
|
||||
}
|
||||
|
||||
ranked_list_ui = [
|
||||
@@ -1414,6 +1423,41 @@ class ImdbSource(_PluginBase):
|
||||
} for key, value in ranked_list_dict.items()
|
||||
]
|
||||
|
||||
companies = {
|
||||
"20th Century Fox": "20世纪福克斯",
|
||||
"DreamWorks": "梦工厂",
|
||||
"MGM": "米高梅",
|
||||
"Paramount": "派拉蒙",
|
||||
"Sony": "索尼",
|
||||
"Universal": "环球",
|
||||
"Walt Disney": "迪士尼",
|
||||
"Warner Bros.": "华纳兄弟",
|
||||
"HBO": "HBO",
|
||||
"Netflix": "Netflix",
|
||||
"Hulu": "Hulu",
|
||||
"Amazon Prime Video": "Amazon Prime",
|
||||
"Apple TV": "Apple TV",
|
||||
"British Broadcasting Corporation (BBC)": "BBC",
|
||||
"Tencent Video": "腾讯视频",
|
||||
"Youku": "优酷",
|
||||
"iQIYI": "爱奇艺",
|
||||
"China Central Television (CCTV)": "CCTV",
|
||||
"Huayi Brothers Media": "华谊兄弟",
|
||||
"Beijing Enlight Pictures": "光线传媒",
|
||||
"Bona Film Group": "博纳影业",
|
||||
}
|
||||
companies_ui = [
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"filter": True,
|
||||
"tile": True,
|
||||
"value": key
|
||||
},
|
||||
"text": value
|
||||
} for key, value in companies.items()
|
||||
]
|
||||
|
||||
return [
|
||||
{
|
||||
"component": "div",
|
||||
@@ -1596,6 +1640,33 @@ class ImdbSource(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "flex justify-start items-center",
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "mr-5"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "VLabel",
|
||||
"text": "出品方"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "VChipGroup",
|
||||
"props": {
|
||||
"model": "company"
|
||||
},
|
||||
"content": companies_ui
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
@@ -1750,7 +1821,7 @@ class ImdbSource(_PluginBase):
|
||||
"user_rating": [1, 10],
|
||||
"using_rating": False,
|
||||
"award": None,
|
||||
"ranked_list": None
|
||||
"ranked_list": None,
|
||||
},
|
||||
depends={
|
||||
"ranked_list": ["mtype"]
|
||||
|
||||
@@ -11,7 +11,7 @@ from app.utils.http import RequestUtils, AsyncRequestUtils
|
||||
from .schema.imdbapi import ImdbApiTitle, ImdbApiEpisode, ImdbApiCredit, ImdbapiImage
|
||||
from .schema.imdbapi import (ImdbApiSearchTitlesResponse, ImdbApiListTitlesResponse, ImdbApiListTitleEpisodesResponse,
|
||||
ImdbApiListTitleSeasonsResponse, ImdbApiListTitleCreditsResponse,
|
||||
ImdbapiListTitleAKAsResponse, ImdbApiTitleImagesResponse)
|
||||
ImdbapiListTitleAKAsResponse, ImdbApiTitleImagesResponse, ImdbapiCompanyCreditResponse)
|
||||
from .schema.imdbtypes import ImdbType
|
||||
|
||||
|
||||
@@ -769,3 +769,24 @@ class ImdbApiClient:
|
||||
page_token = response.next_page_token
|
||||
if not page_token:
|
||||
break
|
||||
|
||||
async def company_credits(self, title_id: str, categories: list[str] | None = None
|
||||
) -> Optional[ImdbapiCompanyCreditResponse]:
|
||||
"""
|
||||
Retrieve the company credits associated with a specific title.
|
||||
|
||||
:param title_id: Required. IMDb title ID in the format "tt1234567".
|
||||
:param categories: Optional. The categories of company credit to filter by.
|
||||
:return: Company Credits.
|
||||
"""
|
||||
path = "/titles/%s/companyCredits"
|
||||
param: dict[str, Any] = {}
|
||||
if categories:
|
||||
param['categories'] = categories
|
||||
try:
|
||||
r = await self._async_free_imdb_api(path=path % title_id, params=param)
|
||||
ret = ImdbapiCompanyCreditResponse.model_validate(r)
|
||||
except Exception as e:
|
||||
logger.debug(f"An error occurred while retrieving company credits: {e}")
|
||||
return None
|
||||
return ret
|
||||
|
||||
@@ -113,7 +113,7 @@ class ImdbHelper:
|
||||
logger.error("Error getting staff picks")
|
||||
return None
|
||||
try:
|
||||
data = StaffPickApiResponse.model_validate_json(res)
|
||||
data = StaffPickApiResponse.model_validate_json(res, by_name=True)
|
||||
except (JSONDecodeError, ValidationError):
|
||||
return None
|
||||
return data
|
||||
@@ -210,7 +210,8 @@ class ImdbHelper:
|
||||
return key
|
||||
return ""
|
||||
|
||||
async def advanced_title_search_generator(self, params: SearchParams, first_page: bool = True) -> AsyncGenerator[TitleEdge, None]:
|
||||
async def advanced_title_search_generator(self, params: SearchParams, first_page: bool = True
|
||||
) -> AsyncGenerator[TitleEdge, None]:
|
||||
await self._async_update_hash()
|
||||
sha256 = self._imdb_api_hash.advanced_title_search
|
||||
if not first_page and params in self._title_generators:
|
||||
@@ -253,7 +254,7 @@ class ImdbHelper:
|
||||
seasons_dict[s] = episode.release_date
|
||||
return seasons_dict
|
||||
|
||||
def match_by(self, name: str, mtype: Optional[MediaType] = None, year: Optional[str] = None) -> Optional[ImdbMediaInfo]:
|
||||
def match_by(self, name: str, mtype: MediaType | None = None, year: str | None = None) -> ImdbMediaInfo | None:
|
||||
"""
|
||||
根据名称同时查询电影和电视剧,没有类型也没有年份时使用
|
||||
|
||||
|
||||
@@ -275,10 +275,35 @@ INTERESTS_ID: Final[Dict[str, Dict[str, str]]] = {
|
||||
"Western Epic": "in0000189"
|
||||
}
|
||||
}
|
||||
|
||||
COMPANY_ID = {
|
||||
"20th Century Fox": ["co0000756", "co0176225", "co0201557", "co0017497"],
|
||||
"DreamWorks": ["co0067641", "co0040938", "co0252576", "co0003158"],
|
||||
"MGM": ["co0007143", "co0026841"],
|
||||
"Paramount": ["co0023400"],
|
||||
"Sony": ["co0050868", "co0026545", "co0121181"],
|
||||
"Universal": ["co0005073", "co0055277", "co0042399"],
|
||||
"Walt Disney": ["co0008970", "co0017902", "co0098836", "co0059516", "co0092035", "co0049348"],
|
||||
"Warner Bros.": ["co0002663", "co0005035", "co0863266", "co0072876", "co0080422", "co0046718"],
|
||||
"HBO": ["co0008693", "co0754095", "co0306346", "co0148466", "co0909975", "co0638197", "co0391378"],
|
||||
"Netflix": ["co0144901", "co0805756"],
|
||||
"Hulu": ["co0218858", "co0381648"],
|
||||
"Amazon Prime Video": ["co0476953", "co1160313", "co0939864", "co0931938"],
|
||||
"Apple TV": ["co0931939", "co0546168"],
|
||||
"British Broadcasting Corporation (BBC)": ['co0043107'],
|
||||
"Tencent Video": ["co0487058"],
|
||||
"Youku": ["co0264223"],
|
||||
"iQIYI": ["co0493506", "co0691262"],
|
||||
"China Central Television (CCTV)": ['co0001524'],
|
||||
"Huayi Brothers Media": ["co0099734"],
|
||||
"Beijing Enlight Pictures": ["co0208796"],
|
||||
"Bona Film Group": ["co0452101"],
|
||||
}
|
||||
|
||||
CACHE_LIFETIME: Final[int] = 86400
|
||||
IMDB_GRAPHQL_QUERY: Final[str] = dedent("""
|
||||
query VerticalListPageItems( $titles: [ID!]! $names: [ID!]! $images: [ID!]! $videos: [ID!]!) {
|
||||
titles(ids: $titles) { ...TitleParts meterRanking { currentRank meterType rankChange {changeDirection difference} } ratingsSummary { aggregateRating } }
|
||||
titles(ids: $titles) { ...TitleParts meterRanking { currentRank meterType rankChange {changeDirection difference} } ratingsSummary { aggregateRating voteCount} }
|
||||
names(ids: $names) { ...NameParts }
|
||||
videos(ids: $videos) { ...VideoParts }
|
||||
images(ids: $images) { ...ImageParts }
|
||||
@@ -500,6 +525,12 @@ class OfficialApiClient:
|
||||
if in_id:
|
||||
constraints.append(in_id)
|
||||
variables["interestConstraint"] = {"allInterestIds": constraints, "excludeInterestIds": []}
|
||||
|
||||
if params.company:
|
||||
company_ids = COMPANY_ID.get(params.company)
|
||||
if company_ids:
|
||||
variables["creditedCompanyConstraint"] = {"anyCompanyIds": company_ids, "excludeCompanyIds": []}
|
||||
|
||||
if last_cursor:
|
||||
variables["after"] = last_cursor
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ class ErrorType(Enum):
|
||||
|
||||
class StaffPickEntry(BaseModel):
|
||||
name: str
|
||||
ttconst: str
|
||||
ttconst: str = Field(..., alias='id')
|
||||
rmconst: str
|
||||
detail: Optional[str] = ""
|
||||
description: Optional[str] = ""
|
||||
relatedconst: List[str] = Field(default_factory=list)
|
||||
relatedconst: List[str] = Field(default_factory=list, alias='relatedConst')
|
||||
viconst: Optional[str] = None
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ class SearchParams(BaseModel):
|
||||
award_constraint: Optional[Tuple[str, ...]] = None
|
||||
ranked: Optional[Tuple[str, ...]] = None
|
||||
interests: Optional[Tuple[str, ...]] = None
|
||||
company: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(
|
||||
frozen=True
|
||||
|
||||
@@ -154,3 +154,20 @@ class ImdbapiListTitleAKAsResponse(BaseModel):
|
||||
|
||||
class ImdbApiTitleImagesResponse(PagedResponse):
|
||||
images: List[ImdbapiImage] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ImdbapiCompany(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class ImdbapiCompanyCredit(BaseModel):
|
||||
company: ImdbapiCompany
|
||||
category: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Category of the company credit, such as production, sales, distribution, etc."
|
||||
)
|
||||
|
||||
|
||||
class ImdbapiCompanyCreditResponse(PagedResponse):
|
||||
company_credits: List[ImdbapiCompanyCredit] = Field(default_factory=list, alias='companyCredits')
|
||||
|
||||
@@ -4,6 +4,15 @@ from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
def format_number(n: int) -> str:
|
||||
units = ["", "K", "M", "B", "T"]
|
||||
idx = 0
|
||||
while n >= 1000 and idx < len(units) - 1:
|
||||
n //= 1000
|
||||
idx += 1
|
||||
return f"{n}{units[idx]}"
|
||||
|
||||
|
||||
class ImdbType(Enum):
|
||||
TV_SERIES = "tvSeries"
|
||||
TV_MINI_SERIES = "tvMiniSeries"
|
||||
@@ -23,6 +32,25 @@ class ImdbType(Enum):
|
||||
class TitleType(BaseModel):
|
||||
id: ImdbType
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
type_mapping = {
|
||||
ImdbType.TV_SERIES: "TV Series",
|
||||
ImdbType.TV_MINI_SERIES: "TV Mini Series",
|
||||
ImdbType.MOVIE: "Movie",
|
||||
ImdbType.TV_MOVIE: "TV Movie",
|
||||
ImdbType.MUSIC_VIDEO: "Music Video",
|
||||
ImdbType.TV_SHORT: "TV Short",
|
||||
ImdbType.SHORT: "Short",
|
||||
ImdbType.TV_EPISODE: "TV Episode",
|
||||
ImdbType.TV_SPECIAL: "TV Special",
|
||||
ImdbType.VIDEO_GAME: "Video Game",
|
||||
ImdbType.VIDEO: "Video",
|
||||
ImdbType.PODCAST_SERIES: "Podcast Series",
|
||||
ImdbType.PODCAST_EPISODE: "Podcast Episode",
|
||||
}
|
||||
return type_mapping.get(self.id, "Unknown")
|
||||
|
||||
|
||||
class ReleaseYear(BaseModel):
|
||||
year: Optional[int] = None
|
||||
@@ -89,6 +117,23 @@ class MeterRanking(BaseModel):
|
||||
meter_type: Optional[str] = Field(default=None, alias='meterType')
|
||||
rank_change: Optional[RankChange] = Field(default=None, alias='rankChange')
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
if self.current_rank:
|
||||
rank = self.current_rank
|
||||
meter_rank = ""
|
||||
if self.meter_type:
|
||||
meter_rank = self.meter_type.replace("_", "").replace("METER", "Meter")
|
||||
meter_rank = f" {meter_rank}"
|
||||
return f"#{rank}{meter_rank}"
|
||||
return ""
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
if self.current_rank and self.meter_type:
|
||||
return f"https://www.imdb.com/chart/{self.meter_type.replace("_", "").lower()}/"
|
||||
return ""
|
||||
|
||||
|
||||
class RatingsSummary(BaseModel):
|
||||
aggregate_rating: Optional[float] = Field(default=None, alias='aggregateRating')
|
||||
@@ -154,8 +199,24 @@ class ImdbTitle(BaseModel):
|
||||
@property
|
||||
def rating_text(self) -> str:
|
||||
if self.ratings_summary and self.ratings_summary.aggregate_rating:
|
||||
return f"{self.ratings_summary.aggregate_rating:.1f}"
|
||||
return "-"
|
||||
votes = ""
|
||||
if self.ratings_summary.vote_count:
|
||||
votes = f" ({format_number(self.ratings_summary.vote_count)})"
|
||||
return f"{self.ratings_summary.aggregate_rating:.1f}{votes}"
|
||||
return "-/10"
|
||||
|
||||
@property
|
||||
def meter_ranking_text(self) -> str:
|
||||
if self.meter_ranking and self.meter_ranking.current_rank:
|
||||
return self.meter_ranking.text
|
||||
return ""
|
||||
|
||||
@property
|
||||
def certificate_text(self) -> str:
|
||||
if self.certificate and self.certificate.rating:
|
||||
return self.certificate.rating
|
||||
return ""
|
||||
|
||||
|
||||
class Thumbnail(BaseModel):
|
||||
url: str
|
||||
|
||||
@@ -60,7 +60,7 @@ class LexiAnnot(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "LexiAnnot.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.2.3"
|
||||
plugin_version = "1.2.4"
|
||||
# 插件作者
|
||||
plugin_author = "wumode"
|
||||
# 作者主页
|
||||
|
||||
@@ -509,10 +509,6 @@ Your goal is two-fold:
|
||||
* **Do NOT include** simple high-frequency words, common fillers ('gonna', 'gotta'), onomatopoeia, or basic swear words.
|
||||
|
||||
-------------------------
|
||||
You MUST return output strictly matching the provided Pydantic schema.
|
||||
Return ONLY valid JSON.
|
||||
|
||||
**Here are the output format instructions you MUST follow strictly:**
|
||||
{format_instructions}
|
||||
""",
|
||||
),
|
||||
@@ -556,10 +552,6 @@ For each word (identified by `WORD_ID`), provide:
|
||||
**Your judgment should be based strictly on the provided subtitle context. DO NOT fabricate context or forced explanation.**
|
||||
|
||||
-------------------------
|
||||
You MUST return output strictly matching the provided Pydantic schema.
|
||||
Return ONLY valid JSON.
|
||||
|
||||
**Here are the output format instructions you MUST follow strictly:**
|
||||
{format_instructions}
|
||||
""",
|
||||
),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import re
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from enum import Enum
|
||||
from enum import Enum, StrEnum
|
||||
from typing import Literal, Generator, Iterator
|
||||
|
||||
from pydantic import BaseModel, Field, RootModel, model_validator
|
||||
from pydantic import BaseModel, Field, RootModel, model_validator, field_validator
|
||||
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
@@ -12,9 +12,8 @@ from app.utils.singleton import Singleton
|
||||
Cefr = Literal["C2", "C1", "B2", "B1", "A2", "A1"]
|
||||
|
||||
|
||||
class UniversalPos(str, Enum):
|
||||
class UniversalPos(StrEnum):
|
||||
"""Universal Part-of-Speech tags"""
|
||||
|
||||
ADJ = "ADJ" # Adjective
|
||||
ADV = "ADV" # Adverb
|
||||
INTJ = "INTJ" # Interjection
|
||||
@@ -34,9 +33,8 @@ class UniversalPos(str, Enum):
|
||||
X = "X" # Other/unknown
|
||||
|
||||
|
||||
class LexicalFeatures(str, Enum):
|
||||
class LexicalFeatures(StrEnum):
|
||||
"""Lexical features for words."""
|
||||
|
||||
FORMAL = "formal"
|
||||
INFORMAL = "informal"
|
||||
SLANG = "slang"
|
||||
@@ -333,6 +331,14 @@ class LlmWordEnrichment(BaseModel):
|
||||
usage_context: str | None = Field(default=None, description="Usage or Cultural Context")
|
||||
lexical_features: list[LexicalFeatures] = Field(default_factory=list, description="Lexical features")
|
||||
|
||||
@field_validator("lexical_features", mode="before")
|
||||
@classmethod
|
||||
def filter_invalid_lexical_features(cls, v):
|
||||
if isinstance(v, list):
|
||||
valid_values = {f.value for f in LexicalFeatures}
|
||||
return [item for item in v if item in valid_values]
|
||||
return v
|
||||
|
||||
|
||||
class LlmEnrichmentResult(BaseModel):
|
||||
enriched_words: list[LlmWordEnrichment] = Field(default_factory=list, description="List of enriched word data")
|
||||
|
||||
@@ -29,6 +29,7 @@ class MediaServerMsg(_PluginBase):
|
||||
# 常量定义
|
||||
DEFAULT_EXPIRATION_TIME = 600 # 默认过期时间(秒)
|
||||
DEFAULT_AGGREGATE_TIME = 15 # 默认聚合时间(秒)
|
||||
DEDUPE_EXPIRATION_TIME = 30 # 去重缓存过期时间(秒)
|
||||
|
||||
# 插件基本信息
|
||||
plugin_name = "媒体库服务器通知"
|
||||
@@ -37,7 +38,7 @@ class MediaServerMsg(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "mediaplay.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.8.2.1"
|
||||
plugin_version = "1.8.2.2"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
@@ -59,7 +60,8 @@ class MediaServerMsg(_PluginBase):
|
||||
|
||||
# TV剧集消息聚合配置
|
||||
_aggregate_time = DEFAULT_AGGREGATE_TIME # 聚合时间窗口(秒)
|
||||
_pending_messages = {} # 待聚合的消息 {series_key: [event_info, ...]}
|
||||
# 待聚合的消息 {series_key: [event_info, ...]}
|
||||
_pending_messages = {}
|
||||
_aggregate_timers = {} # 聚合定时器 {series_key: timer}
|
||||
|
||||
# Webhook事件映射配置
|
||||
@@ -102,8 +104,8 @@ class MediaServerMsg(_PluginBase):
|
||||
self._mediaservers = config.get("mediaservers") or []
|
||||
self._add_play_link = config.get("add_play_link", False)
|
||||
self._aggregate_enabled = config.get("aggregate_enabled", False)
|
||||
self._aggregate_time = int(config.get("aggregate_time", self.DEFAULT_AGGREGATE_TIME))
|
||||
|
||||
self._aggregate_time = int(config.get(
|
||||
"aggregate_time", self.DEFAULT_AGGREGATE_TIME))
|
||||
|
||||
def service_infos(self, type_filter: Optional[str] = None) -> Optional[Dict[str, ServiceInfo]]:
|
||||
"""
|
||||
@@ -119,7 +121,8 @@ class MediaServerMsg(_PluginBase):
|
||||
logger.warning("尚未配置媒体服务器,请检查配置")
|
||||
return None
|
||||
|
||||
services = MediaServerHelper().get_services(type_filter=type_filter, name_filters=self._mediaservers)
|
||||
services = MediaServerHelper().get_services(
|
||||
type_filter=type_filter, name_filters=self._mediaservers)
|
||||
if not services:
|
||||
logger.warning("获取媒体服务器实例失败,请检查配置")
|
||||
return None
|
||||
@@ -454,6 +457,18 @@ class MediaServerMsg(_PluginBase):
|
||||
logger.info(f"未开启媒体服务器类型 {channel} 的消息通知")
|
||||
return
|
||||
|
||||
# 通用去重:构造去重键
|
||||
item_id = getattr(event_info, 'item_id', '')
|
||||
if item_id:
|
||||
# 使用 server_name + event_type + item_id 作为唯一标识
|
||||
dedupe_key = f"{server_name}-{event_type}-{item_id}" if server_name else f"{event_type}-{item_id}"
|
||||
# 检查是否已处理过该事件
|
||||
if dedupe_key in self.__get_elements():
|
||||
logger.debug(f"检测到重复Webhook事件,已处理过: {dedupe_key}")
|
||||
return
|
||||
# 添加到去重缓存(30秒过期)
|
||||
self.__add_element(dedupe_key, duration=self.DEDUPE_EXPIRATION_TIME)
|
||||
|
||||
# TV剧集结入库聚合处理
|
||||
logger.debug("检查是否需要进行TV剧集聚合处理")
|
||||
|
||||
@@ -549,7 +564,8 @@ class MediaServerMsg(_PluginBase):
|
||||
if overview:
|
||||
message_texts.append(f"剧情:{overview}")
|
||||
|
||||
message_texts.append(f"时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}")
|
||||
message_texts.append(
|
||||
f"时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}")
|
||||
|
||||
# 消息内容
|
||||
message_content = "\n".join(message_texts)
|
||||
@@ -684,7 +700,8 @@ class MediaServerMsg(_PluginBase):
|
||||
# 设置新的定时器
|
||||
logger.debug(f"设置新的定时器,将在 {self._aggregate_time} 秒后触发")
|
||||
try:
|
||||
timer = threading.Timer(self._aggregate_time, self._send_aggregated_message, [series_id])
|
||||
timer = threading.Timer(
|
||||
self._aggregate_time, self._send_aggregated_message, [series_id])
|
||||
self._aggregate_timers[series_id] = timer
|
||||
timer.start()
|
||||
except Exception as e:
|
||||
@@ -692,7 +709,8 @@ class MediaServerMsg(_PluginBase):
|
||||
# 如果定时器设置失败,直接发送消息
|
||||
self._send_aggregated_message(series_id)
|
||||
|
||||
logger.debug(f"已添加剧集 {series_id} 的消息到聚合队列,当前队列长度: {len(self._pending_messages.get(series_id, []))},定时器将在 {self._aggregate_time} 秒后触发")
|
||||
logger.debug(
|
||||
f"已添加剧集 {series_id} 的消息到聚合队列,当前队列长度: {len(self._pending_messages.get(series_id, []))},定时器将在 {self._aggregate_time} 秒后触发")
|
||||
logger.debug(f"完成聚合处理: series_id={series_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"聚合处理过程中出现异常: {str(e)}", exc_info=True)
|
||||
@@ -804,7 +822,8 @@ class MediaServerMsg(_PluginBase):
|
||||
# 确保索引在有效范围内
|
||||
if 0 <= ep_index < len(episodes):
|
||||
episode_info = episodes[ep_index]
|
||||
episode_overview = episode_info.get('overview', '')
|
||||
episode_overview = episode_info.get(
|
||||
'overview', '')
|
||||
|
||||
# 如果该集的概述存在且非空,则返回该集概述
|
||||
if episode_overview:
|
||||
@@ -824,7 +843,8 @@ class MediaServerMsg(_PluginBase):
|
||||
# 使用原有逻辑构造消息
|
||||
message_title = f"📺 {self._webhook_actions.get(first_event.event)}剧集:{first_event.item_name}"
|
||||
message_texts = []
|
||||
message_texts.append(f"⏰ 时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}")
|
||||
message_texts.append(
|
||||
f"⏰ 时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}")
|
||||
|
||||
# 收集集数信息
|
||||
episode_details = []
|
||||
@@ -832,17 +852,20 @@ class MediaServerMsg(_PluginBase):
|
||||
if (hasattr(event, 'season_id') and event.season_id is not None and
|
||||
hasattr(event, 'episode_id') and event.episode_id is not None):
|
||||
try:
|
||||
episode_details.append(f"S{int(event.season_id):02d}E{int(event.episode_id):02d}")
|
||||
episode_details.append(
|
||||
f"S{int(event.season_id):02d}E{int(event.episode_id):02d}")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if episode_details:
|
||||
message_texts.append(f"📺 季集:{', '.join(episode_details)}")
|
||||
message_texts.append(
|
||||
f"📺 季集:{', '.join(episode_details)}")
|
||||
|
||||
message_content = "\n".join(message_texts)
|
||||
|
||||
# 使用默认图片
|
||||
image_url = getattr(first_event, 'image_url', None) or self._webhook_images.get(getattr(first_event, 'channel', ''))
|
||||
image_url = getattr(first_event, 'image_url', None) or self._webhook_images.get(
|
||||
getattr(first_event, 'channel', ''))
|
||||
|
||||
# 处理播放链接
|
||||
play_link = None
|
||||
@@ -868,7 +891,8 @@ class MediaServerMsg(_PluginBase):
|
||||
except Exception as e:
|
||||
logger.error(f"获取TMDB信息时出错: {str(e)}")
|
||||
|
||||
overview = safe_get_overview(tmdb_info, first_event, is_multiple_episodes)
|
||||
overview = safe_get_overview(
|
||||
tmdb_info, first_event, is_multiple_episodes)
|
||||
|
||||
# 消息标题
|
||||
show_name = first_event.item_name
|
||||
@@ -894,7 +918,8 @@ class MediaServerMsg(_PluginBase):
|
||||
# 消息内容
|
||||
message_texts = []
|
||||
# 时间信息放在最前面
|
||||
message_texts.append(f"⏰ 时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}")
|
||||
message_texts.append(
|
||||
f"⏰ 时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}")
|
||||
# 添加每个集数的信息并合并连续集数
|
||||
episodes_detail = self._merge_continuous_episodes(events)
|
||||
message_texts.append(f"📺 季集:{episodes_detail}")
|
||||
@@ -993,7 +1018,8 @@ class MediaServerMsg(_PluginBase):
|
||||
play_link = self._get_play_link(first_event)
|
||||
|
||||
# 发送聚合消息
|
||||
logger.debug(f"准备发送消息 - 标题: {message_title}, 内容: {message_content}, 图片: {image_url}")
|
||||
logger.debug(
|
||||
f"准备发送消息 - 标题: {message_title}, 内容: {message_content}, 图片: {image_url}")
|
||||
self.post_message(mtype=NotificationType.MediaServer,
|
||||
title=message_title, text=message_content, image=image_url, link=play_link)
|
||||
|
||||
@@ -1105,26 +1131,31 @@ class MediaServerMsg(_PluginBase):
|
||||
else:
|
||||
# 保存当前区间
|
||||
if start == end:
|
||||
merged_details.append(f"S{season:02d}E{start:02d} {episode_names[0]}")
|
||||
merged_details.append(
|
||||
f"S{season:02d}E{start:02d} {episode_names[0]}")
|
||||
else:
|
||||
# 合并区间
|
||||
merged_details.append(f"S{season:02d}E{start:02d}-E{end:02d}")
|
||||
merged_details.append(
|
||||
f"S{season:02d}E{start:02d}-E{end:02d}")
|
||||
# 开始新区间
|
||||
start = end = current
|
||||
episode_names = [episodes[i]["name"]]
|
||||
|
||||
# 添加最后一个区间
|
||||
if start == end:
|
||||
merged_details.append(f"S{season:02d}E{start:02d} {episode_names[-1] if episode_names else ''}")
|
||||
merged_details.append(
|
||||
f"S{season:02d}E{start:02d} {episode_names[-1] if episode_names else ''}")
|
||||
else:
|
||||
merged_details.append(f"S{season:02d}E{start:02d}-E{end:02d}")
|
||||
merged_details.append(
|
||||
f"S{season:02d}E{start:02d}-E{end:02d}")
|
||||
except Exception as e:
|
||||
logger.error(f"合并集数信息时出错: {str(e)}")
|
||||
# 出错时返回简单的集数列表
|
||||
simple_details = []
|
||||
for season in sorted(season_episodes.keys()):
|
||||
for episode_info in season_episodes[season]:
|
||||
simple_details.append(f"S{season:02d}E{episode_info['episode']:02d}")
|
||||
simple_details.append(
|
||||
f"S{season:02d}E{episode_info['episode']:02d}")
|
||||
return ", ".join(simple_details)
|
||||
|
||||
return ", ".join(merged_details)
|
||||
@@ -1148,7 +1179,8 @@ class MediaServerMsg(_PluginBase):
|
||||
Args:
|
||||
key (str): 要移除的元素键值
|
||||
"""
|
||||
self._webhook_msg_keys = {k: v for k, v in self._webhook_msg_keys.items() if k != key}
|
||||
self._webhook_msg_keys = {
|
||||
k: v for k, v in self._webhook_msg_keys.items() if k != key}
|
||||
|
||||
def __get_elements(self):
|
||||
"""
|
||||
@@ -1250,11 +1282,11 @@ class MediaServerMsg(_PluginBase):
|
||||
if mtype == MediaType.MOVIE:
|
||||
return self.chain.tmdb_info(tmdbid=tmdb_id, mtype=mtype)
|
||||
else: # TV类型
|
||||
tmdb_info = self.chain.tmdb_info(tmdbid=tmdb_id, mtype=mtype, season=season)
|
||||
tmdb_info = self.chain.tmdb_info(
|
||||
tmdbid=tmdb_id, mtype=mtype, season=season)
|
||||
tmdb_info2 = self.chain.tmdb_info(tmdbid=tmdb_id, mtype=mtype)
|
||||
return tmdb_info | tmdb_info2
|
||||
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件时的清理工作
|
||||
|
||||
@@ -23,7 +23,7 @@ class MoviePilotUpdateNotify(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Moviepilot_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "2.3"
|
||||
plugin_version = "2.3.1"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
@@ -83,7 +83,7 @@ class MoviePilotUpdateNotify(_PluginBase):
|
||||
|
||||
# 本地版本
|
||||
local_version = SystemChain().get_server_local_version()
|
||||
if local_version and release_version <= local_version:
|
||||
if local_version and list(map(int, re.findall(r'\d+', release_version))) <= list(map(int, re.findall(r'\d+', local_version))):
|
||||
logger.info(f"当前后端版本:{local_version} 远程版本:{release_version} 停止运行")
|
||||
return False
|
||||
|
||||
@@ -108,7 +108,7 @@ class MoviePilotUpdateNotify(_PluginBase):
|
||||
|
||||
# 本地版本
|
||||
local_version = SystemChain().get_frontend_version()
|
||||
if local_version and release_version <= local_version:
|
||||
if local_version and list(map(int, re.findall(r'\d+', release_version))) <= list(map(int, re.findall(r'\d+', local_version))):
|
||||
logger.info(f"当前前端版本:{local_version} 远程版本:{release_version} 停止运行")
|
||||
return False
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ class AutoSubv2(_PluginBase):
|
||||
# 主题色
|
||||
plugin_color = "#2C4F7E"
|
||||
# 插件版本
|
||||
plugin_version = "2.3"
|
||||
plugin_version = "2.5"
|
||||
# 插件作者
|
||||
plugin_author = "TimoYoung"
|
||||
# 作者主页
|
||||
|
||||
@@ -17,10 +17,16 @@ class OpenAi:
|
||||
compatible: bool = False):
|
||||
self._api_key = api_key
|
||||
self._api_url = api_url
|
||||
openai.api_base = self._api_url if compatible else self._api_url + "/v1"
|
||||
openai.api_key = self._api_key
|
||||
base_url = self._api_url if compatible else self._api_url + "/v1"
|
||||
|
||||
# 创建 OpenAI 客户端实例
|
||||
if proxy and proxy.get("https"):
|
||||
openai.proxy = proxy.get("https")
|
||||
import httpx
|
||||
http_client = httpx.Client(proxies=proxy.get("https"))
|
||||
self.client = openai.OpenAI(api_key=self._api_key, base_url=base_url, http_client=http_client)
|
||||
else:
|
||||
self.client = openai.OpenAI(api_key=self._api_key, base_url=base_url)
|
||||
|
||||
if model:
|
||||
self._model = model
|
||||
|
||||
@@ -92,7 +98,7 @@ class OpenAi:
|
||||
"content": message
|
||||
}
|
||||
]
|
||||
return openai.ChatCompletion.create(
|
||||
return self.client.chat.completions.create(
|
||||
model=self._model,
|
||||
user=user,
|
||||
messages=message,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import re
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
@@ -22,7 +23,7 @@ class MoviePilotUpdateNotify(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Moviepilot_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.5"
|
||||
plugin_version = "1.5.1"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
@@ -82,7 +83,7 @@ class MoviePilotUpdateNotify(_PluginBase):
|
||||
|
||||
# 本地版本
|
||||
local_version = SystemChain().get_server_local_version()
|
||||
if local_version and release_version <= local_version:
|
||||
if local_version and list(map(int, re.findall(r'\d+', release_version))) <= list(map(int, re.findall(r'\d+', local_version))):
|
||||
logger.info(f"当前后端版本:{local_version} 远程版本:{release_version} 停止运行")
|
||||
return False
|
||||
|
||||
@@ -107,7 +108,7 @@ class MoviePilotUpdateNotify(_PluginBase):
|
||||
|
||||
# 本地版本
|
||||
local_version = SystemChain().get_frontend_version()
|
||||
if local_version and release_version <= local_version:
|
||||
if local_version and list(map(int, re.findall(r'\d+', release_version))) <= list(map(int, re.findall(r'\d+', local_version))):
|
||||
logger.info(f"当前前端版本:{local_version} 远程版本:{release_version} 停止运行")
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user