From 6d6de8b22f4e0175feebc05f2da521639cb26d48 Mon Sep 17 00:00:00 2001 From: Attente <19653207+wikrin@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:37:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E4=BB=8EBGM=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E7=9A=84=E6=80=BB=E9=9B=86=E6=95=B0=E4=BC=9A=E8=A2=AB?= =?UTF-8?q?TMDB=E8=A6=86=E7=9B=96=E7=9A=84=E9=97=AE=E9=A2=98,=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=BC=80=E5=85=B3=E9=80=89=E9=A1=B9=E4=BB=A5=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=20=20=20=20=20=20=20=E6=96=B0=E5=A2=9E=E5=BA=95?= =?UTF-8?q?=E9=83=A8=E8=AF=B4=E6=98=8E.=20other:=20=E5=88=86=E7=A6=BB?= =?UTF-8?q?=E7=BB=84=E4=BB=B6,=20=E7=BB=84=E4=BB=B6=E6=94=AF=E6=8C=81HTML?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E7=9A=84=E6=96=87=E6=9C=AC=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 +- plugins/bangumicoll/__init__.py | 314 +++++------------------- plugins/bangumicoll/page_components.py | 318 +++++++++++++++++++++++++ 3 files changed, 386 insertions(+), 254 deletions(-) create mode 100644 plugins/bangumicoll/page_components.py diff --git a/package.json b/package.json index 2e84c46..82e637b 100644 --- a/package.json +++ b/package.json @@ -3,17 +3,15 @@ "name": "Bangumi收藏订阅", "description": "Bangumi用户收藏添加到订阅", "labels": "订阅", - "version": "1.4", + "version": "1.5", "icon": "https://raw.githubusercontent.com/wikrin/MoviePilot-Plugins/main/icons/bangumi_b.png", "author": "Attente", "level": 1, "v2": true, "history": { + "v1.5": "修复总集数会同步TMDB变动的问题,增加开关选项", "v1.4": "结构优化", - "v1.3.1": "修复因修改季号导致未下载剧集而完成订阅的问题", - "v1.3": "添加订阅逻辑优化", - "v1.2.2": "新增: 订阅添加失败总览 修复: 其他方式添加的订阅反复添加的问题", - "v1.2.1": "修复tmdb没有查询到条目导致插件崩溃的问题" + "v1.3.1": "修复因修改季号导致未下载剧集而完成订阅的问题" } } } diff --git a/plugins/bangumicoll/__init__.py b/plugins/bangumicoll/__init__.py index 21d071e..192d236 100644 --- a/plugins/bangumicoll/__init__.py +++ b/plugins/bangumicoll/__init__.py @@ -1,11 +1,20 @@ +# 基础库 import datetime import json -import pytz from typing import Any, Dict, List, Optional, Type +# 第三方库 +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger +import pytz +from sqlalchemy import JSON +from sqlalchemy.orm import Session + +# 项目库 from app.chain.subscribe import SubscribeChain, Subscribe from app.core.config import settings from app.core.context import MediaInfo +from app.core.meta import MetaBase from app.core.metainfo import MetaInfo from app.db.models.subscribehistory import SubscribeHistory from app.db.site_oper import SiteOper @@ -16,10 +25,6 @@ from app.log import logger from app.plugins import _PluginBase from app.schemas.types import NotificationType from app.utils.http import RequestUtils -from apscheduler.schedulers.background import BackgroundScheduler -from apscheduler.triggers.cron import CronTrigger -from sqlalchemy import JSON -from sqlalchemy.orm import Session class BangumiColl(_PluginBase): @@ -30,7 +35,7 @@ class BangumiColl(_PluginBase): # 插件图标 plugin_icon = "https://raw.githubusercontent.com/wikrin/MoviePilot-Plugins/main/icons/bangumi_b.png" # 插件版本 - plugin_version = "1.4" + plugin_version = "1.5" # 插件作者 plugin_author = "Attente" # 作者主页 @@ -50,6 +55,7 @@ class BangumiColl(_PluginBase): # 配置属性 _enabled: bool = False + _total_change: bool = False _cron: str = "" _notify: bool = False _onlyonce: bool = False @@ -76,16 +82,19 @@ class BangumiColl(_PluginBase): def load_config(self, config: dict): """加载配置""" if config: - self._enabled = config.get("enabled", self._enabled) - self._cron = config.get("cron", self._cron) - self._notify = config.get("notify", self._notify) - self._onlyonce = config.get("onlyonce", self._onlyonce) - self._include = config.get("include", self._include) - self._exclude = config.get("exclude", self._exclude) - self._uid = config.get("uid", self._uid) - self._collection_type = config.get("collection_type", [3]) - self._save_path = config.get("save_path", self._save_path) - self._sites = config.get("sites", self._sites) + # 遍历配置中的键并设置相应的属性 + for key in ( + "enabled", + "total_change", + "cron", + "notify", + "onlyonce", + "uid", + "collection_type", + "save_path", + "sites", + ): + setattr(self, f"_{key}", config.get(key, getattr(self, f"_{key}"))) def schedule_once(self): """调度一次性任务""" @@ -109,6 +118,7 @@ class BangumiColl(_PluginBase): { "enabled": self._enabled, "notify": self._notify, + "total_change": self._total_change, "onlyonce": self._onlyonce, "cron": self._cron, "uid": self._uid, @@ -121,199 +131,15 @@ class BangumiColl(_PluginBase): ) def get_form(self): + from .page_components import form + # 列出所有站点 sites_options = [ {"title": site.name, "value": site.id} for site in self.siteoper.list_order_by_pri() ] - return [ - { - 'component': 'VForm', - 'content': [ - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 4}, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'enabled', - 'label': '启用插件', - }, - } - ], - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 4}, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'notify', - 'label': '自动取消订阅并通知', - }, - } - ], - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 4}, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'onlyonce', - 'label': '立即运行一次', - }, - } - ], - }, - ], - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'cron', - 'label': '执行周期', - 'placeholder': '5位cron表达式,留空自动', - }, - } - ], - }, - ], - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'uid', - 'label': 'UID/用户名', - 'placeholder': '设置了用户名填写用户名,否则填写UID', - }, - }, - ], - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VSelect', - 'props': { - 'model': 'collection_type', - 'label': '收藏类型', - 'chips': True, - 'multiple': True, - 'items': [ - {'title': '在看', 'value': 3}, - {'title': '想看', 'value': 1}, - ], - }, - } - ], - }, - ], - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'include', - 'label': '包含', - 'placeholder': '暂未实现', - }, - } - ], - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'exclude', - 'label': '排除', - 'placeholder': '暂未实现', - }, - } - ], - }, - ], - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VTextField', - 'props': { - 'model': 'save_path', - 'label': '保存目录', - 'placeholder': '留空自动', - }, - } - ], - }, - { - 'component': 'VCol', - 'props': {'cols': 12, 'md': 6}, - 'content': [ - { - 'component': 'VSelect', - 'props': { - 'model': 'sites', - 'label': '选择站点', - 'chips': True, - 'multiple': True, - 'items': sites_options, - }, - } - ], - }, - ], - }, - ], - } - ], { - "enabled": False, - "notify": False, - "onlyonce": False, - "cron": "", - "uid": "", - "collection_type": [3], - "include": "", - "exclude": "", - "save_path": "", - "sites": [], - } + return form(sites_options) def get_service(self) -> List[Dict[str, Any]]: """注册插件公共服务""" @@ -372,27 +198,22 @@ class BangumiColl(_PluginBase): def parse_collection_items(self, response) -> Dict[int, Dict[str, Any]]: """解析获取的收藏条目""" - data = response.json().get("data") + data = response.json().get("data", []) if not data: logger.error(f"Bangumi用户:{self._uid} ,没有任何收藏") return {} - items = {} logger.info("解析Bangumi条目信息...") - for item in data: - if item.get("type") not in self._collection_type: - logger.debug( - f"条目: {item['subject'].get('name_cn')} 类型:{item.get('type')} 不符合" - ) - continue - - items[item.get("subject_id")] = { + return { + item.get("subject_id"): { "name": item['subject'].get('name'), "name_cn": item['subject'].get('name_cn'), "date": item['subject'].get('date'), "eps": item['subject'].get('eps'), } - return items + for item in data + if item.get("type") in self._collection_type + } def manage_subscriptions(self, items: Dict[int, Dict[str, Any]]): """管理订阅的新增和删除""" @@ -422,6 +243,7 @@ class BangumiColl(_PluginBase): # 添加订阅 def add_subscribe(self, items: Dict[int, Dict[str, Any]]) -> Dict: """添加订阅""" + fail_items = {} for self._subid, item in items.items(): meta = MetaInfo(item.get("name_cn")) @@ -432,6 +254,7 @@ class BangumiColl(_PluginBase): meta.year = item.get("date")[:4] if item.get("date") else None mediainfo = self.chain.recognize_media(meta=meta) + meta.total_episode = item.get("eps", 0) if not mediainfo: fail_items[self._subid] = f"{item.get('name_cn')} 媒体信息识别失败" continue @@ -442,33 +265,28 @@ class BangumiColl(_PluginBase): mediainfo.tmdb_id, mediainfo.number_of_seasons ) if sid: - logger.info(f"{mediainfo.title_year} {meta.season} 正在订阅中") + logger.info(f"{mediainfo.title_year} 正在订阅中") if len(sid) == 1: self.subscribeoper.update( sid=sid[0].id, payload={"bangumiid": self._subid} ) - logger.info( - f"{mediainfo.title_year} {meta.season} Bangumi条目id更新成功" - ) + logger.info(f"{mediainfo.title_year} Bangumi条目id更新成功") continue sid, msg = self.subscribechain.add( title=mediainfo.title, year=mediainfo.year, - mtype=mediainfo.type, - tmdbid=mediainfo.tmdb_id, bangumiid=self._subid, - season=mediainfo.number_of_seasons, exist_ok=True, username="Bangumi订阅", - **self.prepare_kwargs(item, meta.begin_season, mediainfo), + **self.prepare_kwargs(meta, mediainfo), ) if not sid: fail_items[self._subid] = f"{item.get('name_cn')} {msg}" return fail_items - def prepare_kwargs(self, item: dict, meta_season: int, mediainfo: MediaInfo): + def prepare_kwargs(self, meta: MetaBase, mediainfo: MediaInfo) -> Dict: """准备额外参数""" kwargs = { "save_path": self._save_path, @@ -479,14 +297,24 @@ class BangumiColl(_PluginBase): ), } - if self.check_series_info(meta_season, item.get("eps", 0), mediainfo): - begin_ep, total_ep = self.get_eps() - prev_eps: list = [i for i in range(1, begin_ep)] + total_episode = len(mediainfo.seasons.get(mediainfo.number_of_seasons) or []) + if ( + meta.begin_season + and mediainfo.number_of_seasons != meta.begin_season + or total_episode != meta.total_episode + ): + meta = self.get_eps(meta) + total_ep: int = meta.end_episode if meta.end_episode else total_episode + lock_eps: int = total_ep - meta.begin_episode + 1 + prev_eps: list = [i for i in range(1, meta.begin_episode)] kwargs.update( { "total_episode": total_ep, - "start_episode": begin_ep, - "lack_episode": total_ep - begin_ep + 1, + "start_episode": meta.begin_episode, + "lack_episode": lock_eps, + "manual_total_episode": ( + 1 if meta.total_episode and self._total_change else 0 + ), # 手动修改过总集数 "note": ( prev_eps if self.are_types_equal("note") @@ -495,23 +323,12 @@ class BangumiColl(_PluginBase): } ) logger.info( - f"{mediainfo.title_year} 更新总集数为: {total_ep},开始集数为: {begin_ep}" + f"{mediainfo.title_year} 更新总集数为: {total_ep},开始集数为: {meta.begin_episode}" ) return kwargs - @staticmethod - def check_series_info(meta_season: int, bgm_eps: int, mediainfo: MediaInfo) -> bool: - """检查系列信息是否不一致""" - total_episode = len(mediainfo.seasons.get(mediainfo.number_of_seasons) or []) - return ( - meta_season - and mediainfo.number_of_seasons != meta_season - or (bgm_eps != 0 and total_episode != bgm_eps) - or (bgm_eps == 0 and not total_episode >= 12) - ) - - def update_media_info(self, item, mediainfo): + def update_media_info(self, item: dict, mediainfo: MediaInfo): """更新媒体信息""" for info in mediainfo.season_info: if self.are_dates(item.get("date"), info.get("air_date")): @@ -519,20 +336,19 @@ class BangumiColl(_PluginBase): mediainfo.number_of_episodes = info.get("episode_count") break - def get_eps(self) -> tuple: + def get_eps(self, meta: MetaBase) -> MetaBase: """获取Bangumi条目的集数信息""" try: res = self.get_bgm_res(addr="getEpisodes", id=self._subid) data = res.json().get("data", [{}])[0] - ep = data.get("ep", 1) - sort = data.get("sort", 1) - total = res.json().get("total", 24) - begin_ep = sort - ep + 1 - total_ep = sort - ep + total - return begin_ep, total_ep + prev = data.get("sort", 1) - data.get("ep", 1) + total = res.json().get("total", None) + meta.begin_episode = prev + 1 + meta.end_episode = prev + total if total else None except Exception as e: logger.error(f"获取集数信息失败: {str(e)}") - return 1, 24 # 默认值 + finally: + return meta # 移除订阅 def delete_subscribe(self, del_items: Dict[int, int]): diff --git a/plugins/bangumicoll/page_components.py b/plugins/bangumicoll/page_components.py new file mode 100644 index 0000000..044299c --- /dev/null +++ b/plugins/bangumicoll/page_components.py @@ -0,0 +1,318 @@ +from bs4 import BeautifulSoup + + +def form(sites_options) -> list: + return [ + { + 'component': 'VForm', + 'content': [ + { + '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': 'total_change', + 'label': '不跟随TMDB变动', + }, + } + ], + }, + { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 3}, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + }, + } + ], + }, + ], + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': {'cols': 8, 'md': 4}, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'cron', + 'label': '执行周期', + 'placeholder': '5位cron表达式,留空自动', + }, + } + ], + }, + { + 'component': 'VCol', + 'props': {'cols': 8, 'md': 4}, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'uid', + 'label': 'UID/用户名', + 'placeholder': '设置了用户名填写用户名,否则填写UID', + }, + }, + ], + }, + { + 'component': 'VCol', + 'props': {'cols': 8, 'md': 4}, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'collection_type', + 'label': '收藏类型', + 'chips': True, + 'multiple': True, + 'items': [ + {'title': '在看', 'value': 3}, + {'title': '想看', 'value': 1}, + ], + }, + } + ], + }, + ], + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 6}, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'include', + 'label': '包含', + 'placeholder': '暂未实现', + }, + } + ], + }, + { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 6}, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'exclude', + 'label': '排除', + 'placeholder': '暂未实现', + }, + } + ], + }, + ], + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 6}, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'save_path', + 'label': '保存目录', + 'placeholder': '留空自动', + }, + } + ], + }, + { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 6}, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'sites', + 'label': '选择站点', + 'chips': True, + 'multiple': True, + 'items': sites_options, + }, + } + ], + }, + ], + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + }, + 'content': parse_html( + '
注意: 该插件仅会将公开的收藏添加到订阅。
' + ), + } + ], + } + ], + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + }, + 'content': parse_html( + '注意: 开启自动取消订阅并通知后,已添加的订阅在下一次执行时若不在已选择的收藏类型中,将会被取消订阅。
' + ), + } + ], + } + ], + }, + ], + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + }, + 'content': [ + { + 'component': 'VAlert', + 'props': { + 'type': 'info', + 'variant': 'tonal', + }, + 'content': parse_html( + '注意: 开启不跟随TMDB变动后,从Bangumi API获取的总集数将不再跟随TMDB的集数变动。
' + ), + }, + ], + }, + ], + }, + ], { + "enabled": False, + "total_change": False, + "notify": False, + "onlyonce": False, + "cron": "", + "uid": "", + "collection_type": [3], + "include": "", + "exclude": "", + "save_path": "", + "sites": [], + } + + +def parse_html(html_string: str) -> list: + soup = BeautifulSoup(html_string, 'html.parser') + result: list = [] + + # 定义需要直接转为文本的标签 + inline_text_tags = {'strong', 'u', 'em', 'b', 'i'} + + def process_element(element: BeautifulSoup): + # 处理纯文本节点 + if element.name is None: + text = element.strip() + return text if text else "" + + # 处理HTML标签 + component = element.name + props = {attr: element[attr] for attr in element.attrs} + content = [] + + # 递归处理子元素 + for child in element.children: + child_content = process_element(child) + if isinstance(child_content, str): + content.append({'component': 'span', 'text': child_content}) + elif child_content: # 只有在child_content不为空时添加 + content.append(child_content) + + # 构建标签对象 + tag_data = { + 'component': component, + 'props': props, + 'content': content if component not in inline_text_tags else [], + } + + if content and component in inline_text_tags: + tag_data['text'] = ' '.join( + item['text'] for item in content if 'text' in item + ) + + return tag_data + + # 遍历所有子元素 + for element in soup.children: + element_content = process_element(element) + if element_content: # 只增加非空内容 + result.append(element_content) + + return result