From 5cf62a221a0c41b2463dc7ba9666f263a2473c1d Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sat, 14 Mar 2026 18:34:19 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A6=96=E6=92=AD?= =?UTF-8?q?=E8=AF=95=E7=9C=8B=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins.v2/package.v2.json | 14 + plugins.v2/tvfirstwatch/__init__.py | 677 +++++++++++++++++++++++ plugins.v2/tvfirstwatch/requirements.txt | 1 + 3 files changed, 692 insertions(+) create mode 100644 plugins.v2/package.v2.json create mode 100644 plugins.v2/tvfirstwatch/__init__.py create mode 100644 plugins.v2/tvfirstwatch/requirements.txt diff --git a/plugins.v2/package.v2.json b/plugins.v2/package.v2.json new file mode 100644 index 0000000..455d7ba --- /dev/null +++ b/plugins.v2/package.v2.json @@ -0,0 +1,14 @@ +{ + "TvFirstWatch": { + "name": "首播试看", + "description": "定时抓取RSS,只下载电视剧前N集(首播试看),防重复推送。", + "labels": "订阅,RSS", + "version": "1.0", + "icon": "rss.png", + "author": "Raymond38324", + "level": 2, + "history": { + "v1.0": "首次发布:支持RSS订阅电视剧首播试看" + } + } +} \ No newline at end of file diff --git a/plugins.v2/tvfirstwatch/__init__.py b/plugins.v2/tvfirstwatch/__init__.py new file mode 100644 index 0000000..9086ac0 --- /dev/null +++ b/plugins.v2/tvfirstwatch/__init__.py @@ -0,0 +1,677 @@ +import datetime +import json +import re +from pathlib import Path +from threading import Lock +from typing import Any, Dict, List, Optional, Tuple +from urllib.parse import urlparse + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger + +import feedparser +import requests + +from app.chain.download import DownloadChain +from app.core.config import settings +from app.core.context import Context, MediaInfo, TorrentInfo +from app.core.metainfo import MetaInfo +from app.log import logger +from app.plugins import _PluginBase +from app.helper.event import eventmanager +from app.schemas.types import MediaType, EventType + +lock = Lock() + +# ─── 集数正则(与独立脚本保持一致) ───────────────────────────────────── + +EPISODE_PATTERNS = [ + re.compile(r"[Ss]\d{1,2}[Ee](\d{1,3})", re.IGNORECASE), + re.compile(r"\bEP?\.?(\d{1,3})\b", re.IGNORECASE), + re.compile(r"第\s*(\d{1,3})\s*集"), + re.compile(r"[【\[((](\d{1,3})[】\]))]"), + re.compile(r"\b(\d{1,3})\s*of\s*\d+\b", re.IGNORECASE), +] + +TV_HINTS = re.compile( + r"\b(S\d{1,2}E\d{1,3}|EP?\d{1,3}|第\d+集|Season|Complete|HDTV|WEB-?DL)\b", + re.IGNORECASE, +) + + +class TvFirstWatch(_PluginBase): + """ + 电视剧首播试看自动下载 + + 定时抓取多个 RSS 源,仅下载电视剧前 N 集(默认 1-2 集), + 通过 MoviePilot DownloadChain 触发下载(支持洗版、刮削)。 + """ + + # ── 插件元信息 ────────────────────────────────────────────────────── + plugin_name = "首播试看" + plugin_desc = "定时抓取 RSS,只下载剧集前 N 集(首播试看),防重复推送。" + plugin_icon = "rss.png" + plugin_version = "1.0" + plugin_author = "Raymond38324" + author_url = "https://github.com/Raymond38324" + plugin_config_prefix = "tvfirstwatch_" + plugin_order = 25 + auth_level = 2 + + # ── 私有变量 ───────────────────────────────────────────────────────── + _scheduler: Optional[BackgroundScheduler] = None + _downloadchain: Optional[DownloadChain] = None + _history_path: Optional[Path] = None + + # ── 配置属性 ───────────────────────────────────────────────────────── + _enabled: bool = False + _onlyonce: bool = False + _notify: bool = False + _cron: str = "*/30 * * * *" # cron 轮询周期 + _rss_urls: str = "" # 每行一个 RSS URL(含 Cookie 则用 "|" 分隔) + _max_episode: int = 2 # 最大集号 + _whitelist: str = "1080p,2160p,4K,HEVC,H.265" + _blacklist: str = "720p,CAM,HDTS" + _save_path: str = "" # 下载保存路径(留空 MP 默认) + + # ───────────────────────────────────────────────────────────────────── + + def init_plugin(self, config: dict = None) -> None: + self._downloadchain = DownloadChain() + self.stop_service() + + if config: + self._enabled = config.get("enabled", False) + self._onlyonce = config.get("onlyonce", False) + self._notify = config.get("notify", False) + self._cron = config.get("cron", "*/30 * * * *") or "*/30 * * * *" + self._rss_urls = config.get("rss_urls", "") + self._max_episode = int(config.get("max_episode", 2)) + self._whitelist = config.get("whitelist", "") + self._blacklist = config.get("blacklist", "") + self._save_path = config.get("save_path", "") + + # 数据目录 + self._history_path = self.get_data_path() / "history.json" + + if self._onlyonce: + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + logger.info("[首播试看] 立即运行一次") + self._scheduler.add_job( + func=self._check_all_feeds, + trigger="date", + run_date=datetime.datetime.now(tz=pytz.timezone(settings.TZ)) + + datetime.timedelta(seconds=3), + ) + if self._scheduler.get_jobs(): + self._scheduler.start() + + # 关闭一次性开关并保存 + self._onlyonce = False + self.__update_config() + + def get_state(self) -> bool: + return self._enabled + + def get_command(self) -> List[Dict[str, Any]]: + return [ + { + "cmd": "/tvfirst_check", + "event": EventType.PluginAction, + "desc": "首播试看立即检查", + "category": "订阅", + "data": {"action": "check_feeds"}, + } + ] + + def get_api(self) -> List[Dict[str, Any]]: + return [ + { + "path": "/clear_history", + "endpoint": self._clear_history, + "methods": ["GET"], + "summary": "清空首播试看下载历史", + } + ] + + def get_service(self) -> List[Dict[str, Any]]: + """注册定时任务。""" + if self._enabled and self._cron: + return [ + { + "id": "TvFirstWatch", + "name": "首播试看轮询", + "trigger": CronTrigger.from_crontab(self._cron), + "func": self._check_all_feeds, + "kwargs": {}, + } + ] + return [] + + @eventmanager.register(EventType.PluginAction) + def _plugin_action(self, event): + """处理远程命令。""" + if not self._enabled: + return + event_data = event.event_data + if not event_data or event_data.get("action") != "check_feeds": + return + logger.info("[首播试看] 收到远程命令,立即执行检查") + self._check_all_feeds() + + def stop_service(self) -> None: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + + # ─── 配置页面 ───────────────────────────────────────────────────────── + + def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: + return [ + { + "component": "VForm", + "content": [ + # 第一行:开关 + { + "component": "VRow", + "content": [ + _col(4, _switch("enabled", "启用插件")), + _col(4, _switch("notify", "下载时通知")), + _col(4, _switch("onlyonce", "立即运行一次")), + ], + }, + # 第二行:执行周期 + 最大集号 + { + "component": "VRow", + "content": [ + _col( + 8, + _textfield( + "cron", + "执行周期(Cron)", + placeholder="5位cron,如 */30 * * * *", + ), + ), + _col( + 4, + _textfield( + "max_episode", + "最大集号", + placeholder="默认 2(即下载 EP01-02)", + ), + ), + ], + }, + # 第三行:RSS 地址 + { + "component": "VRow", + "content": [ + _col( + 12, + { + "component": "VTextarea", + "props": { + "model": "rss_urls", + "label": "RSS 地址", + "rows": 4, + "placeholder": ( + "每行一个地址,格式:\n" + "https://site/rss?passkey=xxx\n" + "# 需要Cookie:https://site/rss|Cookie: uid=1; pass=abc" + ), + }, + }, + ), + ], + }, + # 第四行:白名单 / 黑名单 + { + "component": "VRow", + "content": [ + _col( + 6, + _textfield( + "whitelist", + "白名单关键字(逗号分隔)", + placeholder="1080p,4K,HEVC", + ), + ), + _col( + 6, + _textfield( + "blacklist", + "黑名单关键字(逗号分隔)", + placeholder="720p,CAM", + ), + ), + ], + }, + # 第五行:保存路径 + { + "component": "VRow", + "content": [ + _col( + 12, + _textfield( + "save_path", + "下载保存路径(留空使用 MP 默认)", + placeholder="/downloads/TV", + ), + ), + ], + }, + # 说明 + { + "component": "VRow", + "content": [ + _col( + 12, + { + "component": "VAlert", + "props": { + "type": "info", + "variant": "tonal", + "text": ( + "仅下载识别到集号 ≤ 最大集号 的电视剧资源," + "下载记录保存在插件数据目录 history.json," + "点击「清空历史」API 可重置。" + ), + }, + }, + ), + ], + }, + ], + } + ], { + "enabled": False, + "notify": False, + "onlyonce": False, + "cron": "*/30 * * * *", + "rss_urls": "", + "max_episode": 2, + "whitelist": "1080p,2160p,4K,HEVC,H.265", + "blacklist": "720p,CAM,HDTS", + "save_path": "", + } + + def get_page(self) -> List[dict]: + """详情页:展示最近下载记录。""" + history = self._load_history() + if not history: + return [ + { + "component": "div", + "props": {"class": "text-center pa-4"}, + "content": [{"component": "p", "text": "暂无下载记录"}], + } + ] + + rows = [] + for key, meta in sorted( + history.items(), key=lambda x: x[1].get("added_at", ""), reverse=True + ): + rows.append( + { + "component": "tr", + "content": [ + {"component": "td", "text": meta.get("series_name", key)}, + {"component": "td", "text": str(meta.get("episode", ""))}, + {"component": "td", "text": meta.get("source", "")}, + {"component": "td", "text": meta.get("added_at", "")}, + ], + } + ) + + return [ + { + "component": "VTable", + "props": {"hover": True}, + "content": [ + { + "component": "thead", + "content": [ + { + "component": "tr", + "content": [ + {"component": "th", "text": "剧名"}, + {"component": "th", "text": "集号"}, + {"component": "th", "text": "来源"}, + {"component": "th", "text": "下载时间"}, + ], + } + ], + }, + {"component": "tbody", "content": rows}, + ], + } + ] + + # ─── 核心逻辑 ───────────────────────────────────────────────────────── + + def _check_all_feeds(self) -> None: + """轮询所有 RSS 源(定时任务入口)。""" + if not self._rss_urls: + logger.warning("[首播试看] 未配置任何 RSS 地址,跳过。") + return + + lines = [l.strip() for l in self._rss_urls.splitlines() if l.strip()] + for line in lines: + try: + self._process_feed(line) + except Exception as exc: + logger.error("[首播试看] 处理 RSS 源出错: %s — %s", line, exc) + + def _parse_feed_line(self, line: str) -> Tuple[str, dict]: + """ + 解析 RSS 行格式: + URL + URL|Cookie: xxx + """ + parts = line.split("|", 1) + url = parts[0].strip() + headers = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/123.0.0.0 Safari/537.36" + ) + } + if len(parts) == 2: + cookie_part = parts[1].strip() + # 支持 "Cookie: xxx" 或直接是 cookie 字符串 + if cookie_part.lower().startswith("cookie:"): + headers["Cookie"] = cookie_part[7:].strip() + else: + headers["Cookie"] = cookie_part + return url, headers + + def _process_feed(self, line: str) -> None: + url, headers = self._parse_feed_line(line) + source = urlparse(url).netloc + logger.info("[首播试看] 抓取 RSS: %s", url) + + try: + resp = requests.get(url, headers=headers, timeout=30) + resp.raise_for_status() + except requests.RequestException as exc: + logger.error("[首播试看] RSS 请求失败 [%s]: %s", source, exc) + return + + parsed = feedparser.parse(resp.text) + if not parsed.entries: + logger.warning("[首播试看] RSS 无条目 [%s]", source) + return + + logger.info("[首播试看] [%s] 解析到 %d 个条目", source, len(parsed.entries)) + for entry in parsed.entries: + try: + self._process_entry(entry, source) + except Exception as exc: + logger.exception( + "[首播试看] 处理条目异常 [%s]: %s", entry.get("title", ""), exc + ) + + def _process_entry(self, entry, source: str) -> None: + title = entry.get("title", "") + if not title: + return + + # 1. 是否为电视剧 + if not self._is_tv(entry): + logger.debug("[首播试看][跳过-非TV] %s", title) + return + + # 2. 提取集数 + episodes = _extract_episodes(title) + if not episodes: + logger.debug("[首播试看][跳过-无集数] %s", title) + return + + # 3. 集数范围 + eps_in_range = [ep for ep in episodes if ep <= self._max_episode] + if not eps_in_range: + logger.info( + "[首播试看][跳过-超限] %s | 识别集号=%s 限制≤%d", + title, + episodes, + self._max_episode, + ) + return + + # 4. 关键字过滤 + ok, reason = self._keyword_filter(title) + if not ok: + logger.info("[首播试看][跳过-关键字] %s | %s", title, reason) + return + + # 5. 去重检查 + series_name = _guess_series_name(title) + with lock: + history = self._load_history() + new_eps = [ + ep + for ep in eps_in_range + if not self._is_downloaded(history, series_name, ep) + ] + if not new_eps: + logger.info( + "[首播试看][跳过-已下载] %s | 剧名=%s | 集号=%s", + title, + series_name, + eps_in_range, + ) + return + + # 6. 触发下载 + logger.info( + "[首播试看][下载] %s | 剧名=%s | 集号=%s | 来源=%s", + title, + series_name, + new_eps, + source, + ) + success = self._do_download(entry, title, series_name) + + if success: + now = datetime.datetime.now().isoformat(timespec="seconds") + for ep in new_eps: + key = _make_key(series_name, ep) + history[key] = { + "series_name": series_name, + "episode": ep, + "title": title, + "source": source, + "added_at": now, + } + self._save_history(history) + + if self._notify: + self.systemmessage.put( + f"📺 首播试看已推送下载\n" + f"剧名:{series_name}\n" + f"集号:{new_eps}\n" + f"标题:{title}" + ) + + def _do_download(self, entry, title: str, series_name: str) -> bool: + """通过 MoviePilot DownloadChain 触发下载。""" + # 构造 TorrentInfo + torrent_url = "" + for enc in getattr(entry, "enclosures", []): + if enc.get("type", "").startswith("application/"): + torrent_url = enc.get("href", "") + break + if not torrent_url: + torrent_url = entry.get("link", "") + + if not torrent_url: + logger.error("[首播试看] 条目缺少种子 URL: %s", title) + return False + + meta = MetaInfo(title=title) + mediainfo = MediaInfo() + mediainfo.type = MediaType.TV + mediainfo.title = series_name + + torrent = TorrentInfo( + title=title, + enclosure=torrent_url, + page_url=entry.get("link", ""), + ) + + context = Context( + meta_info=meta, + media_info=mediainfo, + torrent_info=torrent, + ) + + try: + did, msg = self._downloadchain.download_single( + context=context, + torrent_file=None, + save_path=self._save_path or None, + ) + if did: + logger.info("[首播试看] ✅ DownloadChain 推送成功: %s", title) + return True + else: + logger.error( + "[首播试看] ❌ DownloadChain 推送失败 [%s]: %s", title, msg + ) + return False + except Exception as exc: + logger.error("[首播试看] ❌ 下载异常 [%s]: %s", title, exc) + return False + + # ─── 辅助:关键字过滤 ───────────────────────────────────────────────── + + def _keyword_filter(self, title: str) -> Tuple[bool, str]: + title_lower = title.lower() + for kw in [k.strip() for k in self._blacklist.split(",") if k.strip()]: + if kw.lower() in title_lower: + return False, f"命中黑名单「{kw}」" + wl = [k.strip() for k in self._whitelist.split(",") if k.strip()] + if wl and not any(k.lower() in title_lower for k in wl): + return False, f"未命中白名单 {wl}" + return True, "" + + @staticmethod + def _is_tv(entry) -> bool: + cat = "" + if hasattr(entry, "tags") and entry.tags: + cat = entry.tags[0].get("term", "").lower() + elif hasattr(entry, "category"): + cat = (entry.category or "").lower() + + tv_cats = ("tv", "series", "drama", "television", "综艺", "剧集", "连续剧") + movie_kw = ("movie", "film", "电影", "纪录片") + if any(k in cat for k in tv_cats): + return True + if any(k in cat for k in movie_kw): + return False + return bool(TV_HINTS.search(entry.get("title", ""))) + + # ─── 历史记录操作 ───────────────────────────────────────────────────── + + def _load_history(self) -> dict: + if self._history_path and self._history_path.exists(): + try: + return json.loads(self._history_path.read_text(encoding="utf-8")) + except Exception: + pass + return {} + + def _save_history(self, history: dict) -> None: + if self._history_path: + self._history_path.write_text( + json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8" + ) + + @staticmethod + def _is_downloaded(history: dict, series_name: str, episode: int) -> bool: + return _make_key(series_name, episode) in history + + def _clear_history(self, token: str = "") -> dict: + """API:清空下载历史。""" + if token != settings.API_TOKEN: + return {"success": False, "message": "认证失败"} + self._save_history({}) + logger.info("[首播试看] 下载历史已清空。") + return {"success": True, "message": "历史已清空"} + + def __update_config(self) -> None: + self.update_config( + { + "enabled": self._enabled, + "onlyonce": self._onlyonce, + "notify": self._notify, + "cron": self._cron, + "rss_urls": self._rss_urls, + "max_episode": self._max_episode, + "whitelist": self._whitelist, + "blacklist": self._blacklist, + "save_path": self._save_path, + } + ) + + +# ─── 模块级工具函数 ─────────────────────────────────────────────────────── + + +def _extract_episodes(title: str) -> List[int]: + found: set[int] = set() + for pat in EPISODE_PATTERNS: + for m in pat.finditer(title): + try: + ep = int(m.group(1)) + if 0 < ep < 1000: + found.add(ep) + except (IndexError, ValueError): + pass + return sorted(found) + + +def _guess_series_name(title: str) -> str: + name = re.split( + r"[\s._\-]*(?:[Ss]\d{1,2}[Ee]\d{1,3}|[Ee][Pp]?\d{1,3}|第\d+集|\d+of\d+|[\[(【]\d+[】\])])", + title, + maxsplit=1, + )[0] + name = re.sub(r"^\s*\[.*?\]\s*", "", name) + name = re.sub(r"\b(19|20)\d{2}\b", "", name) + name = re.sub(r"[\s._\-]+", " ", name).strip(" .-_") + return name or title + + +def _make_key(series_name: str, episode: int) -> str: + norm = re.sub(r"\W+", "", series_name.lower()) + return f"{norm}__ep{episode:03d}" + + +# ─── Vuetify 组件快捷函数 ───────────────────────────────────────────────── + + +def _col(md: int, *children) -> dict: + return { + "component": "VCol", + "props": {"cols": 12, "md": md}, + "content": list(children), + } + + +def _switch(model: str, label: str) -> dict: + return { + "component": "VSwitch", + "props": {"model": model, "label": label}, + } + + +def _textfield(model: str, label: str, placeholder: str = "") -> dict: + props: dict = {"model": model, "label": label} + if placeholder: + props["placeholder"] = placeholder + return {"component": "VTextField", "props": props} diff --git a/plugins.v2/tvfirstwatch/requirements.txt b/plugins.v2/tvfirstwatch/requirements.txt new file mode 100644 index 0000000..3244d21 --- /dev/null +++ b/plugins.v2/tvfirstwatch/requirements.txt @@ -0,0 +1 @@ +feedparser>=6.0.0 \ No newline at end of file From 6a03f626be6704f824ae2d836dc4511affebc50a Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sat, 14 Mar 2026 18:40:39 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 635 +------------------------------------ plugins.v2/package.v2.json | 14 - 2 files changed, 8 insertions(+), 641 deletions(-) delete mode 100644 plugins.v2/package.v2.json diff --git a/package.v2.json b/package.v2.json index eccdbea..455d7ba 100644 --- a/package.v2.json +++ b/package.v2.json @@ -1,633 +1,14 @@ { - "SiteStatistic": { - "name": "站点数据统计", - "description": "站点统计数据图表。", - "labels": "站点,仪表板", - "version": "1.9", - "icon": "statistic.png", - "author": "lightolly,jxxghp", - "level": 2, - "history": { - "v1.9": "过滤未启用的站点数据", - "v1.8": "修复站点数据增量处理逻辑", - "v1.7.1": "优化内存占用", - "v1.6": "优化了站点数据获取失败时的回退逻辑", - "v1.5": "修复了发送增量通知失败等一些问题", - "v1.4.1": "支持数据刷新时发送消息通知", - "v1.3": "远程刷新命令移植到主程序", - "v1.2": "继续修复增量数据统计问题", - "v1.1": "修复增量数据统计问题", - "v1.0": "MoviePilot V2 版本站点数据统计插件" - } - }, - "BrushFlow": { - "name": "站点刷流", - "description": "自动托管刷流,将会提高对应站点的访问频率。", - "labels": "刷流,仪表板", - "version": "4.3.5", - "icon": "brush.jpg", - "author": "jxxghp,InfinityPacer,Seed680", - "level": 2, - "history": { - "v4.3.5": "提升匹配规则时的健壮性", - "v4.3.4": "添加RSS支持配置选项", - "v4.3.2": "增加'删除促销结束的未完成下载'功能", - "v4.3.1": "修复了一些细节问题", - "v4.3": "支持带宽采样并计算平均值,以优化刷流效率", - "v4.2": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v4.1": "支持通过CRON表达式配置开启时间,固定10分钟为执行周期", - "v4.0": "站点独立配置项支持配置NexusPHP 站点自动跳过下载提示页", - "v3.9": "MoviePilot V2 版本站点刷流插件" - } - }, - "AutoSignIn": { - "name": "站点自动签到", - "description": "自动模拟登录、签到站点。", - "labels": "站点", - "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!", - "v2.5.4": "增加保号风险提示", - "v2.5.3": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v2.5.2": "修复HDArea签到", - "v2.5.1": "修复空签到失败问题", - "v2.5": "MoviePilot V2 版本站点自动签到插件" - } - }, - "DownloadSiteTag": { - "name": "下载任务分类与标签", - "description": "自动给下载任务分类与打站点标签、剧集名称标签", - "labels": "下载管理", - "version": "2.6", - "icon": "Youtube-dl_B.png", - "author": "叮叮当", - "level": 1, - "history": { - "v2.6": "增加站点/剧名前缀功能", - "v2.5": "优化采用公共服务自动清理未使用标签", - "v2.4": "增加自动清理未使用标签", - "v2.3": "增加tracker映射配置", - "v2.2": "MoviePilot V2 版本下载任务分类与标签插件" - } - }, - "MediaServerRefresh": { - "name": "媒体库服务器刷新", - "description": "入库后自动刷新Emby/Jellyfin/Plex服务器海报墙。", - "labels": "媒体库", - "version": "1.3.3", - "icon": "refresh2.png", - "author": "jxxghp", - "level": 1, - "history": { - "v1.3.3": "优化延迟刷新", - "v1.3.2": "适配飞牛媒体库", - "v1.3.1": "修复兼容性问题", - "v1.3": "MoviePilot V2 版本媒体库服务器刷新插件" - } - }, - "MediaServerMsg": { - "name": "媒体库服务器通知", - "description": "发送Emby/Jellyfin/Plex服务器的播放、入库等通知消息。", - "labels": "消息通知,媒体库", - "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时,会尝试从媒体服务器中获取", - "v1.7.1": "未获取到tmdb信息则按原有逻辑发送;电影显示海报", - "v1.7": "对TV剧集入库事件进行聚合,避免消息轰炸。更新后如果打不开插件,请重置插件", - "v1.6": "查询剧集图片兼容没有季集信息的情况", - "v1.5": "支持独立控制媒体服务器通知", - "v1.4": "MoviePilot V2 版本媒体库服务器通知插件" - } - }, - "ChatGPT": { - "name": "ChatGPT", - "description": "消息交互支持与ChatGPT对话。", - "labels": "消息通知,识别", - "version": "2.1.8", - "icon": "Chatgpt_A.png", - "author": "jxxghp", - "level": 1, - "history": { - "v2.1.8": "修复 OpenAI API >=1.0.0 兼容性问题", - "v2.1.7":"独立安装OpenAi SDK依赖", - "v2.1.6": "支持自定义辅助识别提示词", - "v2.1.5": "兼容一些模型返回json数据信息用markdown语法包裹的情况", - "v2.1.4": "不处理http链接", - "v2.1.3": "修复通知异常", - "v2.1.2": "支持传入多个api key", - "v2.1.1": "兼容/v1后仍有路径的接口", - "v2.1.0": "优化辅助识别提示词", - "v2.0.1": "修复辅助识别", - "v2.0": "适配MoviePilot V2 版本,采用链式事件机制" - } - }, - "TorrentTransfer": { - "name": "自动转移做种", - "description": "定期转移下载器中的做种任务到另一个下载器。", - "labels": "做种", - "version": "1.10.2", - "icon": "seed.png", - "author": "jxxghp", - "level": 2, - "history": { - "v1.10.2": "增加保留原标签和原分类的选项", - "v1.10.1": "优化“立即运行一次”按钮位置", - "v1.10": "支持跳过校验(仅支持 qBittorrent)", - "v1.9": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v1.8": "支持qbittorrent 5", - "v1.7": "MoviePilot V2 版本自动转移做种插件", - "v1.7.1": "修复兼容性问题" - } - }, - "RssSubscribe": { - "name": "自定义订阅", - "description": "定时刷新RSS报文,识别内容后添加订阅或直接下载。", - "labels": "订阅", - "version": "2.1", - "icon": "rss.png", - "author": "jxxghp", - "level": 2, - "history": { - "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v2.0": "兼容MoviePilot V2 版本" - } - }, - "FFmpegThumb": { - "name": "FFmpeg缩略图", - "description": "TheMovieDb没有背景图片时使用FFmpeg截取视频文件缩略图", - "labels": "刮削", - "version": "2.1", - "icon": "ffmpeg.png", - "author": "jxxghp", - "level": 1, - "history": { - "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v2.0": "兼容MoviePilot V2 版本" - } - }, - "LibraryScraper": { - "name": "媒体库刮削", - "description": "定时对媒体库进行刮削,补齐缺失元数据和图片。", - "labels": "刮削", - "version": "2.1.1", - "icon": "scraper.png", - "author": "jxxghp", - "level": 1, - "history": { - "v2.1.1": "调整目录计算方法,以支持更多重命名格式", - "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v2.0": "兼容MoviePilot V2 版本", - "v1.5": "修复未获取fanart图片的问题", - "v1.4.1": "修复nfo文件读取失败时任务中断问题" - } - }, - "PersonMeta": { - "name": "演职人员刮削", - "description": "刮削演职人员图片以及中文名称。", - "labels": "媒体库,刮削", - "version": "2.2.2", - "icon": "actor.png", - "author": "jxxghp", - "level": 1, - "history": { - "v2.2.2": "修复异常日志问题", - "v2.2.1": "优化错误数据兼容处理", - "v2.2": "修改使用自定义图片域名时无法下载图片的问题", - "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v2.0": "兼容MoviePilot V2 版本", - "v1.4": "人物图片调整为优先从TMDB获取,避免douban图片CDN加载过慢的问题", - "v1.3": "修复v1.8.5版本后刮削报错问题" - } - }, - "SpeedLimiter": { - "name": "播放限速", - "description": "外网播放媒体库视频时,自动对下载器进行限速。", - "labels": "网络", - "version": "2.1", - "icon": "Librespeed_A.png", - "author": "Shurelol", - "level": 1, - "history": { - "v2.1": "修复表单参数", - "v2.0": "兼容MoviePilot V2 版本", - "v1.2": "增加不限速路径配置,以应对网盘直链播放的情况" - } - }, - "AutoClean": { - "name": "定时清理媒体库", - "description": "定时清理用户下载的种子、源文件、媒体库文件。", - "labels": "媒体库", - "version": "2.2", - "icon": "clean.png", - "author": "thsrite", - "level": 2, - "history": { - "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v2.0": "兼容MoviePilot V2 版本", - "v2.2": "fix" - } - }, - "TorrentRemover": { - "name": "自动删种", - "description": "自动删除下载器中的下载任务。", - "labels": "做种", - "version": "2.2", - "icon": "delete.jpg", - "author": "jxxghp", - "level": 2, - "history": { - "v2.2": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v2.1.1": "修复兼容MoviePilot V2 版本", - "v2.0": "兼容MoviePilot V2 版本" - } - }, - "IYUUAutoSeed": { - "name": "IYUU自动辅种", - "description": "基于IYUU官方Api实现自动辅种。", - "labels": "做种,IYUU", - "version": "2.15", - "icon": "IYUU.png", - "author": "jxxghp,CKun", - "level": 2, - "history": { - "v2.15": "修复海豹不能辅种的问题", - "v2.14": "修复馒头不能辅种的问题", - "v2.13": "开启跳过校验后需手动开启自动开始", - "v2.12": "增加qb下载器分类复用配置", - "v2.11": "修复qb跳过校验不自动开始的问题", - "v2.10": "Revert 辅种结束后,一起开始所有辅种后暂停的种子(排除了出错的种子)", - "v2.9": "修复开启跳过校验后,Tr下载器不自动开始的问题", - "v2.8": "为配置主辅分离时,不走辅种下载器检查", - "v2.7": "增加主辅分离配置,单独指定辅种下载器", - "v2.6": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v2.5": "修复qb辅种结束后自动开始暂停的种子", - "v2.4": "辅种结束后,一起开始所有辅种后暂停的种子(排除了出错的种子)", - "v2.3": "支持qbittorrent 5", - "v2.2": "修复种子校验服务未生效", - "v2.1": "调整IYUU最新域名", - "v2.0": "兼容MoviePilot V2 版本" - } - }, - "CrossSeed": { - "name": "青蛙辅种助手", - "description": "参考ReseedPuppy和IYUU辅种插件实现自动辅种,支持站点:青蛙、AGSVPT、麒麟、UBits、聆音、憨憨等。", - "labels": "做种", - "version": "3.0.1", - "icon": "qingwa.png", - "author": "233@qingwa", - "level": 2, - "history": { - "v3.0.1": "遗漏了一个私有属性", - "v3.0": "兼容MoviePilot V2 版本" - } - }, - "QbCommand": { - "name": "QB远程操作", - "description": "通过定时任务或交互命令远程操作QB暂停/开始/限速等。", - "labels": "下载管理,Qbittorrent", - "version": "2.1", - "icon": "Qbittorrent_A.png", - "author": "DzAvril", - "level": 1, - "history": { - "v2.1": "支持qbittorrent 5", - "v2.0": "适配MoviePilot V2 版本" - } - }, - "HistoryToV2": { - "name": "历史记录迁移", - "description": "将MoviePilot V1版本的整理历史记录迁移至V2版本。", - "labels": "整理,历史记录", - "version": "1.1", - "icon": "Moviepilot_A.png", - "author": "jxxghp", - "level": 1, - "history": { - "v1.1": "修复启动提示信息" - } - }, - "SyncCookieCloud": { - "name": "同步CookieCloud", - "description": "同步MoviePilot站点Cookie到本地CookieCloud。", - "labels": "站点", - "version": "2.2", - "icon": "Cookiecloud_A.png", - "author": "thsrite", - "level": 1, - "history": { - "v2.2": "优化执行周期输入,需要MoviePilot v2.2.1+", - "v2.1": "兼容MoviePilot V2" - } - }, - "ChineseSubFinder": { - "name": "ChineseSubFinder", - "description": "整理入库时通知ChineseSubFinder下载字幕。", - "labels": "字幕", - "version": "2.0", - "icon": "chinesesubfinder.png", - "author": "jxxghp", - "level": 1, - "history": { - "v2.0": "兼容MoviePilot V2" - } - }, - "CleanInvalidSeed": { - "name": "清理QB无效做种", - "description": "清理已经被站点删除的种子及对应源文件,仅支持QB", - "labels": "Qbittorrent", - "version": "2.0", - "icon": "clean_a.png", - "author": "DzAvril", - "level": 1, - "history": { - "v2.0": "适配 MoviePilot V2" - } - }, - "PlayletCategory": { - "name": "短剧自动分类", - "description": "网络短剧自动整理到独立的分类目录。", - "labels": "文件整理", - "version": "2.1", - "icon": "Amule_A.png", - "author": "jxxghp,longqiuyu", - "level": 1, - "history": { - "v2.1": "兼容MoviePilot V2", - "v2.0": "适配新的目录结构变化,短剧分类名称调整为配置目录路径,升级后需要重新调整设置后才能使用。" - } - }, - "MultiClass": { - "name": "视频多级分类", - "description": "支持视频多级分类", - "labels": "文件整理", - "version": "0.1", - "icon": "Calibreweb_B.png", - "author": "liuhangbin", - "level": 1, - "history": { - "v0.1": "视频多级分类插件, 目前仅支持电影按评分,年代,系列分类。" - } - }, - "MoviePilotUpdateNotify": { - "name": "MoviePilot更新推送", - "description": "MoviePilot推送release更新通知、自动重启。", - "labels": "消息通知,自动更新", - "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+", - "v2.0": "兼容MoviePilot V2" - } - }, - "DoubanRank": { - "name": "豆瓣榜单订阅", - "description": "监控豆瓣热门榜单,自动添加订阅。", - "labels": "订阅", - "version": "2.0.1", - "icon": "movie.jpg", - "author": "jxxghp", - "level": 2, - "history": { - "v2.0.1": "优化douban_id匹配和类型匹配", - "v2.0.0": "优化cron表达式输入" - } - }, - "DoubanSync": { - "name": "豆瓣想看", - "description": "同步豆瓣想看数据,自动添加订阅。", - "labels": "订阅", - "version": "2.1.0", - "icon": "douban.png", - "author": "jxxghp,dwhmofly", - "level": 2, - "history": { - "v2.1.0": "新增配置项-搜索下载,开启后会优先搜索站点资源进行下载,下载不到才会添加订阅", - "v2.0.1": "支持将豆瓣ID转换为MoviePilot中已有用户(在用户个人信息中绑定豆瓣ID),需要MoviePilot v2.2.6+", - "v2.0.0": "优化cron表达式输入" - } - }, - "TvdbDiscover": { - "name": "TheTVDB探索", - "description": "让探索支持TheTVDB的数据浏览。", - "labels": "探索", - "version": "1.1", - "icon": "TheTVDB_A.png", - "author": "jxxghp", - "level": 1, - "history": { - "v1.1": "需要MoviePilot v2.2.7-1+ 版本,否则无法显示图片" - } - }, - "SubscribeClear": { - "name": "订阅种子清理", - "description": "删除指定下载信息。", - "labels": "下载管理", + "TvFirstWatch": { + "name": "首播试看", + "description": "定时抓取RSS,只下载电视剧前N集(首播试看),防重复推送。", + "labels": "订阅,RSS", "version": "1.0", - "icon": "Moviepilot_A.jpg", - "author": "k0ala", - "level": 1, - "history": { - "v1.0": "支持清理QB中已下载的订阅文件" - } - }, - "ToBypassTrackers": { - "name": "绕过Trackers", - "description": "提供tracker服务器IP地址列表,帮助IPv6连接绕过OpenClash。", - "labels": "工具", - "version": "1.5.3", - "icon": "Clash_A.png", - "author": "wumode", + "icon": "rss.png", + "author": "Raymond38324", "level": 2, "history": { - "v1.5.3": "修复 Rousi 种子获取问题", - "v1.5.2": "支持从站点首页获取最新 Trackers", - "v1.5.1": "新增 Tracker", - "v1.5.0": "新增 Page 界面; 支持通过`/check_ip` 命令检查IP; 改进 UI", - "v1.4.3": "修复 bug", - "v1.4.2": "修复插件动作", - "v1.4.1": "修复通知类型错误", - "v1.4": "异步查询DNS", - "v1.3": "新增一些Trackers", - "v1.2": "修复Trackers加载错误", - "v1.1": "更新列表后发送通知", - "v1.0": "支持自定义Trackers" - } - }, - "ImdbSource": { - "name": "IMDb源", - "description": "让探索,推荐和媒体识别支持IMDb数据源。", - "labels": "探索", - "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": "为元数据增加背景图", - "v1.6.3": "优化媒体识别速度; 适配 Pydantic V2 (主程序版本需高于 2.8.1-1)", - "v1.6.2": "修复 API 查询错误重试问题", - "v1.6.1": "添加中文主屏幕组件; 修复 bug", - "v1.5.8": "修改UA", - "v1.5.7": "改进异常处理", - "v1.5.6": "固定仪表盘组件海报比例; 修复 bug", - "v1.5.5": "修复初始化错误", - "v1.5.4": "改进媒体识别", - "v1.5.3": "异步执行; 修复 bugs (主程序版本需要高于 2.6.8)", - "v1.5.2": "修复一些bugs", - "v1.5.1": "改进媒体id转换; 支持二级分类和自定义推荐", - "v1.5.0": "支持媒体识别", - "v1.4.4": "更新数据源", - "v1.4.3": "为仪表盘组件添加缓存", - "v1.4.2": "优化小屏幕组件显示", - "v1.4.1": "优化亮色主题显示", - "v1.4.0": "添加仪表盘组件: IMDb 编辑精选", - "v1.3.3": "修复依赖问题", - "v1.3.2": "更新 API query hash", - "v1.3.1": "修复按日期排序错误", - "v1.3": "优化网络连接", - "v1.2": "推荐热门纪录片", - "v1.1": "推荐支持IMDB数据源; 优化海报尺寸,减少卡顿", - "v1.0": "探索支持IMDb数据源" - } - }, - "ClashRuleProvider": { - "name": "Clash Rule Provider", - "description": "随时为Clash添加一些额外的规则。", - "labels": "工具", - "version": "2.1.3", - "icon": "Mihomo_Meta_A.png", - "author": "wumode", - "level": 1, - "release": true, - "history": { - "v2.1.3": "修复代理删除问题", - "v2.1.2": "修复规则集序列化错误", - "v2.1.1": "增强数据管理功能", - "v2.0.10": "适配 MoviePilot 2.8.4", - "v2.0.9": "适配 Pydantic V2 (主程序版本需高于 2.8.1-1)", - "v2.0.8": "修复已知问题", - "v2.0.7": "修复子规则比较错误", - "v2.0.6": "修复已知问题; 改进对代理组的配置和验证", - "v2.0.5": "完善了对嵌套逻辑规则和子规则的配置和验证", - "v2.0.4": "修复已知问题; 使用异步调度器; 显示规则更改日期", - "v2.0.3": "修复已知问题", - "v2.0.2": "修复分享链接转换问题", - "v2.0.1": "支持独立的订阅链接配置, 覆写代理组和出站代理; 优化数据结构; 修复分享链接解析问题", - "v1.4.2": "优化移动端 UI; 支持显示节点链接", - "v1.4.1": "修复配置模板保存错误, 请重新配置Clash模板", - "v1.4.0": "优化 UI; 支持连接多个 Clash Dashboards", - "v1.3.3": "通过emoji识别国家; 按国家分组节点; mrs格式支持", - "v1.3.2": "注册插件动作", - "v1.3.1": "支持配置 Hosts", - "v1.2.8": "改进导入界面", - "v1.2.7": "修复分享链接解析错误", - "v1.2.6": "修复代理组修改丢失问题", - "v1.2.4": "支持geo规则补全; 代理组编辑", - "v1.2.3": "修复规则集名称错误", - "v1.2.2": "展示更多信息; 修复交互问题", - "v1.2.1": "修复配置模板错误", - "v1.2.0": "支持管理多个订阅; 支持导入配置模板和 V2Ray 链接; 优化界面", - "v1.1.3": "添加仪表盘组件", - "v1.1.1": "支持解析 V2ray 订阅", - "v1.1.0": "支持规则集合; 添加ACL4SSR规则集; 配置说明", - "v1.0.1": "支持规则搜索, 优化细节", - "v1.0.0": "支持: 规则分页; 导入规则; 代理组; 附加出站代理; 按区域分组", - "v0.1.0": "新增ClashRuleProvider" - } - }, - "LexiAnnot": { - "name": "美剧生词标注", - "description": "根据CEFR等级,为英语影视剧标注高级词汇。", - "labels": "英语", - "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", - "v1.1.4": "优化字幕选择决策", - "v1.1.3": "适配 Pydantic V2 (主程序版本需高于 2.8.1-1)", - "v1.1.2": "使用子进程避免 spaCy 模型常驻内存", - "v1.1.1": "添加任务页面; 改进 spaCy 模型加载逻辑", - "v1.1.0": "支持考试词汇标注; 优化分词处理; 修复错误", - "v1.0.1": "合并连字符词; 避免ARM平台依赖问题", - "v1.0": "新增LexiAnnot" - } - }, - "MeoWMsg": { - "name": "MeoW消息通知", - "description": "支持使用MeoW发送消息通知。", - "labels": "消息通知", - "version": "1.0.1", - "icon": "MeoW_A.png", - "author": "Licardo", - "level": 2, - "history": { - "v1.0.0": "首个版本,新增MeoW消息通知", - "v1.0.1": "优化代码,修复运行一次按钮没办法自动关闭的问题" - } - }, - "BugReporter": { - "name": "Bug反馈", - "description": "自动上报异常,协助开发者发现和解决问题。", - "labels": "开发", - "version": "1.3", - "icon": "Alist_encrypt_A.png", - "author": "jxxghp", - "level": 1, - "history": { - "v1.3": "减少网络异常信息上送", - "v1.2": "优化上报信息量", - "v1.1": "加强脱敏处理" - } - }, - "TmdbWallpaper": { - "name": "登录壁纸本地化", - "description": "将MoviePilot的登录壁纸下载到本地。", - "labels": "壁纸,本地化", - "version": "1.4.2", - "icon": "Macos_Sierra.png", - "author": "jxxghp", - "level": 1, - "history": { - "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": "首次发布:可配置报告模块、历史记录页面、下载器通用化、存储路径可配置" + "v1.0": "首次发布:支持RSS订阅电视剧首播试看" } } -} +} \ No newline at end of file diff --git a/plugins.v2/package.v2.json b/plugins.v2/package.v2.json deleted file mode 100644 index 455d7ba..0000000 --- a/plugins.v2/package.v2.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "TvFirstWatch": { - "name": "首播试看", - "description": "定时抓取RSS,只下载电视剧前N集(首播试看),防重复推送。", - "labels": "订阅,RSS", - "version": "1.0", - "icon": "rss.png", - "author": "Raymond38324", - "level": 2, - "history": { - "v1.0": "首次发布:支持RSS订阅电视剧首播试看" - } - } -} \ No newline at end of file From f1355f3400375748d8a47370a31c6cb0ba31ee42 Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sat, 14 Mar 2026 18:47:37 +0800 Subject: [PATCH 03/11] =?UTF-8?q?bug=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins.v2/tvfirstwatch/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins.v2/tvfirstwatch/__init__.py b/plugins.v2/tvfirstwatch/__init__.py index 9086ac0..8385db7 100644 --- a/plugins.v2/tvfirstwatch/__init__.py +++ b/plugins.v2/tvfirstwatch/__init__.py @@ -19,7 +19,7 @@ from app.core.context import Context, MediaInfo, TorrentInfo from app.core.metainfo import MetaInfo from app.log import logger from app.plugins import _PluginBase -from app.helper.event import eventmanager +from app.core.event import eventmanager from app.schemas.types import MediaType, EventType lock = Lock() From 2f78083c7fe1876525e28858ebec2866b1f450b6 Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sat, 14 Mar 2026 20:38:41 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E5=8D=A0=E7=94=A8=E7=A9=BA=E9=97=B4=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins.v2/package.v2.json | 16 ++ plugins.v2/tvfirstwatch/__init__.py | 234 +++++++++++++++++++--------- 2 files changed, 175 insertions(+), 75 deletions(-) create mode 100644 plugins.v2/package.v2.json diff --git a/plugins.v2/package.v2.json b/plugins.v2/package.v2.json new file mode 100644 index 0000000..54de119 --- /dev/null +++ b/plugins.v2/package.v2.json @@ -0,0 +1,16 @@ +{ + "TvFirstWatch": { + "name": "首播试看", + "description": "定时抓取RSS,只下载电视剧前N集(首播试看),支持空间限制和预估大小。", + "labels": "订阅,RSS", + "version": "1.2", + "icon": "rss.png", + "author": "Raymond38324", + "level": 2, + "history": { + "v1.2": "新增预估大小配置,当RSS无种子大小时使用", + "v1.1": "新增空间限制功能,下载前检查剩余空间", + "v1.0": "首次发布:支持RSS订阅电视剧首播试看" + } + } +} \ No newline at end of file diff --git a/plugins.v2/tvfirstwatch/__init__.py b/plugins.v2/tvfirstwatch/__init__.py index 8385db7..c9abb26 100644 --- a/plugins.v2/tvfirstwatch/__init__.py +++ b/plugins.v2/tvfirstwatch/__init__.py @@ -24,8 +24,6 @@ from app.schemas.types import MediaType, EventType lock = Lock() -# ─── 集数正则(与独立脚本保持一致) ───────────────────────────────────── - EPISODE_PATTERNS = [ re.compile(r"[Ss]\d{1,2}[Ee](\d{1,3})", re.IGNORECASE), re.compile(r"\bEP?\.?(\d{1,3})\b", re.IGNORECASE), @@ -41,41 +39,31 @@ TV_HINTS = re.compile( class TvFirstWatch(_PluginBase): - """ - 电视剧首播试看自动下载 - - 定时抓取多个 RSS 源,仅下载电视剧前 N 集(默认 1-2 集), - 通过 MoviePilot DownloadChain 触发下载(支持洗版、刮削)。 - """ - - # ── 插件元信息 ────────────────────────────────────────────────────── plugin_name = "首播试看" plugin_desc = "定时抓取 RSS,只下载剧集前 N 集(首播试看),防重复推送。" plugin_icon = "rss.png" - plugin_version = "1.0" + plugin_version = "1.2" plugin_author = "Raymond38324" author_url = "https://github.com/Raymond38324" plugin_config_prefix = "tvfirstwatch_" plugin_order = 25 auth_level = 2 - # ── 私有变量 ───────────────────────────────────────────────────────── _scheduler: Optional[BackgroundScheduler] = None _downloadchain: Optional[DownloadChain] = None _history_path: Optional[Path] = None - # ── 配置属性 ───────────────────────────────────────────────────────── _enabled: bool = False _onlyonce: bool = False _notify: bool = False - _cron: str = "*/30 * * * *" # cron 轮询周期 - _rss_urls: str = "" # 每行一个 RSS URL(含 Cookie 则用 "|" 分隔) - _max_episode: int = 2 # 最大集号 + _cron: str = "*/30 * * * *" + _rss_urls: str = "" + _max_episode: int = 2 _whitelist: str = "1080p,2160p,4K,HEVC,H.265" _blacklist: str = "720p,CAM,HDTS" - _save_path: str = "" # 下载保存路径(留空 MP 默认) - - # ───────────────────────────────────────────────────────────────────── + _save_path: str = "" + _max_storage_gb: int = 0 + _default_size_gb: float = 2.0 def init_plugin(self, config: dict = None) -> None: self._downloadchain = DownloadChain() @@ -91,8 +79,9 @@ class TvFirstWatch(_PluginBase): self._whitelist = config.get("whitelist", "") self._blacklist = config.get("blacklist", "") self._save_path = config.get("save_path", "") + self._max_storage_gb = int(config.get("max_storage_gb", 0)) + self._default_size_gb = float(config.get("default_size_gb", 2.0)) - # 数据目录 self._history_path = self.get_data_path() / "history.json" if self._onlyonce: @@ -107,7 +96,6 @@ class TvFirstWatch(_PluginBase): if self._scheduler.get_jobs(): self._scheduler.start() - # 关闭一次性开关并保存 self._onlyonce = False self.__update_config() @@ -132,11 +120,16 @@ class TvFirstWatch(_PluginBase): "endpoint": self._clear_history, "methods": ["GET"], "summary": "清空首播试看下载历史", - } + }, + { + "path": "/storage_status", + "endpoint": self._storage_status, + "methods": ["GET"], + "summary": "获取存储空间使用情况", + }, ] def get_service(self) -> List[Dict[str, Any]]: - """注册定时任务。""" if self._enabled and self._cron: return [ { @@ -151,7 +144,6 @@ class TvFirstWatch(_PluginBase): @eventmanager.register(EventType.PluginAction) def _plugin_action(self, event): - """处理远程命令。""" if not self._enabled: return event_data = event.event_data @@ -167,14 +159,11 @@ class TvFirstWatch(_PluginBase): self._scheduler.shutdown() self._scheduler = None - # ─── 配置页面 ───────────────────────────────────────────────────────── - def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: return [ { "component": "VForm", "content": [ - # 第一行:开关 { "component": "VRow", "content": [ @@ -183,29 +172,43 @@ class TvFirstWatch(_PluginBase): _col(4, _switch("onlyonce", "立即运行一次")), ], }, - # 第二行:执行周期 + 最大集号 { "component": "VRow", "content": [ _col( - 8, + 4, _textfield( "cron", "执行周期(Cron)", - placeholder="5位cron,如 */30 * * * *", + placeholder="*/30 * * * *", ), ), _col( - 4, + 2, _textfield( "max_episode", "最大集号", - placeholder="默认 2(即下载 EP01-02)", + placeholder="默认 2", + ), + ), + _col( + 3, + _textfield( + "max_storage_gb", + "空间上限(GB)", + placeholder="0=不限制", + ), + ), + _col( + 3, + _textfield( + "default_size_gb", + "预估大小(GB)", + placeholder="RSS无大小默认值", ), ), ], }, - # 第三行:RSS 地址 { "component": "VRow", "content": [ @@ -227,7 +230,6 @@ class TvFirstWatch(_PluginBase): ), ], }, - # 第四行:白名单 / 黑名单 { "component": "VRow", "content": [ @@ -249,7 +251,6 @@ class TvFirstWatch(_PluginBase): ), ], }, - # 第五行:保存路径 { "component": "VRow", "content": [ @@ -263,7 +264,6 @@ class TvFirstWatch(_PluginBase): ), ], }, - # 说明 { "component": "VRow", "content": [ @@ -275,9 +275,9 @@ class TvFirstWatch(_PluginBase): "type": "info", "variant": "tonal", "text": ( - "仅下载识别到集号 ≤ 最大集号 的电视剧资源," - "下载记录保存在插件数据目录 history.json," - "点击「清空历史」API 可重置。" + "仅下载集号 ≤ 最大集号的电视剧。" + "空间上限:设置后超出将停止下载,0表示不限制。" + "预估大小:当RSS不包含种子大小时使用此值计算空间。" ), }, }, @@ -296,17 +296,34 @@ class TvFirstWatch(_PluginBase): "whitelist": "1080p,2160p,4K,HEVC,H.265", "blacklist": "720p,CAM,HDTS", "save_path": "", + "max_storage_gb": 0, + "default_size_gb": 2.0, } def get_page(self) -> List[dict]: - """详情页:展示最近下载记录。""" history = self._load_history() + total_bytes = self._calculate_total_size(history) + total_gb = total_bytes / (1024**3) + max_gb = self._max_storage_gb + usage_percent = (total_gb / max_gb * 100) if max_gb > 0 else 0 + if not history: return [ { "component": "div", "props": {"class": "text-center pa-4"}, - "content": [{"component": "p", "text": "暂无下载记录"}], + "content": [ + {"component": "p", "text": "暂无下载记录"}, + { + "component": "p", + "text": f"已用空间: {total_gb:.2f} GB" + + ( + f" / {max_gb} GB ({usage_percent:.1f}%)" + if max_gb > 0 + else "" + ), + }, + ], } ] @@ -314,12 +331,14 @@ class TvFirstWatch(_PluginBase): for key, meta in sorted( history.items(), key=lambda x: x[1].get("added_at", ""), reverse=True ): + size_str = meta.get("size_str", "-") rows.append( { "component": "tr", "content": [ {"component": "td", "text": meta.get("series_name", key)}, {"component": "td", "text": str(meta.get("episode", ""))}, + {"component": "td", "text": size_str}, {"component": "td", "text": meta.get("source", "")}, {"component": "td", "text": meta.get("added_at", "")}, ], @@ -327,6 +346,22 @@ class TvFirstWatch(_PluginBase): ) return [ + { + "component": "div", + "props": {"class": "mb-4 pa-2"}, + "content": [ + { + "component": "p", + "props": {"class": "text-h6"}, + "text": f"已用空间: {total_gb:.2f} GB" + + ( + f" / {max_gb} GB ({usage_percent:.1f}%)" + if max_gb > 0 + else "" + ), + }, + ], + }, { "component": "VTable", "props": {"hover": True}, @@ -339,6 +374,7 @@ class TvFirstWatch(_PluginBase): "content": [ {"component": "th", "text": "剧名"}, {"component": "th", "text": "集号"}, + {"component": "th", "text": "大小"}, {"component": "th", "text": "来源"}, {"component": "th", "text": "下载时间"}, ], @@ -347,13 +383,10 @@ class TvFirstWatch(_PluginBase): }, {"component": "tbody", "content": rows}, ], - } + }, ] - # ─── 核心逻辑 ───────────────────────────────────────────────────────── - def _check_all_feeds(self) -> None: - """轮询所有 RSS 源(定时任务入口)。""" if not self._rss_urls: logger.warning("[首播试看] 未配置任何 RSS 地址,跳过。") return @@ -366,11 +399,6 @@ class TvFirstWatch(_PluginBase): logger.error("[首播试看] 处理 RSS 源出错: %s — %s", line, exc) def _parse_feed_line(self, line: str) -> Tuple[str, dict]: - """ - 解析 RSS 行格式: - URL - URL|Cookie: xxx - """ parts = line.split("|", 1) url = parts[0].strip() headers = { @@ -382,7 +410,6 @@ class TvFirstWatch(_PluginBase): } if len(parts) == 2: cookie_part = parts[1].strip() - # 支持 "Cookie: xxx" 或直接是 cookie 字符串 if cookie_part.lower().startswith("cookie:"): headers["Cookie"] = cookie_part[7:].strip() else: @@ -420,18 +447,15 @@ class TvFirstWatch(_PluginBase): if not title: return - # 1. 是否为电视剧 if not self._is_tv(entry): logger.debug("[首播试看][跳过-非TV] %s", title) return - # 2. 提取集数 episodes = _extract_episodes(title) if not episodes: logger.debug("[首播试看][跳过-无集数] %s", title) return - # 3. 集数范围 eps_in_range = [ep for ep in episodes if ep <= self._max_episode] if not eps_in_range: logger.info( @@ -442,13 +466,11 @@ class TvFirstWatch(_PluginBase): ) return - # 4. 关键字过滤 ok, reason = self._keyword_filter(title) if not ok: logger.info("[首播试看][跳过-关键字] %s | %s", title, reason) return - # 5. 去重检查 series_name = _guess_series_name(title) with lock: history = self._load_history() @@ -466,18 +488,35 @@ class TvFirstWatch(_PluginBase): ) return - # 6. 触发下载 + torrent_size, is_estimated = self._get_torrent_size(entry) + size_label = "预估" if is_estimated else "实际" + if self._max_storage_gb > 0: + current_total = self._calculate_total_size(history) + max_bytes = self._max_storage_gb * (1024**3) + if current_total + torrent_size > max_bytes: + logger.warning( + "[首播试看][跳过-空间不足] 已用 %.2f GB + 新增 %.2f GB(%s) > 上限 %d GB", + current_total / (1024**3), + torrent_size / (1024**3), + size_label, + self._max_storage_gb, + ) + return + logger.info( - "[首播试看][下载] %s | 剧名=%s | 集号=%s | 来源=%s", + "[首播试看][下载] %s | 剧名=%s | 集号=%s | 大小=%.2f GB(%s) | 来源=%s", title, series_name, new_eps, + torrent_size / (1024**3), + size_label, source, ) success = self._do_download(entry, title, series_name) if success: now = datetime.datetime.now().isoformat(timespec="seconds") + size_str = self._format_size(torrent_size, is_estimated) for ep in new_eps: key = _make_key(series_name, ep) history[key] = { @@ -486,6 +525,9 @@ class TvFirstWatch(_PluginBase): "title": title, "source": source, "added_at": now, + "size": torrent_size, + "size_str": size_str, + "is_estimated": is_estimated, } self._save_history(history) @@ -494,12 +536,50 @@ class TvFirstWatch(_PluginBase): f"📺 首播试看已推送下载\n" f"剧名:{series_name}\n" f"集号:{new_eps}\n" + f"大小:{size_str}\n" f"标题:{title}" ) + def _get_torrent_size(self, entry) -> Tuple[int, bool]: + """ + 获取种子大小。 + 返回: (大小字节数, 是否为预估大小) + """ + try: + if hasattr(entry, "enclosures") and entry.enclosures: + for enc in entry.enclosures: + length = enc.get("length") + if length: + return int(length), False + if hasattr(entry, "content_length"): + return int(entry.content_length), False + except Exception: + pass + default_bytes = int(self._default_size_gb * (1024**3)) + return default_bytes, True + + @staticmethod + def _format_size(size_bytes: int, is_estimated: bool = False) -> str: + label = "(预估)" if is_estimated else "" + if size_bytes == 0: + return "未知" + label + elif size_bytes < 1024: + return f"{size_bytes} B{label}" + elif size_bytes < 1024**2: + return f"{size_bytes / 1024:.1f} KB{label}" + elif size_bytes < 1024**3: + return f"{size_bytes / (1024**2):.1f} MB{label}" + else: + return f"{size_bytes / (1024**3):.2f} GB{label}" + + @staticmethod + def _calculate_total_size(history: dict) -> int: + total = 0 + for meta in history.values(): + total += meta.get("size", 0) + return total + def _do_download(self, entry, title: str, series_name: str) -> bool: - """通过 MoviePilot DownloadChain 触发下载。""" - # 构造 TorrentInfo torrent_url = "" for enc in getattr(entry, "enclosures", []): if enc.get("type", "").startswith("application/"): @@ -530,7 +610,7 @@ class TvFirstWatch(_PluginBase): ) try: - did, msg = self._downloadchain.download_single( + did = self._downloadchain.download_single( context=context, torrent_file=None, save_path=self._save_path or None, @@ -539,16 +619,12 @@ class TvFirstWatch(_PluginBase): logger.info("[首播试看] ✅ DownloadChain 推送成功: %s", title) return True else: - logger.error( - "[首播试看] ❌ DownloadChain 推送失败 [%s]: %s", title, msg - ) + logger.error("[首播试看] ❌ DownloadChain 推送失败 [%s]", title) return False except Exception as exc: logger.error("[首播试看] ❌ 下载异常 [%s]: %s", title, exc) return False - # ─── 辅助:关键字过滤 ───────────────────────────────────────────────── - def _keyword_filter(self, title: str) -> Tuple[bool, str]: title_lower = title.lower() for kw in [k.strip() for k in self._blacklist.split(",") if k.strip()]: @@ -575,8 +651,6 @@ class TvFirstWatch(_PluginBase): return False return bool(TV_HINTS.search(entry.get("title", ""))) - # ─── 历史记录操作 ───────────────────────────────────────────────────── - def _load_history(self) -> dict: if self._history_path and self._history_path.exists(): try: @@ -596,13 +670,27 @@ class TvFirstWatch(_PluginBase): return _make_key(series_name, episode) in history def _clear_history(self, token: str = "") -> dict: - """API:清空下载历史。""" if token != settings.API_TOKEN: return {"success": False, "message": "认证失败"} self._save_history({}) logger.info("[首播试看] 下载历史已清空。") return {"success": True, "message": "历史已清空"} + def _storage_status(self, token: str = "") -> dict: + if token != settings.API_TOKEN: + return {"success": False, "message": "认证失败"} + history = self._load_history() + total_bytes = self._calculate_total_size(history) + total_gb = total_bytes / (1024**3) + count = len(history) + return { + "success": True, + "total_bytes": total_bytes, + "total_gb": round(total_gb, 2), + "max_gb": self._max_storage_gb, + "count": count, + } + def __update_config(self) -> None: self.update_config( { @@ -615,13 +703,12 @@ class TvFirstWatch(_PluginBase): "whitelist": self._whitelist, "blacklist": self._blacklist, "save_path": self._save_path, + "max_storage_gb": self._max_storage_gb, + "default_size_gb": self._default_size_gb, } ) -# ─── 模块级工具函数 ─────────────────────────────────────────────────────── - - def _extract_episodes(title: str) -> List[int]: found: set[int] = set() for pat in EPISODE_PATTERNS: @@ -652,9 +739,6 @@ def _make_key(series_name: str, episode: int) -> str: return f"{norm}__ep{episode:03d}" -# ─── Vuetify 组件快捷函数 ───────────────────────────────────────────────── - - def _col(md: int, *children) -> dict: return { "component": "VCol", From 4d2bc309ac0a61ec46c479c61ebdab495f541f29 Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sat, 14 Mar 2026 21:00:27 +0800 Subject: [PATCH 05/11] =?UTF-8?q?bug=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins.v2/tvfirstwatch/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins.v2/tvfirstwatch/__init__.py b/plugins.v2/tvfirstwatch/__init__.py index c9abb26..e849501 100644 --- a/plugins.v2/tvfirstwatch/__init__.py +++ b/plugins.v2/tvfirstwatch/__init__.py @@ -438,7 +438,7 @@ class TvFirstWatch(_PluginBase): try: self._process_entry(entry, source) except Exception as exc: - logger.exception( + logger.error( "[首播试看] 处理条目异常 [%s]: %s", entry.get("title", ""), exc ) From be12618b0fe89355a08e7268fc7e6ac9d20277eb Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sat, 14 Mar 2026 21:01:21 +0800 Subject: [PATCH 06/11] =?UTF-8?q?bug=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 6 ++++-- plugins.v2/package.v2.json | 16 ---------------- 2 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 plugins.v2/package.v2.json diff --git a/package.v2.json b/package.v2.json index 455d7ba..54de119 100644 --- a/package.v2.json +++ b/package.v2.json @@ -1,13 +1,15 @@ { "TvFirstWatch": { "name": "首播试看", - "description": "定时抓取RSS,只下载电视剧前N集(首播试看),防重复推送。", + "description": "定时抓取RSS,只下载电视剧前N集(首播试看),支持空间限制和预估大小。", "labels": "订阅,RSS", - "version": "1.0", + "version": "1.2", "icon": "rss.png", "author": "Raymond38324", "level": 2, "history": { + "v1.2": "新增预估大小配置,当RSS无种子大小时使用", + "v1.1": "新增空间限制功能,下载前检查剩余空间", "v1.0": "首次发布:支持RSS订阅电视剧首播试看" } } diff --git a/plugins.v2/package.v2.json b/plugins.v2/package.v2.json deleted file mode 100644 index 54de119..0000000 --- a/plugins.v2/package.v2.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "TvFirstWatch": { - "name": "首播试看", - "description": "定时抓取RSS,只下载电视剧前N集(首播试看),支持空间限制和预估大小。", - "labels": "订阅,RSS", - "version": "1.2", - "icon": "rss.png", - "author": "Raymond38324", - "level": 2, - "history": { - "v1.2": "新增预估大小配置,当RSS无种子大小时使用", - "v1.1": "新增空间限制功能,下载前检查剩余空间", - "v1.0": "首次发布:支持RSS订阅电视剧首播试看" - } - } -} \ No newline at end of file From 2a4002032db1d751169ff9f0583b1dba4a608dcb Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sat, 14 Mar 2026 22:56:18 +0800 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B8=85=E7=A9=BA=E5=8E=86=E5=8F=B2=E6=8C=89?= =?UTF-8?q?=E9=92=AE=EF=BC=8C=E5=8F=AF=E9=87=8D=E7=BD=AE=E7=A9=BA=E9=97=B4?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 9 +- plugins.v2/tvfirstwatch/__init__.py | 152 +++++++++++++++++++--------- 2 files changed, 112 insertions(+), 49 deletions(-) diff --git a/package.v2.json b/package.v2.json index 54de119..41bcc91 100644 --- a/package.v2.json +++ b/package.v2.json @@ -1,16 +1,19 @@ { "TvFirstWatch": { "name": "首播试看", - "description": "定时抓取RSS,只下载电视剧前N集(首播试看),支持空间限制和预估大小。", + "description": "定时抓取RSS,只下载电视剧前N集,自动跳过合集和过大文件。", "labels": "订阅,RSS", - "version": "1.2", + "version": "1.5", "icon": "rss.png", "author": "Raymond38324", "level": 2, "history": { + "v1.5": "详情页新增清空历史按钮,可重置空间统计", + "v1.4": "新增Complete/合集检测,新增单集大小上限过滤", + "v1.3": "修复媒体信息识别,下载前自动获取TMDB信息", "v1.2": "新增预估大小配置,当RSS无种子大小时使用", "v1.1": "新增空间限制功能,下载前检查剩余空间", - "v1.0": "首次发布:支持RSS订阅电视剧首播试看" + "v1.0": "首次发布" } } } \ No newline at end of file diff --git a/plugins.v2/tvfirstwatch/__init__.py b/plugins.v2/tvfirstwatch/__init__.py index e849501..f82133c 100644 --- a/plugins.v2/tvfirstwatch/__init__.py +++ b/plugins.v2/tvfirstwatch/__init__.py @@ -37,12 +37,17 @@ TV_HINTS = re.compile( re.IGNORECASE, ) +COMPLETE_HINTS = re.compile( + r"\b(Complete|全集|全季|Season\s*\d+\s*Complete|S\d+\s*Complete)\b", + re.IGNORECASE, +) + class TvFirstWatch(_PluginBase): plugin_name = "首播试看" plugin_desc = "定时抓取 RSS,只下载剧集前 N 集(首播试看),防重复推送。" plugin_icon = "rss.png" - plugin_version = "1.2" + plugin_version = "1.5" plugin_author = "Raymond38324" author_url = "https://github.com/Raymond38324" plugin_config_prefix = "tvfirstwatch_" @@ -64,6 +69,7 @@ class TvFirstWatch(_PluginBase): _save_path: str = "" _max_storage_gb: int = 0 _default_size_gb: float = 2.0 + _max_single_size_gb: float = 10.0 def init_plugin(self, config: dict = None) -> None: self._downloadchain = DownloadChain() @@ -81,6 +87,7 @@ class TvFirstWatch(_PluginBase): self._save_path = config.get("save_path", "") self._max_storage_gb = int(config.get("max_storage_gb", 0)) self._default_size_gb = float(config.get("default_size_gb", 2.0)) + self._max_single_size_gb = float(config.get("max_single_size_gb", 10.0)) self._history_path = self.get_data_path() / "history.json" @@ -201,12 +208,33 @@ class TvFirstWatch(_PluginBase): ), _col( 3, + _textfield( + "max_single_size_gb", + "单集上限(GB)", + placeholder="超过跳过", + ), + ), + ], + }, + { + "component": "VRow", + "content": [ + _col( + 6, _textfield( "default_size_gb", "预估大小(GB)", placeholder="RSS无大小默认值", ), ), + _col( + 6, + _textfield( + "save_path", + "下载保存路径", + placeholder="留空使用MP默认", + ), + ), ], }, { @@ -251,19 +279,6 @@ class TvFirstWatch(_PluginBase): ), ], }, - { - "component": "VRow", - "content": [ - _col( - 12, - _textfield( - "save_path", - "下载保存路径(留空使用 MP 默认)", - placeholder="/downloads/TV", - ), - ), - ], - }, { "component": "VRow", "content": [ @@ -275,9 +290,9 @@ class TvFirstWatch(_PluginBase): "type": "info", "variant": "tonal", "text": ( - "仅下载集号 ≤ 最大集号的电视剧。" - "空间上限:设置后超出将停止下载,0表示不限制。" - "预估大小:当RSS不包含种子大小时使用此值计算空间。" + "仅下载集号≤最大集号的电视剧,自动跳过Complete/全集。" + "单集上限:超过此大小的种子将跳过(防合集)。" + "空间上限:超出将停止下载,0表示不限制。" ), }, }, @@ -298,6 +313,7 @@ class TvFirstWatch(_PluginBase): "save_path": "", "max_storage_gb": 0, "default_size_gb": 2.0, + "max_single_size_gb": 10.0, } def get_page(self) -> List[dict]: @@ -306,22 +322,54 @@ class TvFirstWatch(_PluginBase): total_gb = total_bytes / (1024**3) max_gb = self._max_storage_gb usage_percent = (total_gb / max_gb * 100) if max_gb > 0 else 0 + count = len(history) + + header_content = [ + { + "component": "div", + "props": {"class": "d-flex justify-space-between align-center"}, + "content": [ + { + "component": "p", + "props": {"class": "text-h6 mb-0"}, + "text": f"已用空间: {total_gb:.2f} GB" + + ( + f" / {max_gb} GB ({usage_percent:.1f}%)" + if max_gb > 0 + else f" | 共 {count} 条记录" + ), + }, + { + "component": "VBtn", + "props": { + "color": "error", + "variant": "outlined", + "size": "small", + }, + "text": "清空历史", + "events": { + "click": { + "api": "plugin/TvFirstWatch/clear_history", + "method": "get", + "params": {"token": settings.API_TOKEN}, + } + }, + }, + ], + } + ] if not history: return [ { "component": "div", - "props": {"class": "text-center pa-4"}, - "content": [ - {"component": "p", "text": "暂无下载记录"}, + "props": {"class": "pa-4"}, + "content": header_content + + [ { "component": "p", - "text": f"已用空间: {total_gb:.2f} GB" - + ( - f" / {max_gb} GB ({usage_percent:.1f}%)" - if max_gb > 0 - else "" - ), + "text": "暂无下载记录", + "props": {"class": "text-center mt-4"}, }, ], } @@ -348,19 +396,8 @@ class TvFirstWatch(_PluginBase): return [ { "component": "div", - "props": {"class": "mb-4 pa-2"}, - "content": [ - { - "component": "p", - "props": {"class": "text-h6"}, - "text": f"已用空间: {total_gb:.2f} GB" - + ( - f" / {max_gb} GB ({usage_percent:.1f}%)" - if max_gb > 0 - else "" - ), - }, - ], + "props": {"class": "pa-2"}, + "content": header_content, }, { "component": "VTable", @@ -451,6 +488,10 @@ class TvFirstWatch(_PluginBase): logger.debug("[首播试看][跳过-非TV] %s", title) return + if COMPLETE_HINTS.search(title): + logger.info("[首播试看][跳过-合集] %s | 检测到Complete/全集关键词", title) + return + episodes = _extract_episodes(title) if not episodes: logger.debug("[首播试看][跳过-无集数] %s", title) @@ -472,6 +513,20 @@ class TvFirstWatch(_PluginBase): return series_name = _guess_series_name(title) + + torrent_size, is_estimated = self._get_torrent_size(entry) + size_label = "预估" if is_estimated else "实际" + size_gb = torrent_size / (1024**3) + + if self._max_single_size_gb > 0 and size_gb > self._max_single_size_gb: + logger.info( + "[首播试看][跳过-过大] %s | 大小 %.2f GB > 上限 %.1f GB (可能是合集)", + title, + size_gb, + self._max_single_size_gb, + ) + return + with lock: history = self._load_history() new_eps = [ @@ -488,8 +543,6 @@ class TvFirstWatch(_PluginBase): ) return - torrent_size, is_estimated = self._get_torrent_size(entry) - size_label = "预估" if is_estimated else "实际" if self._max_storage_gb > 0: current_total = self._calculate_total_size(history) max_bytes = self._max_storage_gb * (1024**3) @@ -497,7 +550,7 @@ class TvFirstWatch(_PluginBase): logger.warning( "[首播试看][跳过-空间不足] 已用 %.2f GB + 新增 %.2f GB(%s) > 上限 %d GB", current_total / (1024**3), - torrent_size / (1024**3), + size_gb, size_label, self._max_storage_gb, ) @@ -508,7 +561,7 @@ class TvFirstWatch(_PluginBase): title, series_name, new_eps, - torrent_size / (1024**3), + size_gb, size_label, source, ) @@ -593,9 +646,15 @@ class TvFirstWatch(_PluginBase): return False meta = MetaInfo(title=title) - mediainfo = MediaInfo() - mediainfo.type = MediaType.TV - mediainfo.title = series_name + if not meta.name: + meta.name = series_name + + mediainfo = self.chain.recognize_media(meta=meta) + if not mediainfo: + logger.warning("[首播试看] 未识别到媒体信息,使用基本信息: %s", title) + mediainfo = MediaInfo() + mediainfo.type = MediaType.TV + mediainfo.title = series_name torrent = TorrentInfo( title=title, @@ -705,6 +764,7 @@ class TvFirstWatch(_PluginBase): "save_path": self._save_path, "max_storage_gb": self._max_storage_gb, "default_size_gb": self._default_size_gb, + "max_single_size_gb": self._max_single_size_gb, } ) From 72bb3320ac107c564b73a81650de8290fa45ecd2 Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sat, 14 Mar 2026 23:06:44 +0800 Subject: [PATCH 08/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DNoneType?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=8C=E5=A2=9E=E5=BC=BA=E7=A9=BA=E5=80=BC?= =?UTF-8?q?=E5=A4=84=E7=90=86=20v1.5.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 9 +++++---- plugins.v2/tvfirstwatch/__init__.py | 27 ++++++++++++++++----------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/package.v2.json b/package.v2.json index 41bcc91..35df35b 100644 --- a/package.v2.json +++ b/package.v2.json @@ -3,16 +3,17 @@ "name": "首播试看", "description": "定时抓取RSS,只下载电视剧前N集,自动跳过合集和过大文件。", "labels": "订阅,RSS", - "version": "1.5", + "version": "1.5.1", "icon": "rss.png", "author": "Raymond38324", "level": 2, "history": { - "v1.5": "详情页新增清空历史按钮,可重置空间统计", + "v1.5.1": "修复NoneType错误,增强空值处理", + "v1.5": "详情页新增清空历史按钮", "v1.4": "新增Complete/合集检测,新增单集大小上限过滤", "v1.3": "修复媒体信息识别,下载前自动获取TMDB信息", - "v1.2": "新增预估大小配置,当RSS无种子大小时使用", - "v1.1": "新增空间限制功能,下载前检查剩余空间", + "v1.2": "新增预估大小配置", + "v1.1": "新增空间限制功能", "v1.0": "首次发布" } } diff --git a/plugins.v2/tvfirstwatch/__init__.py b/plugins.v2/tvfirstwatch/__init__.py index f82133c..aa5489d 100644 --- a/plugins.v2/tvfirstwatch/__init__.py +++ b/plugins.v2/tvfirstwatch/__init__.py @@ -47,7 +47,7 @@ class TvFirstWatch(_PluginBase): plugin_name = "首播试看" plugin_desc = "定时抓取 RSS,只下载剧集前 N 集(首播试看),防重复推送。" plugin_icon = "rss.png" - plugin_version = "1.5" + plugin_version = "1.5.1" plugin_author = "Raymond38324" author_url = "https://github.com/Raymond38324" plugin_config_prefix = "tvfirstwatch_" @@ -476,11 +476,11 @@ class TvFirstWatch(_PluginBase): self._process_entry(entry, source) except Exception as exc: logger.error( - "[首播试看] 处理条目异常 [%s]: %s", entry.get("title", ""), exc + "[首播试看] 处理条目异常 [%s]: %s", entry.get("title") or "", exc ) def _process_entry(self, entry, source: str) -> None: - title = entry.get("title", "") + title = entry.get("title") or "" if not title: return @@ -635,11 +635,12 @@ class TvFirstWatch(_PluginBase): def _do_download(self, entry, title: str, series_name: str) -> bool: torrent_url = "" for enc in getattr(entry, "enclosures", []): - if enc.get("type", "").startswith("application/"): - torrent_url = enc.get("href", "") + enc_type = enc.get("type") or "" + if enc_type.startswith("application/"): + torrent_url = enc.get("href") or "" break if not torrent_url: - torrent_url = entry.get("link", "") + torrent_url = entry.get("link") or "" if not torrent_url: logger.error("[首播试看] 条目缺少种子 URL: %s", title) @@ -697,10 +698,14 @@ class TvFirstWatch(_PluginBase): @staticmethod def _is_tv(entry) -> bool: cat = "" - if hasattr(entry, "tags") and entry.tags: - cat = entry.tags[0].get("term", "").lower() - elif hasattr(entry, "category"): - cat = (entry.category or "").lower() + try: + if hasattr(entry, "tags") and entry.tags: + term = entry.tags[0].get("term") or "" + cat = term.lower() + elif hasattr(entry, "category"): + cat = (entry.category or "").lower() + except Exception: + pass tv_cats = ("tv", "series", "drama", "television", "综艺", "剧集", "连续剧") movie_kw = ("movie", "film", "电影", "纪录片") @@ -708,7 +713,7 @@ class TvFirstWatch(_PluginBase): return True if any(k in cat for k in movie_kw): return False - return bool(TV_HINTS.search(entry.get("title", ""))) + return bool(TV_HINTS.search(entry.get("title") or "")) def _load_history(self) -> dict: if self._history_path and self._history_path.exists(): From 8a5b01f58fb729181f062bbddd9b546322e0a36e Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sun, 15 Mar 2026 21:45:12 +0800 Subject: [PATCH 09/11] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 3 +- plugins.v2/tvfirstwatch/__init__.py | 58 +++++++++++++++++++---------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/package.v2.json b/package.v2.json index 35df35b..a7a3a89 100644 --- a/package.v2.json +++ b/package.v2.json @@ -3,11 +3,12 @@ "name": "首播试看", "description": "定时抓取RSS,只下载电视剧前N集,自动跳过合集和过大文件。", "labels": "订阅,RSS", - "version": "1.5.1", + "version": "1.5.2", "icon": "rss.png", "author": "Raymond38324", "level": 2, "history": { + "v1.5.2": "去除是否发送通知的配置", "v1.5.1": "修复NoneType错误,增强空值处理", "v1.5": "详情页新增清空历史按钮", "v1.4": "新增Complete/合集检测,新增单集大小上限过滤", diff --git a/plugins.v2/tvfirstwatch/__init__.py b/plugins.v2/tvfirstwatch/__init__.py index aa5489d..2a74dbb 100644 --- a/plugins.v2/tvfirstwatch/__init__.py +++ b/plugins.v2/tvfirstwatch/__init__.py @@ -60,7 +60,6 @@ class TvFirstWatch(_PluginBase): _enabled: bool = False _onlyonce: bool = False - _notify: bool = False _cron: str = "*/30 * * * *" _rss_urls: str = "" _max_episode: int = 2 @@ -76,18 +75,19 @@ class TvFirstWatch(_PluginBase): self.stop_service() if config: - self._enabled = config.get("enabled", False) - self._onlyonce = config.get("onlyonce", False) - self._notify = config.get("notify", False) + self._enabled = _to_bool(config.get("enabled", False), False) + self._onlyonce = _to_bool(config.get("onlyonce", False), False) self._cron = config.get("cron", "*/30 * * * *") or "*/30 * * * *" self._rss_urls = config.get("rss_urls", "") - self._max_episode = int(config.get("max_episode", 2)) + self._max_episode = _to_int(config.get("max_episode", 2), 2) self._whitelist = config.get("whitelist", "") self._blacklist = config.get("blacklist", "") self._save_path = config.get("save_path", "") - self._max_storage_gb = int(config.get("max_storage_gb", 0)) - self._default_size_gb = float(config.get("default_size_gb", 2.0)) - self._max_single_size_gb = float(config.get("max_single_size_gb", 10.0)) + self._max_storage_gb = _to_int(config.get("max_storage_gb", 0), 0) + self._default_size_gb = _to_float(config.get("default_size_gb", 2.0), 2.0) + self._max_single_size_gb = _to_float( + config.get("max_single_size_gb", 10.0), 10.0 + ) self._history_path = self.get_data_path() / "history.json" @@ -175,7 +175,6 @@ class TvFirstWatch(_PluginBase): "component": "VRow", "content": [ _col(4, _switch("enabled", "启用插件")), - _col(4, _switch("notify", "下载时通知")), _col(4, _switch("onlyonce", "立即运行一次")), ], }, @@ -303,7 +302,6 @@ class TvFirstWatch(_PluginBase): } ], { "enabled": False, - "notify": False, "onlyonce": False, "cron": "*/30 * * * *", "rss_urls": "", @@ -584,15 +582,6 @@ class TvFirstWatch(_PluginBase): } self._save_history(history) - if self._notify: - self.systemmessage.put( - f"📺 首播试看已推送下载\n" - f"剧名:{series_name}\n" - f"集号:{new_eps}\n" - f"大小:{size_str}\n" - f"标题:{title}" - ) - def _get_torrent_size(self, entry) -> Tuple[int, bool]: """ 获取种子大小。 @@ -760,7 +749,6 @@ class TvFirstWatch(_PluginBase): { "enabled": self._enabled, "onlyonce": self._onlyonce, - "notify": self._notify, "cron": self._cron, "rss_urls": self._rss_urls, "max_episode": self._max_episode, @@ -804,6 +792,36 @@ def _make_key(series_name: str, episode: int) -> str: return f"{norm}__ep{episode:03d}" +def _to_bool(value: Any, default: bool = False) -> bool: + if isinstance(value, bool): + return value + if value is None: + return default + if isinstance(value, (int, float)): + return value != 0 + if isinstance(value, str): + v = value.strip().lower() + if v in ("1", "true", "yes", "y", "on"): + return True + if v in ("0", "false", "no", "n", "off", ""): + return False + return default + + +def _to_int(value: Any, default: int) -> int: + try: + return int(value) + except (TypeError, ValueError): + return default + + +def _to_float(value: Any, default: float) -> float: + try: + return float(value) + except (TypeError, ValueError): + return default + + def _col(md: int, *children) -> dict: return { "component": "VCol", From fc23e3639dd820f2210cca62915d9fabc4269413 Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sun, 15 Mar 2026 21:47:29 +0800 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins.v2/tvfirstwatch/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins.v2/tvfirstwatch/__init__.py b/plugins.v2/tvfirstwatch/__init__.py index 2a74dbb..716fad7 100644 --- a/plugins.v2/tvfirstwatch/__init__.py +++ b/plugins.v2/tvfirstwatch/__init__.py @@ -47,7 +47,7 @@ class TvFirstWatch(_PluginBase): plugin_name = "首播试看" plugin_desc = "定时抓取 RSS,只下载剧集前 N 集(首播试看),防重复推送。" plugin_icon = "rss.png" - plugin_version = "1.5.1" + plugin_version = "1.5.2" plugin_author = "Raymond38324" author_url = "https://github.com/Raymond38324" plugin_config_prefix = "tvfirstwatch_" From 750d5917a21ccb0b0e97307c0936440edceb3f6d Mon Sep 17 00:00:00 2001 From: raymond531 Date: Sun, 15 Mar 2026 22:26:21 +0800 Subject: [PATCH 11/11] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 644 +++++++++++++++++++++++++++- plugins.v2/tvfirstwatch/__init__.py | 4 +- 2 files changed, 636 insertions(+), 12 deletions(-) diff --git a/package.v2.json b/package.v2.json index a7a3a89..45c2748 100644 --- a/package.v2.json +++ b/package.v2.json @@ -1,21 +1,645 @@ { + "SiteStatistic": { + "name": "站点数据统计", + "description": "站点统计数据图表。", + "labels": "站点,仪表板", + "version": "1.9", + "icon": "statistic.png", + "author": "lightolly,jxxghp", + "level": 2, + "history": { + "v1.9": "过滤未启用的站点数据", + "v1.8": "修复站点数据增量处理逻辑", + "v1.7.1": "优化内存占用", + "v1.6": "优化了站点数据获取失败时的回退逻辑", + "v1.5": "修复了发送增量通知失败等一些问题", + "v1.4.1": "支持数据刷新时发送消息通知", + "v1.3": "远程刷新命令移植到主程序", + "v1.2": "继续修复增量数据统计问题", + "v1.1": "修复增量数据统计问题", + "v1.0": "MoviePilot V2 版本站点数据统计插件" + } + }, + "BrushFlow": { + "name": "站点刷流", + "description": "自动托管刷流,将会提高对应站点的访问频率。", + "labels": "刷流,仪表板", + "version": "4.3.5", + "icon": "brush.jpg", + "author": "jxxghp,InfinityPacer,Seed680", + "level": 2, + "history": { + "v4.3.5": "提升匹配规则时的健壮性", + "v4.3.4": "添加RSS支持配置选项", + "v4.3.2": "增加'删除促销结束的未完成下载'功能", + "v4.3.1": "修复了一些细节问题", + "v4.3": "支持带宽采样并计算平均值,以优化刷流效率", + "v4.2": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v4.1": "支持通过CRON表达式配置开启时间,固定10分钟为执行周期", + "v4.0": "站点独立配置项支持配置NexusPHP 站点自动跳过下载提示页", + "v3.9": "MoviePilot V2 版本站点刷流插件" + } + }, + "AutoSignIn": { + "name": "站点自动签到", + "description": "自动模拟登录、签到站点。", + "labels": "站点", + "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!", + "v2.5.4": "增加保号风险提示", + "v2.5.3": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v2.5.2": "修复HDArea签到", + "v2.5.1": "修复空签到失败问题", + "v2.5": "MoviePilot V2 版本站点自动签到插件" + } + }, + "DownloadSiteTag": { + "name": "下载任务分类与标签", + "description": "自动给下载任务分类与打站点标签、剧集名称标签", + "labels": "下载管理", + "version": "2.6", + "icon": "Youtube-dl_B.png", + "author": "叮叮当", + "level": 1, + "history": { + "v2.6": "增加站点/剧名前缀功能", + "v2.5": "优化采用公共服务自动清理未使用标签", + "v2.4": "增加自动清理未使用标签", + "v2.3": "增加tracker映射配置", + "v2.2": "MoviePilot V2 版本下载任务分类与标签插件" + } + }, + "MediaServerRefresh": { + "name": "媒体库服务器刷新", + "description": "入库后自动刷新Emby/Jellyfin/Plex服务器海报墙。", + "labels": "媒体库", + "version": "1.3.3", + "icon": "refresh2.png", + "author": "jxxghp", + "level": 1, + "history": { + "v1.3.3": "优化延迟刷新", + "v1.3.2": "适配飞牛媒体库", + "v1.3.1": "修复兼容性问题", + "v1.3": "MoviePilot V2 版本媒体库服务器刷新插件" + } + }, + "MediaServerMsg": { + "name": "媒体库服务器通知", + "description": "发送Emby/Jellyfin/Plex服务器的播放、入库等通知消息。", + "labels": "消息通知,媒体库", + "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时,会尝试从媒体服务器中获取", + "v1.7.1": "未获取到tmdb信息则按原有逻辑发送;电影显示海报", + "v1.7": "对TV剧集入库事件进行聚合,避免消息轰炸。更新后如果打不开插件,请重置插件", + "v1.6": "查询剧集图片兼容没有季集信息的情况", + "v1.5": "支持独立控制媒体服务器通知", + "v1.4": "MoviePilot V2 版本媒体库服务器通知插件" + } + }, + "ChatGPT": { + "name": "ChatGPT", + "description": "消息交互支持与ChatGPT对话。", + "labels": "消息通知,识别", + "version": "2.1.8", + "icon": "Chatgpt_A.png", + "author": "jxxghp", + "level": 1, + "history": { + "v2.1.8": "修复 OpenAI API >=1.0.0 兼容性问题", + "v2.1.7":"独立安装OpenAi SDK依赖", + "v2.1.6": "支持自定义辅助识别提示词", + "v2.1.5": "兼容一些模型返回json数据信息用markdown语法包裹的情况", + "v2.1.4": "不处理http链接", + "v2.1.3": "修复通知异常", + "v2.1.2": "支持传入多个api key", + "v2.1.1": "兼容/v1后仍有路径的接口", + "v2.1.0": "优化辅助识别提示词", + "v2.0.1": "修复辅助识别", + "v2.0": "适配MoviePilot V2 版本,采用链式事件机制" + } + }, + "TorrentTransfer": { + "name": "自动转移做种", + "description": "定期转移下载器中的做种任务到另一个下载器。", + "labels": "做种", + "version": "1.10.2", + "icon": "seed.png", + "author": "jxxghp", + "level": 2, + "history": { + "v1.10.2": "增加保留原标签和原分类的选项", + "v1.10.1": "优化“立即运行一次”按钮位置", + "v1.10": "支持跳过校验(仅支持 qBittorrent)", + "v1.9": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v1.8": "支持qbittorrent 5", + "v1.7": "MoviePilot V2 版本自动转移做种插件", + "v1.7.1": "修复兼容性问题" + } + }, + "RssSubscribe": { + "name": "自定义订阅", + "description": "定时刷新RSS报文,识别内容后添加订阅或直接下载。", + "labels": "订阅", + "version": "2.1", + "icon": "rss.png", + "author": "jxxghp", + "level": 2, + "history": { + "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v2.0": "兼容MoviePilot V2 版本" + } + }, + "FFmpegThumb": { + "name": "FFmpeg缩略图", + "description": "TheMovieDb没有背景图片时使用FFmpeg截取视频文件缩略图", + "labels": "刮削", + "version": "2.1", + "icon": "ffmpeg.png", + "author": "jxxghp", + "level": 1, + "history": { + "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v2.0": "兼容MoviePilot V2 版本" + } + }, + "LibraryScraper": { + "name": "媒体库刮削", + "description": "定时对媒体库进行刮削,补齐缺失元数据和图片。", + "labels": "刮削", + "version": "2.1.1", + "icon": "scraper.png", + "author": "jxxghp", + "level": 1, + "history": { + "v2.1.1": "调整目录计算方法,以支持更多重命名格式", + "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v2.0": "兼容MoviePilot V2 版本", + "v1.5": "修复未获取fanart图片的问题", + "v1.4.1": "修复nfo文件读取失败时任务中断问题" + } + }, + "PersonMeta": { + "name": "演职人员刮削", + "description": "刮削演职人员图片以及中文名称。", + "labels": "媒体库,刮削", + "version": "2.2.2", + "icon": "actor.png", + "author": "jxxghp", + "level": 1, + "history": { + "v2.2.2": "修复异常日志问题", + "v2.2.1": "优化错误数据兼容处理", + "v2.2": "修改使用自定义图片域名时无法下载图片的问题", + "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v2.0": "兼容MoviePilot V2 版本", + "v1.4": "人物图片调整为优先从TMDB获取,避免douban图片CDN加载过慢的问题", + "v1.3": "修复v1.8.5版本后刮削报错问题" + } + }, + "SpeedLimiter": { + "name": "播放限速", + "description": "外网播放媒体库视频时,自动对下载器进行限速。", + "labels": "网络", + "version": "2.1", + "icon": "Librespeed_A.png", + "author": "Shurelol", + "level": 1, + "history": { + "v2.1": "修复表单参数", + "v2.0": "兼容MoviePilot V2 版本", + "v1.2": "增加不限速路径配置,以应对网盘直链播放的情况" + } + }, + "AutoClean": { + "name": "定时清理媒体库", + "description": "定时清理用户下载的种子、源文件、媒体库文件。", + "labels": "媒体库", + "version": "2.2", + "icon": "clean.png", + "author": "thsrite", + "level": 2, + "history": { + "v2.1": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v2.0": "兼容MoviePilot V2 版本", + "v2.2": "fix" + } + }, + "TorrentRemover": { + "name": "自动删种", + "description": "自动删除下载器中的下载任务。", + "labels": "做种", + "version": "2.2", + "icon": "delete.jpg", + "author": "jxxghp", + "level": 2, + "history": { + "v2.2": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v2.1.1": "修复兼容MoviePilot V2 版本", + "v2.0": "兼容MoviePilot V2 版本" + } + }, + "IYUUAutoSeed": { + "name": "IYUU自动辅种", + "description": "基于IYUU官方Api实现自动辅种。", + "labels": "做种,IYUU", + "version": "2.15", + "icon": "IYUU.png", + "author": "jxxghp,CKun", + "level": 2, + "history": { + "v2.15": "修复海豹不能辅种的问题", + "v2.14": "修复馒头不能辅种的问题", + "v2.13": "开启跳过校验后需手动开启自动开始", + "v2.12": "增加qb下载器分类复用配置", + "v2.11": "修复qb跳过校验不自动开始的问题", + "v2.10": "Revert 辅种结束后,一起开始所有辅种后暂停的种子(排除了出错的种子)", + "v2.9": "修复开启跳过校验后,Tr下载器不自动开始的问题", + "v2.8": "为配置主辅分离时,不走辅种下载器检查", + "v2.7": "增加主辅分离配置,单独指定辅种下载器", + "v2.6": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v2.5": "修复qb辅种结束后自动开始暂停的种子", + "v2.4": "辅种结束后,一起开始所有辅种后暂停的种子(排除了出错的种子)", + "v2.3": "支持qbittorrent 5", + "v2.2": "修复种子校验服务未生效", + "v2.1": "调整IYUU最新域名", + "v2.0": "兼容MoviePilot V2 版本" + } + }, + "CrossSeed": { + "name": "青蛙辅种助手", + "description": "参考ReseedPuppy和IYUU辅种插件实现自动辅种,支持站点:青蛙、AGSVPT、麒麟、UBits、聆音、憨憨等。", + "labels": "做种", + "version": "3.0.1", + "icon": "qingwa.png", + "author": "233@qingwa", + "level": 2, + "history": { + "v3.0.1": "遗漏了一个私有属性", + "v3.0": "兼容MoviePilot V2 版本" + } + }, + "QbCommand": { + "name": "QB远程操作", + "description": "通过定时任务或交互命令远程操作QB暂停/开始/限速等。", + "labels": "下载管理,Qbittorrent", + "version": "2.1", + "icon": "Qbittorrent_A.png", + "author": "DzAvril", + "level": 1, + "history": { + "v2.1": "支持qbittorrent 5", + "v2.0": "适配MoviePilot V2 版本" + } + }, + "HistoryToV2": { + "name": "历史记录迁移", + "description": "将MoviePilot V1版本的整理历史记录迁移至V2版本。", + "labels": "整理,历史记录", + "version": "1.1", + "icon": "Moviepilot_A.png", + "author": "jxxghp", + "level": 1, + "history": { + "v1.1": "修复启动提示信息" + } + }, + "SyncCookieCloud": { + "name": "同步CookieCloud", + "description": "同步MoviePilot站点Cookie到本地CookieCloud。", + "labels": "站点", + "version": "2.2", + "icon": "Cookiecloud_A.png", + "author": "thsrite", + "level": 1, + "history": { + "v2.2": "优化执行周期输入,需要MoviePilot v2.2.1+", + "v2.1": "兼容MoviePilot V2" + } + }, + "ChineseSubFinder": { + "name": "ChineseSubFinder", + "description": "整理入库时通知ChineseSubFinder下载字幕。", + "labels": "字幕", + "version": "2.0", + "icon": "chinesesubfinder.png", + "author": "jxxghp", + "level": 1, + "history": { + "v2.0": "兼容MoviePilot V2" + } + }, + "CleanInvalidSeed": { + "name": "清理QB无效做种", + "description": "清理已经被站点删除的种子及对应源文件,仅支持QB", + "labels": "Qbittorrent", + "version": "2.0", + "icon": "clean_a.png", + "author": "DzAvril", + "level": 1, + "history": { + "v2.0": "适配 MoviePilot V2" + } + }, + "PlayletCategory": { + "name": "短剧自动分类", + "description": "网络短剧自动整理到独立的分类目录。", + "labels": "文件整理", + "version": "2.1", + "icon": "Amule_A.png", + "author": "jxxghp,longqiuyu", + "level": 1, + "history": { + "v2.1": "兼容MoviePilot V2", + "v2.0": "适配新的目录结构变化,短剧分类名称调整为配置目录路径,升级后需要重新调整设置后才能使用。" + } + }, + "MultiClass": { + "name": "视频多级分类", + "description": "支持视频多级分类", + "labels": "文件整理", + "version": "0.1", + "icon": "Calibreweb_B.png", + "author": "liuhangbin", + "level": 1, + "history": { + "v0.1": "视频多级分类插件, 目前仅支持电影按评分,年代,系列分类。" + } + }, + "MoviePilotUpdateNotify": { + "name": "MoviePilot更新推送", + "description": "MoviePilot推送release更新通知、自动重启。", + "labels": "消息通知,自动更新", + "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+", + "v2.0": "兼容MoviePilot V2" + } + }, + "DoubanRank": { + "name": "豆瓣榜单订阅", + "description": "监控豆瓣热门榜单,自动添加订阅。", + "labels": "订阅", + "version": "2.0.1", + "icon": "movie.jpg", + "author": "jxxghp", + "level": 2, + "history": { + "v2.0.1": "优化douban_id匹配和类型匹配", + "v2.0.0": "优化cron表达式输入" + } + }, + "DoubanSync": { + "name": "豆瓣想看", + "description": "同步豆瓣想看数据,自动添加订阅。", + "labels": "订阅", + "version": "2.1.0", + "icon": "douban.png", + "author": "jxxghp,dwhmofly", + "level": 2, + "history": { + "v2.1.0": "新增配置项-搜索下载,开启后会优先搜索站点资源进行下载,下载不到才会添加订阅", + "v2.0.1": "支持将豆瓣ID转换为MoviePilot中已有用户(在用户个人信息中绑定豆瓣ID),需要MoviePilot v2.2.6+", + "v2.0.0": "优化cron表达式输入" + } + }, + "TvdbDiscover": { + "name": "TheTVDB探索", + "description": "让探索支持TheTVDB的数据浏览。", + "labels": "探索", + "version": "1.1", + "icon": "TheTVDB_A.png", + "author": "jxxghp", + "level": 1, + "history": { + "v1.1": "需要MoviePilot v2.2.7-1+ 版本,否则无法显示图片" + } + }, + "SubscribeClear": { + "name": "订阅种子清理", + "description": "删除指定下载信息。", + "labels": "下载管理", + "version": "1.0", + "icon": "Moviepilot_A.jpg", + "author": "k0ala", + "level": 1, + "history": { + "v1.0": "支持清理QB中已下载的订阅文件" + } + }, + "ToBypassTrackers": { + "name": "绕过Trackers", + "description": "提供tracker服务器IP地址列表,帮助IPv6连接绕过OpenClash。", + "labels": "工具", + "version": "1.5.3", + "icon": "Clash_A.png", + "author": "wumode", + "level": 2, + "history": { + "v1.5.3": "修复 Rousi 种子获取问题", + "v1.5.2": "支持从站点首页获取最新 Trackers", + "v1.5.1": "新增 Tracker", + "v1.5.0": "新增 Page 界面; 支持通过`/check_ip` 命令检查IP; 改进 UI", + "v1.4.3": "修复 bug", + "v1.4.2": "修复插件动作", + "v1.4.1": "修复通知类型错误", + "v1.4": "异步查询DNS", + "v1.3": "新增一些Trackers", + "v1.2": "修复Trackers加载错误", + "v1.1": "更新列表后发送通知", + "v1.0": "支持自定义Trackers" + } + }, + "ImdbSource": { + "name": "IMDb源", + "description": "让探索,推荐和媒体识别支持IMDb数据源。", + "labels": "探索", + "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": "为元数据增加背景图", + "v1.6.3": "优化媒体识别速度; 适配 Pydantic V2 (主程序版本需高于 2.8.1-1)", + "v1.6.2": "修复 API 查询错误重试问题", + "v1.6.1": "添加中文主屏幕组件; 修复 bug", + "v1.5.8": "修改UA", + "v1.5.7": "改进异常处理", + "v1.5.6": "固定仪表盘组件海报比例; 修复 bug", + "v1.5.5": "修复初始化错误", + "v1.5.4": "改进媒体识别", + "v1.5.3": "异步执行; 修复 bugs (主程序版本需要高于 2.6.8)", + "v1.5.2": "修复一些bugs", + "v1.5.1": "改进媒体id转换; 支持二级分类和自定义推荐", + "v1.5.0": "支持媒体识别", + "v1.4.4": "更新数据源", + "v1.4.3": "为仪表盘组件添加缓存", + "v1.4.2": "优化小屏幕组件显示", + "v1.4.1": "优化亮色主题显示", + "v1.4.0": "添加仪表盘组件: IMDb 编辑精选", + "v1.3.3": "修复依赖问题", + "v1.3.2": "更新 API query hash", + "v1.3.1": "修复按日期排序错误", + "v1.3": "优化网络连接", + "v1.2": "推荐热门纪录片", + "v1.1": "推荐支持IMDB数据源; 优化海报尺寸,减少卡顿", + "v1.0": "探索支持IMDb数据源" + } + }, + "ClashRuleProvider": { + "name": "Clash Rule Provider", + "description": "随时为Clash添加一些额外的规则。", + "labels": "工具", + "version": "2.1.3", + "icon": "Mihomo_Meta_A.png", + "author": "wumode", + "level": 1, + "release": true, + "history": { + "v2.1.3": "修复代理删除问题", + "v2.1.2": "修复规则集序列化错误", + "v2.1.1": "增强数据管理功能", + "v2.0.10": "适配 MoviePilot 2.8.4", + "v2.0.9": "适配 Pydantic V2 (主程序版本需高于 2.8.1-1)", + "v2.0.8": "修复已知问题", + "v2.0.7": "修复子规则比较错误", + "v2.0.6": "修复已知问题; 改进对代理组的配置和验证", + "v2.0.5": "完善了对嵌套逻辑规则和子规则的配置和验证", + "v2.0.4": "修复已知问题; 使用异步调度器; 显示规则更改日期", + "v2.0.3": "修复已知问题", + "v2.0.2": "修复分享链接转换问题", + "v2.0.1": "支持独立的订阅链接配置, 覆写代理组和出站代理; 优化数据结构; 修复分享链接解析问题", + "v1.4.2": "优化移动端 UI; 支持显示节点链接", + "v1.4.1": "修复配置模板保存错误, 请重新配置Clash模板", + "v1.4.0": "优化 UI; 支持连接多个 Clash Dashboards", + "v1.3.3": "通过emoji识别国家; 按国家分组节点; mrs格式支持", + "v1.3.2": "注册插件动作", + "v1.3.1": "支持配置 Hosts", + "v1.2.8": "改进导入界面", + "v1.2.7": "修复分享链接解析错误", + "v1.2.6": "修复代理组修改丢失问题", + "v1.2.4": "支持geo规则补全; 代理组编辑", + "v1.2.3": "修复规则集名称错误", + "v1.2.2": "展示更多信息; 修复交互问题", + "v1.2.1": "修复配置模板错误", + "v1.2.0": "支持管理多个订阅; 支持导入配置模板和 V2Ray 链接; 优化界面", + "v1.1.3": "添加仪表盘组件", + "v1.1.1": "支持解析 V2ray 订阅", + "v1.1.0": "支持规则集合; 添加ACL4SSR规则集; 配置说明", + "v1.0.1": "支持规则搜索, 优化细节", + "v1.0.0": "支持: 规则分页; 导入规则; 代理组; 附加出站代理; 按区域分组", + "v0.1.0": "新增ClashRuleProvider" + } + }, + "LexiAnnot": { + "name": "美剧生词标注", + "description": "根据CEFR等级,为英语影视剧标注高级词汇。", + "labels": "英语", + "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", + "v1.1.4": "优化字幕选择决策", + "v1.1.3": "适配 Pydantic V2 (主程序版本需高于 2.8.1-1)", + "v1.1.2": "使用子进程避免 spaCy 模型常驻内存", + "v1.1.1": "添加任务页面; 改进 spaCy 模型加载逻辑", + "v1.1.0": "支持考试词汇标注; 优化分词处理; 修复错误", + "v1.0.1": "合并连字符词; 避免ARM平台依赖问题", + "v1.0": "新增LexiAnnot" + } + }, + "MeoWMsg": { + "name": "MeoW消息通知", + "description": "支持使用MeoW发送消息通知。", + "labels": "消息通知", + "version": "1.0.1", + "icon": "MeoW_A.png", + "author": "Licardo", + "level": 2, + "history": { + "v1.0.0": "首个版本,新增MeoW消息通知", + "v1.0.1": "优化代码,修复运行一次按钮没办法自动关闭的问题" + } + }, + "BugReporter": { + "name": "Bug反馈", + "description": "自动上报异常,协助开发者发现和解决问题。", + "labels": "开发", + "version": "1.3", + "icon": "Alist_encrypt_A.png", + "author": "jxxghp", + "level": 1, + "history": { + "v1.3": "减少网络异常信息上送", + "v1.2": "优化上报信息量", + "v1.1": "加强脱敏处理" + } + }, + "TmdbWallpaper": { + "name": "登录壁纸本地化", + "description": "将MoviePilot的登录壁纸下载到本地。", + "labels": "壁纸,本地化", + "version": "1.4.2", + "icon": "Macos_Sierra.png", + "author": "jxxghp", + "level": 1, + "history": { + "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": "首次发布:可配置报告模块、历史记录页面、下载器通用化、存储路径可配置" + } + }, "TvFirstWatch": { "name": "首播试看", - "description": "定时抓取RSS,只下载电视剧前N集,自动跳过合集和过大文件。", + "description": "定时抓取RSS,只下载电视剧前N集,自动跳过合集和过大文件。", "labels": "订阅,RSS", - "version": "1.5.2", + "version": "1.0.0", "icon": "rss.png", "author": "Raymond38324", "level": 2, "history": { - "v1.5.2": "去除是否发送通知的配置", - "v1.5.1": "修复NoneType错误,增强空值处理", - "v1.5": "详情页新增清空历史按钮", - "v1.4": "新增Complete/合集检测,新增单集大小上限过滤", - "v1.3": "修复媒体信息识别,下载前自动获取TMDB信息", - "v1.2": "新增预估大小配置", - "v1.1": "新增空间限制功能", - "v1.0": "首次发布" + "v1.0.0": "首次发布" } } } \ No newline at end of file diff --git a/plugins.v2/tvfirstwatch/__init__.py b/plugins.v2/tvfirstwatch/__init__.py index 716fad7..46967dd 100644 --- a/plugins.v2/tvfirstwatch/__init__.py +++ b/plugins.v2/tvfirstwatch/__init__.py @@ -45,9 +45,9 @@ COMPLETE_HINTS = re.compile( class TvFirstWatch(_PluginBase): plugin_name = "首播试看" - plugin_desc = "定时抓取 RSS,只下载剧集前 N 集(首播试看),防重复推送。" + plugin_desc = "定时抓取 RSS, 只下载剧集前 N 集(首播试看),防重复推送。" plugin_icon = "rss.png" - plugin_version = "1.5.2" + plugin_version = "1.0.0" plugin_author = "Raymond38324" author_url = "https://github.com/Raymond38324" plugin_config_prefix = "tvfirstwatch_"