From 509bedefba801d376d00a5a4b30120b4928a16b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AE=E5=8F=AE=E5=BD=93?= <604054726@qq.com> Date: Tue, 20 Feb 2024 18:14:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E7=B1=BB=E8=87=AA=E5=8A=A8=E5=88=9B?= =?UTF-8?q?=E5=BB=BA/=E6=96=B0=E5=A2=9E=E5=AE=9A=E6=97=B6=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- plugins/downloadsitetag/__init__.py | 437 ++++++++++++++++++---------- 2 files changed, 279 insertions(+), 160 deletions(-) diff --git a/package.json b/package.json index 2881213..96c50f7 100644 --- a/package.json +++ b/package.json @@ -338,7 +338,7 @@ "DownloadSiteTag": { "name": "下载任务分类与标签", "description": "自动给下载任务分类与打站点标签、剧集名称标签", - "version": "1.5", + "version": "1.6", "icon": "Youtube-dl_B.png", "author": "叮叮当", "level": 1 diff --git a/plugins/downloadsitetag/__init__.py b/plugins/downloadsitetag/__init__.py index df9d6e2..02d4c4f 100644 --- a/plugins/downloadsitetag/__init__.py +++ b/plugins/downloadsitetag/__init__.py @@ -14,6 +14,7 @@ from app.db.downloadhistory_oper import DownloadHistoryOper from app.db.models.downloadhistory import DownloadHistory from app.modules.themoviedb.tmdbapi import TmdbHelper from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger from app.helper.sites import SitesHelper from app.utils.string import StringUtils @@ -25,7 +26,7 @@ class DownloadSiteTag(_PluginBase): # 插件图标 plugin_icon = "Youtube-dl_B.png" # 插件版本 - plugin_version = "1.5" + plugin_version = "1.6" # 插件作者 plugin_author = "叮叮当" # 作者主页 @@ -47,6 +48,10 @@ class DownloadSiteTag(_PluginBase): _scheduler = None _enabled = False _onlyonce = False + _interval = "计划任务" + _interval_cron = "5 4 * * *" + _interval_time = 6 + _interval_unit = "小时" _enabled_media_tag = False _enabled_tag = True _enabled_category = False @@ -63,19 +68,33 @@ class DownloadSiteTag(_PluginBase): if config: self._enabled = config.get("enabled") self._onlyonce = config.get("onlyonce") + self._interval = config.get("interval") or "计划任务" + self._interval_cron = config.get("interval_cron") or "5 4 * * *" + self._interval_time = self.str_to_number(config.get("interval_time"), 6) + self._interval_unit = config.get("interval_unit") or "小时" self._enabled_media_tag = config.get("enabled_media_tag") self._enabled_tag = config.get("enabled_tag") self._enabled_category = config.get("enabled_category") self._category_movie = config.get("category_movie") or "电影" self._category_tv = config.get("category_tv") or "电视" self._category_anime = config.get("category_anime") or "动漫" + if self.plugin_version == "1.6" and not ("interval_time" in config): + # 新版本v1.6更新插件配置默认配置 + config["interval"] = self._interval + config["interval_time"] = self._interval_cron + config["interval_time"] = self._interval_time + config["interval_unit"] = self._interval_unit + self.update_config(config) + logger.warn(f"{self.LOG_TAG}新版本v{self.plugin_version} 配置修正 ...") # 停止现有任务 self.stop_service() - if self._onlyonce: + if self._enabled or self._onlyonce: # 创建定时任务控制器 self._scheduler = BackgroundScheduler(timezone=settings.TZ) + + if self._onlyonce: # 执行一次, 关闭onlyonce self._onlyonce = False config.update({"onlyonce": self._onlyonce}) @@ -85,6 +104,29 @@ class DownloadSiteTag(_PluginBase): run_date=datetime.datetime.now( tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3) ) + if self._enabled: + if self._interval == "计划任务" or self._interval == "固定间隔": + args = {} + if self._interval == "固定间隔": + args["trigger"] = "interval" + if self._interval_unit == "小时": + args["hours"] = self._interval_time + else: + args["minutes"] = self._interval_time + if args["minutes"] < 5: + args["minutes"] = 5 + logger.info(f"{self.LOG_TAG}启动定时服务: 最小不少于5分钟, 防止执行间隔太短任务冲突") + else: + args["trigger"] = CronTrigger.from_crontab(self._interval_cron) + try: + self._scheduler.add_job(func=lambda: self._complemented_history(interval=True), + **args, + name="补全下载历史的标签与分类") + logger.info( + f"{self.LOG_TAG}添加定时服务: 补全下载历史的标签与分类" + (f"(每){args.get('hours') or args.get('minutes')}{self._interval_unit}执行一次" if args["trigger"] == "interval" else f",计划任务: {self._interval_cron}")) + except Exception as e: + logger.error( + f"{self.LOG_TAG}添加定时服务发生了错误: {str(e)}") if self._scheduler and self._scheduler.get_jobs(): # 启动服务 @@ -98,14 +140,21 @@ class DownloadSiteTag(_PluginBase): def get_command() -> List[Dict[str, Any]]: pass + def get_api(self) -> List[Dict[str, Any]]: pass - def _complemented_history(self): + def str_to_number(self, s: str, i: int) -> int: + try: + return int(s) + except: + return i + + def _complemented_history(self, interval: bool = False): """ 补全下载历史的标签与分类 """ - logger.info(f"{self.LOG_TAG}开始执行: 补全下载历史的标签与分类 ...") + logger.info(f"{self.LOG_TAG}开始执行{'(定时任务)' if interval else ''}: 补全下载历史的标签与分类 ...") # 记录处理的种子, 供辅种(无下载历史)使用 dispose_history = {} for DOWNLOADER in ["qbittorrent", "transmission"]: @@ -119,84 +168,89 @@ class DownloadSiteTag(_PluginBase): # 如果下载器获取种子发生错误 或 没有种子 则跳过 if error or not torrents: continue - else: - logger.info(f"{self.LOG_TAG}下载器 {DOWNLOADER} 种子数:{len(torrents)}") + logger.info(f"{self.LOG_TAG}按时间重新排序 {DOWNLOADER} 种子数:{len(torrents)}") # 按添加时间进行排序, 时间靠前的按大小和名称加入处理历史, 判定为原始种子, 其他为辅种 torrents = self._torrents_sort(torrents=torrents, dl_type=DOWNLOADER) + logger.info(f"{self.LOG_TAG}下载器 {DOWNLOADER} 分析种子信息中 ...") for torrent in torrents: - # 获取已处理种子的key (size, name) - _key = self._torrent_key(torrent=torrent, dl_type=DOWNLOADER) - # 获取种子hash - _hash = self._get_hash(torrent=torrent, dl_type=DOWNLOADER) - if not _hash: - continue - # 提取种子hash对应的下载历史 - history: DownloadHistory = self.downloadhistory_oper.get_by_hash(_hash) - if not history: - # 如果找到已处理种子的历史, 表明当前种子是辅种, 否则创建一个空DownloadHistory - if _key and _key in dispose_history: - history = dispose_history[_key] - # 因为辅种站点必定不同, 所以需要更新站点名字 history.torrent_site - history.torrent_site = None - else: - history = DownloadHistory( - torrent_site=None, - title=None, - type=None, - tmdbid=None) - else: - # 加入历史记录 - if _key: - dispose_history[_key] = history - # 如果站点名称为空, 尝试通过trackers识别 - if not history.torrent_site: - trackers = self._get_trackers(torrent=torrent, dl_type=DOWNLOADER) - for tracker in trackers: - domain = StringUtils.get_url_domain(tracker) - site_info = SitesHelper().get_indexer(domain) - if site_info: - history.torrent_site = site_info.get("name") - break - # 如果通过tracker还是无法获取站点名称, 且tmdbid, type, title都是空的, 那么跳过当前种子 - if not history.torrent_site and not history.tmdbid and not history.type and not history.title: + try: + # 获取已处理种子的key (size, name) + _key = self._torrent_key(torrent=torrent, dl_type=DOWNLOADER) + # 获取种子hash + _hash = self._get_hash(torrent=torrent, dl_type=DOWNLOADER) + if not _hash: continue - # 获取种子当前标签 - torrent_tags = self._get_label(torrent=torrent, dl_type=DOWNLOADER) - torrent_cat = self._get_category(torrent=torrent, dl_type=DOWNLOADER) - # 按设置生成需要写入的标签与分类 - _tags = [] - _cat = None - # 站点标签, 如果勾选开关的话 因允许torrent_site为空时运行到此, 因此需要判断torrent_site不为空 - if self._enabled_tag and history.torrent_site: - _tags.append(history.torrent_site) - # 媒体标题标签, 如果勾选开关的话 因允许title为空时运行到此, 因此需要判断title不为空 - if self._enabled_media_tag and history.title: - _tags.append(history.title) - # 分类, 如果勾选开关的话 因允许mtype为空时运行到此, 因此需要判断mtype不为空。为防止不必要的识别, 种子已经存在分类torrent_cat时 也不执行 - if DOWNLOADER == "qbittorrent" and self._enabled_category and not torrent_cat and history.type: - # 如果是电视剧 需要区分是否动漫 - genre_ids = None - # 因允许tmdbid为空时运行到此, 因此需要判断tmdbid不为空 - if history.tmdbid and (history.type == MediaType.TV or history.type == MediaType.TV.value): - # tmdb_id获取tmdb信息 - tmdb_info = self.tmdb_helper.get_info(mtype=history.type, tmdbid=history.tmdbid) - if tmdb_info: - genre_ids = tmdb_info.get("genre_ids") - _cat = self._genre_ids_get_cat(history.type, genre_ids) - - # 去除种子已经存在的标签 - if _tags and torrent_tags: - _tags = list(set(_tags) - set(torrent_tags)) - # 如果分类一样, 那么不需要修改 - if _cat == torrent_cat: + # 提取种子hash对应的下载历史 + history: DownloadHistory = self.downloadhistory_oper.get_by_hash(_hash) + if not history: + # 如果找到已处理种子的历史, 表明当前种子是辅种, 否则创建一个空DownloadHistory + if _key and _key in dispose_history: + history = dispose_history[_key] + # 因为辅种站点必定不同, 所以需要更新站点名字 history.torrent_site + history.torrent_site = None + else: + history = DownloadHistory( + torrent_site=None, + title=None, + type=None, + tmdbid=None) + else: + # 加入历史记录 + if _key: + dispose_history[_key] = history + # 如果站点名称为空, 尝试通过trackers识别 + if not history.torrent_site: + trackers = self._get_trackers(torrent=torrent, dl_type=DOWNLOADER) + for tracker in trackers: + domain = StringUtils.get_url_domain(tracker) + site_info = SitesHelper().get_indexer(domain) + if site_info: + history.torrent_site = site_info.get("name") + break + # 如果通过tracker还是无法获取站点名称, 且tmdbid, type, title都是空的, 那么跳过当前种子 + if not history.torrent_site and not history.tmdbid and not history.type and not history.title: + continue + # 获取种子当前标签 + torrent_tags = self._get_label(torrent=torrent, dl_type=DOWNLOADER) + torrent_cat = self._get_category(torrent=torrent, dl_type=DOWNLOADER) + # 按设置生成需要写入的标签与分类 + _tags = [] _cat = None - # 判断当前种子是否不需要修改 - if not _cat and not _tags: - continue - # 执行通用方法, 设置种子标签与分类 - self._set_torrent_info(DOWNLOADER=DOWNLOADER, _hash=_hash, _tags=_tags, _cat=_cat, _original_tags=torrent_tags) + # 站点标签, 如果勾选开关的话 因允许torrent_site为空时运行到此, 因此需要判断torrent_site不为空 + if self._enabled_tag and history.torrent_site: + _tags.append(history.torrent_site) + # 媒体标题标签, 如果勾选开关的话 因允许title为空时运行到此, 因此需要判断title不为空 + if self._enabled_media_tag and history.title: + _tags.append(history.title) + # 分类, 如果勾选开关的话 因允许mtype为空时运行到此, 因此需要判断mtype不为空。为防止不必要的识别, 种子已经存在分类torrent_cat时 也不执行 + if DOWNLOADER == "qbittorrent" and self._enabled_category and not torrent_cat and history.type: + # 如果是电视剧 需要区分是否动漫 + genre_ids = None + # 因允许tmdbid为空时运行到此, 因此需要判断tmdbid不为空 + if history.tmdbid and (history.type == MediaType.TV or history.type == MediaType.TV.value): + # tmdb_id获取tmdb信息 + tmdb_info = self.tmdb_helper.get_info(mtype=history.type, tmdbid=history.tmdbid) + if tmdb_info: + genre_ids = tmdb_info.get("genre_ids") + _cat = self._genre_ids_get_cat(history.type, genre_ids) + + # 去除种子已经存在的标签 + if _tags and torrent_tags: + _tags = list(set(_tags) - set(torrent_tags)) + # 如果分类一样, 那么不需要修改 + if _cat == torrent_cat: + _cat = None + # 判断当前种子是否不需要修改 + if not _cat and not _tags: + continue + # 执行通用方法, 设置种子标签与分类 + self._set_torrent_info(DOWNLOADER=DOWNLOADER, _hash=_hash, _torrent=torrent, _tags=_tags, _cat=_cat, _original_tags=torrent_tags) + except Exception as e: + logger.error( + f"{self.LOG_TAG}分析种子信息时发生了错误: {str(e)}") - logger.info(f"{self.LOG_TAG}执行完成: 补全下载历史的标签与分类 ...") + + logger.info(f"{self.LOG_TAG}执行完成{'(定时任务)' if interval else ''}: 补全下载历史的标签与分类 ...") def _genre_ids_get_cat(self, mtype, genre_ids = None): """ @@ -327,14 +381,23 @@ class DownloadSiteTag(_PluginBase): print(str(e)) return None - def _set_torrent_info(self, DOWNLOADER: str, _hash: str, _tags: list, _cat: str, _original_tags: list = None): + def _set_torrent_info(self, DOWNLOADER: str, _hash: str, _torrent: Any = None, _tags: list = [], _cat: str = None, _original_tags: list = None): """ 设置种子标签与分类 """ # 当前下载器 downloader_obj = self._get_downloader(DOWNLOADER) + if not _torrent: + _torrent, error = downloader_obj.get_torrents(ids=_hash) + if not _torrent or error: + logger.error( + f"{self.LOG_TAG}设置种子标签与分类时发生了错误: 通过 {_hash} 查询不到任何种子!") + return + logger.info( + f"{self.LOG_TAG}设置种子标签与分类: {_hash} 查询到 {len(_torrent)} 个种子") + _torrent = _torrent[0] # 判断是否可执行 - if DOWNLOADER and downloader_obj and _hash: + if DOWNLOADER and downloader_obj and _hash and _torrent: # 下载器api不通用, 因此需分开处理 if DOWNLOADER == "qbittorrent": # 设置标签 @@ -342,15 +405,18 @@ class DownloadSiteTag(_PluginBase): downloader_obj.set_torrents_tag(ids=_hash, tags=_tags) # 设置分类 if _cat: - downloader_obj.qbc.torrents_set_category(category=_cat, torrent_hashes=_hash) + # 尝试设置种子分类, 如果失败, 则创建再设置一遍 + try: + _torrent.setCategory(category=_cat) + except: + downloader_obj.qbc.torrents_createCategory(name=_cat) + _torrent.setCategory(category=_cat) else: # 设置标签 if _tags: # _original_tags = None表示未指定, 因此需要获取原始标签 if _original_tags == None: - torrent = downloader_obj.trc.get_torrent(torrent_id=_hash) - if torrent: - _original_tags = self._get_label(torrent=torrent, dl_type=DOWNLOADER) + _original_tags = self._get_label(torrent=_torrent, dl_type=DOWNLOADER) # 如果原始标签不是空的, 那么合并原始标签 if _original_tags: _tags = list(set(_original_tags).union(set(_tags))) @@ -368,23 +434,28 @@ class DownloadSiteTag(_PluginBase): if not event.event_data: return - context: Context = event.event_data.get("context") - _hash = event.event_data.get("hash") - _torrent = context.torrent_info - _media = context.media_info - _tags = [] - _cat = None - # 站点标签, 如果勾选开关的话 - if self._enabled_tag: - _tags.append(_torrent.site_name) - # 媒体标题标签, 如果勾选开关的话 - if self._enabled_media_tag: - _tags.append(_media.title) - # 分类, 如果勾选开关的话 - if self._enabled_category: - _cat = self._genre_ids_get_cat(_media.type, _media.genre_ids) - # 执行通用方法, 设置种子标签与分类 - self._set_torrent_info(DOWNLOADER=settings.DOWNLOADER, _hash=_hash, _tags=_tags, _cat=_cat) + try: + context: Context = event.event_data.get("context") + _hash = event.event_data.get("hash") + _torrent = context.torrent_info + _media = context.media_info + _tags = [] + _cat = None + # 站点标签, 如果勾选开关的话 + if self._enabled_tag and _torrent.site_name: + _tags.append(_torrent.site_name) + # 媒体标题标签, 如果勾选开关的话 + if self._enabled_media_tag and _media.title: + _tags.append(_media.title) + # 分类, 如果勾选开关的话 + if self._enabled_category and _media.type: + _cat = self._genre_ids_get_cat(_media.type, _media.genre_ids) + if _hash and (_tags or _cat): + # 执行通用方法, 设置种子标签与分类 + self._set_torrent_info(DOWNLOADER=settings.DOWNLOADER, _hash=_hash, _tags=_tags, _cat=_cat) + except Exception as e: + logger.error( + f"{self.LOG_TAG}分析下载事件时发生了错误: {str(e)}") def get_form(self) -> Tuple[List[dict], Dict[str, Any]]: @@ -422,7 +493,7 @@ class DownloadSiteTag(_PluginBase): }, 'content': [ { - 'component': 'VSwitch', + 'component': 'VCheckboxBtn', 'props': { 'model': 'enabled_tag', 'label': '自动站点标签', @@ -438,7 +509,7 @@ class DownloadSiteTag(_PluginBase): }, 'content': [ { - 'component': 'VSwitch', + 'component': 'VCheckboxBtn', 'props': { 'model': 'enabled_media_tag', 'label': '自动剧名标签', @@ -454,7 +525,7 @@ class DownloadSiteTag(_PluginBase): }, 'content': [ { - 'component': 'VSwitch', + 'component': 'VCheckboxBtn', 'props': { 'model': 'enabled_category', 'label': '自动设置分类', @@ -464,6 +535,107 @@ class DownloadSiteTag(_PluginBase): } ] }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 12 + }, + 'content': [ + { + 'component': 'VCheckboxBtn', + 'props': { + 'model': 'onlyonce', + 'label': '补全下载历史的标签与分类(一次性任务)' + } + } + ] + } + ] + }, + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3 + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'interval', + 'label': '定时任务', + 'items': [ + {'title': '禁用', 'value': '禁用'}, + {'title': '计划任务', 'value': '计划任务'}, + {'title': '固定间隔', 'value': '固定间隔'} + ] + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 3, + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'interval_cron', + 'label': '计划任务设置', + 'placeholder': '5 4 * * *' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3, + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'interval_time', + 'label': '固定间隔设置, 间隔每', + 'placeholder': '6' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 6, + 'md': 3, + }, + 'content': [ + { + 'component': 'VSelect', + 'props': { + 'model': 'interval_unit', + 'label': '单位', + 'items': [ + {'title': '小时', 'value': '小时'}, + {'title': '分钟', 'value': '分钟'} + ] + } + } + ] + } + ] + }, { 'component': 'VRow', 'content': [ @@ -526,63 +698,6 @@ class DownloadSiteTag(_PluginBase): ] } ] - }, - { - 'component': 'VCol', - 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VSwitch', - 'props': { - 'model': 'onlyonce', - 'label': '立即执行一次: 补全下载历史的标签与分类', - } - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VAlert', - 'props': { - 'type': 'info', - 'variant': 'tonal', - 'text': '注意:本插件将自动对下载任务打上站点标签' - } - } - ] - } - ] - }, - { - 'component': 'VRow', - 'content': [ - { - 'component': 'VCol', - 'props': { - 'cols': 12, - }, - 'content': [ - { - 'component': 'VAlert', - 'props': { - 'type': 'info', - 'variant': 'tonal', - 'text': '注意:分类只支持qb, 并提前在qb创建好分类' - } - } - ] - } - ] } ] } @@ -595,6 +710,10 @@ class DownloadSiteTag(_PluginBase): "category_movie": "电影", "category_tv": "电视", "category_anime": "动漫", + "interval": "计划任务", + "interval_cron": "5 4 * * *", + "interval_time": "6", + "interval_unit": "小时" } def get_page(self) -> List[dict]: