From d961afb0101de010b7ffe4d32d02154751ed6464 Mon Sep 17 00:00:00 2001 From: Attente <19653207+wikrin@users.noreply.github.com> Date: Fri, 6 Sep 2024 04:45:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=8F=92=E4=BB=B6=EF=BC=9ABa?= =?UTF-8?q?ngumiColl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- icons/bangumi_b.png | Bin 0 -> 4355 bytes package.json | 14 + plugins/bangumicoll/__init__.py | 505 ++++++++++++++++++++++++++++++++ 3 files changed, 519 insertions(+) create mode 100644 icons/bangumi_b.png create mode 100644 package.json create mode 100644 plugins/bangumicoll/__init__.py diff --git a/icons/bangumi_b.png b/icons/bangumi_b.png new file mode 100644 index 0000000000000000000000000000000000000000..29ea9b4e36fe30a046e0ea952c6d3ae2b69ab92c GIT binary patch literal 4355 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..384ccf0 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "BangumiColl": { + "name": "Bangumi收藏订阅", + "description": "Bangumi用户收藏添加到订阅", + "labels": "订阅", + "version": "1.0", + "icon": "https://raw.githubusercontent.com/wikrin/MoviePilot-Plugins/main/icons/bangumi_b.png", + "author": "Attente", + "level": 2, + "history": { + "v1.0": "将bangumi用户收藏添加到 MP 订阅,部分功能未实现" + } + }, +} diff --git a/plugins/bangumicoll/__init__.py b/plugins/bangumicoll/__init__.py new file mode 100644 index 0000000..7cb76b7 --- /dev/null +++ b/plugins/bangumicoll/__init__.py @@ -0,0 +1,505 @@ +import datetime + +from typing import Optional, Any, List, Dict + +import pytz +from apscheduler.schedulers.background import BackgroundScheduler + +from apscheduler.triggers.cron import CronTrigger + +from app.chain.subscribe import SubscribeChain +from app.core.config import settings + +from app.core.context import MediaInfo +from app.core.metainfo import MetaInfo +from app.log import logger +from app.plugins import _PluginBase +from app.db.site_oper import SiteOper +from app.utils.http import RequestUtils + + +class BangumiColl(_PluginBase): + # 插件名称 + plugin_name = "bangumi收藏订阅" + # 插件描述 + plugin_desc = "将bangumi用户收藏添加到订阅" + # 插件图标 + plugin_icon = "https://raw.githubusercontent.com/wikrin/MoviePilot-Plugins/main/icons/bangumi_b.png" + # 插件版本 + plugin_version = "1.0" + # 插件作者 + plugin_author = "Attente" + # 作者主页 + author_url = "https://github.com/wikrin" + # 插件配置项ID前缀 + plugin_config_prefix = "bangumicoll_" + # 加载顺序 + plugin_order = 23 + # 可使用的用户级别 + auth_level = 2 + + # 私有变量 + _scheduler: Optional[BackgroundScheduler] = None + siteoper: SiteOper = None + + # 配置属性 + _enabled: bool = False + _cron: str = "" + _notify: bool = False + _onlyonce: bool = False + _include: str = "" + _exclude: str = "" + _uid: str = "" + _collection_type = [] + _collection: Dict = {} + _save_path: str = "" + _sites: list = [] + + + def init_plugin(self, config: dict = None): + self.subscribechain = SubscribeChain() + self.siteoper = SiteOper() + + # 停止现有任务 + self.stop_service() + + # 配置 + if config: + self._enabled = config.get("enabled") + self._cron = config.get("cron") + self._notify = config.get("notify") + self._onlyonce = config.get("onlyonce") + self._include = config.get("include") + self._exclude = config.get("exclude") + self._uid = config.get("uid") + self._collection_type = config.get("collection_type") or [3] + self._collection = config.get("collection") + self._save_path = config.get("save_path") + self._sites = config.get("sites") + + if self._onlyonce: + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + logger.info(f"bangumi收藏订阅启动,立即运行一次") + self._scheduler.add_job( + func=self.bangumi_coll, + trigger='date', + run_date=datetime.datetime.now(tz=pytz.timezone(settings.TZ)) + + datetime.timedelta(seconds=3), + ) + + # 启动任务 + if self._scheduler.get_jobs(): + self._scheduler.print_jobs() + self._scheduler.start() + + if self._onlyonce: + # 关闭一次性开关 + self._onlyonce = False + # 保存设置 + self.__update_config() + + def __update_config(self): + """ + 更新设置 + """ + self.update_config( + { + "enabled": self._enabled, + "notify": self._notify, + "onlyonce": self._onlyonce, + "cron": self._cron, + "uid": self._uid, + "collection_type": self._collection_type, + "collection": self._collection, + "include": self._include, + "exclude": self._exclude, + "save_path": self._save_path, + "sites": self._sites, + } + ) + + def get_api(self): + pass + + def get_command(self): + pass + + def get_form(self): + # 列出所有站点 + 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": [], + } + + def get_page(self): + pass + + # 注册定时任务 + def get_service(self) -> List[Dict[str, Any]]: + """ + 注册插件公共服务 + [{ + "id": "服务ID", + "name": "服务名称", + "trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()", + "func": self.xxx, + "kwargs": {} # 定时器参数 + }] + """ + if self._enabled and self._cron: + return [ + { + "id": "BangumiColl", + "name": "Bangumi收藏订阅", + "trigger": CronTrigger.from_crontab(self._cron), + "func": self.bangumi_coll, + "kwargs": {}, + } + ] + elif self._enabled: + return [ + { + "id": "BangumiColl", + "name": "Bangumi收藏订阅", + "trigger": "interval", + "func": self.bangumi_coll, + "kwargs": {"hours": 6}, + } + ] + return [] + + def stop_service(self): + """ + 退出插件 + """ + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._scheduler.shutdown() + self._scheduler = None + except Exception as e: + logger.error("退出插件失败:%s" % str(e)) + + def get_state(self): + return self._enabled + + def bangumi_coll(self): + """ + 订阅bangumi用户收藏 + """ + if not self._uid: + logger.error("请设置UID") + return + + addr = f"https://api.bgm.tv/v0/users/{self._uid}/collections?subject_type=2" + headers = { + "User-Agent": "jxxghp/MoviePilot-Plugins (https://github.com/jxxghp/MoviePilot-Plugins)" + } + + try: + logger.info(f"查询bangumi条目信息:{addr} ...") + res = RequestUtils(headers=headers).get_res(url=addr) + res = res.json().get("data") + if not res: + logger.error(f"bangumi用户:{self._uid} ,未查询到数据") + except Exception as e: + logger.error(f"获取bangumi收藏数据失败:{addr} 失败:{str(e)}") + + # 解析出必要数据 + items: Dict[int, Dict[str, Any]] = {} + logger.info(f"解析bangumi条目信息...") + for item in res: + if item.get("type") not in self._collection_type: + continue + # 条目id + subject_id = item.get("subject_id") + # 主标题 + name = item['subject'].get('name') + # 中文标题 + name_cn = item['subject'].get('name_cn') + # 放送时间 + date = item['subject'].get('date') + ## 这里在后面添加排除规则 + items.update({subject_id: {"name": name, "name_cn": name_cn, "date": date}}) + ## 获取此插件添加的订阅 + db_sub = {i.bangumiid: i.id for i in self.subscribechain.subscribeoper.list() if i.bangumiid and i.username == "Bangumi订阅"} + # 新增条目 + new_sub = items.keys() - db_sub.keys() + # 移除条目, 这里暂时不做 + # del_sub = dbrid.keys() - items.keys() + logger.info(f"解析bangumi条目信息完成,共{len(items)}条,新增{len(new_sub)}条") + + # # 执行移除操作 + # if del_sub: + # del_items = {dbrid[i]: i for i in del_sub} + # logger.info(f"开始移除订阅...") + # self.delete_subscribe(del_items) + + # 执行添加操作 + if new_sub: + new_sub = {i: items[i] for i in new_sub} + logger.info(f"开始添加订阅...") + self.add_subscribe(new_sub) + + # 结束 + logger.info(f"bangumi收藏订阅执行完成") + + + + # 添加订阅 + def add_subscribe(self, items: Dict[int, Dict[str, Any]]): + for subject_id, item in items.items(): + meta = MetaInfo(item.get("name_cn")) + if not meta.name: + logger.warn(f"{item.get('name_cn')} 未识别到有效数据") + continue + # 由于bangumi的api不包含季度信息,不传入bangumi条目id,默认使用tmdb + mediainfo: MediaInfo = self.chain.recognize_media(meta=meta) + # 对比bangumi和tmdb的信息确定季度 + for info in mediainfo.season_info: + # 对比日期, 误差默认7天 + if not self.are_dates(item.get("date"), info.get("air_date")): + continue + else: + # 更新季度信息 + mediainfo.number_of_seasons = info.get("season_number") + # 更新集数信息 + mediainfo.number_of_episodes = info.get("episode_count") + + # 检查是否已经订阅 + subflag = self.subscribechain.exists(mediainfo=mediainfo, meta=meta) + if subflag: + logger.info(f'{mediainfo.title_year} {meta.season} 正在订阅中') + continue + + # 额外参数 + kwargs = { + "save_path": self._save_path, + "sites": self._sites, + } + # 添加到订阅 + self.subscribechain.add( + title=mediainfo.title, + year=mediainfo.year, + mtype=mediainfo.type, + tmdbid=mediainfo.tmdb_id, + bangumiid=subject_id, + season=mediainfo.number_of_seasons, + exist_ok=True, + username="Bangumi订阅", + **kwargs, + ) + + def delete_subscribe(self, del_items: dict): + pass + + @staticmethod + def are_dates(date_str1, date_str2, threshold_days: int = 7) -> bool: + """ + 对比两个日期字符串是否接近 + :param date_str1: 第一个日期字符串,格式为'YYYY-MM-DD' + :param date_str2: 第二个日期字符串,格式为'YYYY-MM-DD' + :param threshold_days: 阈值天数,默认为1天 + :return: 如果两个日期之间的差异小于等于阈值天数,则返回True,否则返回False + """ + # 将日期字符串转换为datetime对象 + date1 = datetime.datetime.strptime(date_str1, '%Y-%m-%d') + date2 = datetime.datetime.strptime(date_str2, '%Y-%m-%d') + + # 计算两个日期之间的差异 + delta = abs(date1 - date2) + + # 将阈值转换为timedelta对象 + threshold = datetime.timedelta(days=threshold_days) + + # 比较差异和阈值 + return delta <= threshold