mirror of
https://github.com/d0zingcat/MoviePilot-Plugins.git
synced 2026-05-14 15:09:17 +00:00
feat:标识兼容v2的插件
This commit is contained in:
36
package.json
36
package.json
@@ -3,12 +3,13 @@
|
||||
"name": "站点自动签到",
|
||||
"description": "自动模拟登录、签到站点。",
|
||||
"labels": "站点",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"icon": "signin.png",
|
||||
"author": "thsrite",
|
||||
"level": 2,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v2.4.2": "修复PT时间签到失败问题",
|
||||
"v2.4.1": "修复海胆签到失败问题",
|
||||
"v2.4": "适配m-team Api地址变化",
|
||||
"v2.3.2": "修复YemaPT登录失败,支持YemaPT自动签到",
|
||||
@@ -34,11 +35,12 @@
|
||||
"name": "站点数据统计",
|
||||
"description": "自动统计和展示站点数据。",
|
||||
"labels": "站点,仪表板",
|
||||
"version": "4.0",
|
||||
"version": "4.0.1",
|
||||
"icon": "statistic.png",
|
||||
"author": "lightolly",
|
||||
"level": 2,
|
||||
"history": {
|
||||
"v4.0.1": "修复PTT的魔力值统计",
|
||||
"v4.0": "修复插件数据页异常",
|
||||
"v3.9.3": "修复PTT的用户等级统计",
|
||||
"v3.9.2": "修复YemaPT的上传下载统计错误",
|
||||
@@ -170,12 +172,13 @@
|
||||
"name": "自定义Hosts",
|
||||
"description": "修改系统hosts文件,加速网络访问。",
|
||||
"labels": "网络",
|
||||
"version": "1.1",
|
||||
"version": "1.2",
|
||||
"icon": "hosts.png",
|
||||
"author": "thsrite",
|
||||
"level": 1,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.2": "支持写入注释",
|
||||
"v1.1": "关闭插件时自动恢复系统hosts"
|
||||
}
|
||||
},
|
||||
@@ -225,12 +228,13 @@
|
||||
"name": "媒体库服务器通知",
|
||||
"description": "发送Emby/Jellyfin/Plex服务器的播放、入库等通知消息。",
|
||||
"labels": "消息通知,媒体库",
|
||||
"version": "1.2",
|
||||
"version": "1.3",
|
||||
"icon": "mediaplay.png",
|
||||
"author": "jxxghp",
|
||||
"level": 1,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.3": "兼容处理Emby部分客户端暂停重复推送停止播放webhook的场景",
|
||||
"v1.2": "播放通知增加超链接跳转(需要v1.9.4+)"
|
||||
}
|
||||
},
|
||||
@@ -298,12 +302,14 @@
|
||||
"name": "IYUU自动辅种",
|
||||
"description": "基于IYUU官方Api实现自动辅种。",
|
||||
"labels": "做种,IYUU",
|
||||
"version": "1.9.3",
|
||||
"version": "1.9.5",
|
||||
"icon": "IYUU.png",
|
||||
"author": "jxxghp",
|
||||
"level": 2,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.9.5": "Revert qBittorrent跳检之后自动开始",
|
||||
"v1.9.4": "修复qBittorrent辅种后不会自动开始做种",
|
||||
"v1.9.3": "修复Monika因缺少rsskey,种子下载失败的问题",
|
||||
"v1.9.2": "适配馒头使用API下载种子",
|
||||
"v1.9.1": "支持自定义辅种的种子分类",
|
||||
@@ -332,14 +338,17 @@
|
||||
},
|
||||
"VCBAnimeMonitor": {
|
||||
"name": "整理VCB动漫压制组作品",
|
||||
"description": "提高部分VCB-Studio作品的识别准确率,将VCB-Studio的作品统一转移到指定目录同时进行刮削整理",
|
||||
"description": "一款辅助整理&提高识别VCB-Stuido动漫压制组作品的插件",
|
||||
"labels": "文件整理,识别",
|
||||
"version": "1.8",
|
||||
"version": "1.8.2.1",
|
||||
"icon": "vcbmonitor.png",
|
||||
"author": "pixel@qingwa",
|
||||
"level": 2,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.8.2.1": "修复日志输出&同步目录监控插件功能",
|
||||
"v1.8.2": "提高识别率",
|
||||
"v1.8.1": "重构插件,测试版",
|
||||
"v1.8": "增加了元数据刮削开关,升级后需要手动打开,否则默认不刮削",
|
||||
"v1.7.1": "修复偶尔安装失败问题"
|
||||
}
|
||||
@@ -348,12 +357,13 @@
|
||||
"name": "自动转移做种",
|
||||
"description": "定期转移下载器中的做种任务到另一个下载器。",
|
||||
"labels": "做种",
|
||||
"version": "1.4",
|
||||
"version": "1.5",
|
||||
"icon": "seed.png",
|
||||
"author": "jxxghp",
|
||||
"level": 2,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.5": "修复在转移时只保留了第一个tracker,导致红种问题。此修复确保保留所有的tracker,以提高在不同网络条件下的可达性。",
|
||||
"v1.4": "支持自动删除源下载器在目的下载器中存在的种子"
|
||||
}
|
||||
},
|
||||
@@ -376,22 +386,26 @@
|
||||
"name": "下载器文件同步",
|
||||
"description": "同步下载器的文件信息到数据库,删除文件时联动删除下载任务。",
|
||||
"labels": "下载管理",
|
||||
"version": "1.1",
|
||||
"version": "1.1.1",
|
||||
"icon": "Youtube-dl_A.png",
|
||||
"author": "thsrite",
|
||||
"level": 1,
|
||||
"v2": true
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v1.1.1": "修复时区问题导致的上次同步后8h内的种子不同步的问题"
|
||||
}
|
||||
},
|
||||
"BrushFlow": {
|
||||
"name": "站点刷流",
|
||||
"description": "自动托管刷流,将会提高对应站点的访问频率。",
|
||||
"labels": "刷流,仪表板",
|
||||
"version": "3.6",
|
||||
"version": "3.7",
|
||||
"icon": "brush.jpg",
|
||||
"author": "jxxghp,InfinityPacer",
|
||||
"level": 2,
|
||||
"v2": true,
|
||||
"history": {
|
||||
"v3.7": "下载数量调整为仅获取刷流标签种子并修复了一些细节问题",
|
||||
"v3.6": "优化检查服务中的时间管控",
|
||||
"v3.5": "移除「删种排除MoviePilot任务」配置项(请使用「删除排除标签」替代),完善刷流任务触发插件事件相关逻辑(联动H&R助手)",
|
||||
"v3.4": "移除「记录更多日志」配置项并调整为DEBUG日志,支持「删除排除标签」配置项,增加刷流任务时支持触发插件事件(联动H&R助手)",
|
||||
|
||||
@@ -38,7 +38,7 @@ class AutoSignIn(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "signin.png"
|
||||
# 插件版本
|
||||
plugin_version = "2.4.1"
|
||||
plugin_version = "2.4.2"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
|
||||
64
plugins/autosignin/sites/pttime.py
Normal file
64
plugins/autosignin/sites/pttime.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from typing import Tuple
|
||||
|
||||
from ruamel.yaml import CommentedMap
|
||||
|
||||
from app.log import logger
|
||||
from app.plugins.autosignin.sites import _ISiteSigninHandler
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
class PTTime(_ISiteSigninHandler):
|
||||
"""
|
||||
PT时间签到
|
||||
"""
|
||||
# 匹配的站点Url,每一个实现类都需要设置为自己的站点Url
|
||||
site_url = "pttime.org"
|
||||
|
||||
# 签到成功
|
||||
_succeed_regex = ['签到成功']
|
||||
|
||||
@classmethod
|
||||
def match(cls, url: str) -> bool:
|
||||
"""
|
||||
根据站点Url判断是否匹配当前站点签到类,大部分情况使用默认实现即可
|
||||
:param url: 站点Url
|
||||
:return: 是否匹配,如匹配则会调用该类的signin方法
|
||||
"""
|
||||
return True if StringUtils.url_equal(url, cls.site_url) else False
|
||||
|
||||
def signin(self, site_info: CommentedMap) -> Tuple[bool, str]:
|
||||
"""
|
||||
执行签到操作
|
||||
:param site_info: 站点信息,含有站点Url、站点Cookie、UA等信息
|
||||
:return: 签到结果信息
|
||||
"""
|
||||
site = site_info.get("name")
|
||||
site_cookie = site_info.get("cookie")
|
||||
ua = site_info.get("ua")
|
||||
proxy = site_info.get("proxy")
|
||||
render = site_info.get("render")
|
||||
|
||||
# 签到
|
||||
# 签到返回:<html><head></head><body>签到成功</body></html>
|
||||
html_text = self.get_page_source(url='https://www.pttime.org/attendance.php',
|
||||
cookie=site_cookie,
|
||||
ua=ua,
|
||||
proxy=proxy,
|
||||
render=render)
|
||||
|
||||
if not html_text:
|
||||
logger.error(f"{site} 签到失败,请检查站点连通性")
|
||||
return False, '签到失败,请检查站点连通性'
|
||||
|
||||
if "login.php" in html_text:
|
||||
logger.error(f"{site} 签到失败,Cookie已失效")
|
||||
return False, '签到失败,Cookie已失效'
|
||||
|
||||
sign_status = self.sign_in_result(html_res=html_text,
|
||||
regexs=self._succeed_regex)
|
||||
if sign_status:
|
||||
logger.info(f"{site} 签到成功")
|
||||
return True, '签到成功'
|
||||
|
||||
logger.error(f"{site} 签到失败,签到接口返回 {html_text}")
|
||||
return False, '签到失败'
|
||||
@@ -257,7 +257,7 @@ class BrushFlow(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "brush.jpg"
|
||||
# 插件版本
|
||||
plugin_version = "3.6"
|
||||
plugin_version = "3.7"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp,InfinityPacer"
|
||||
# 作者主页
|
||||
@@ -3641,12 +3641,21 @@ class BrushFlow(_PluginBase):
|
||||
"""
|
||||
获取正在下载的任务数量
|
||||
"""
|
||||
brush_config = self.__get_brush_config()
|
||||
downloader = self.__get_downloader(brush_config.downloader)
|
||||
if not downloader:
|
||||
try:
|
||||
brush_config = self.__get_brush_config()
|
||||
downloader = self.__get_downloader(brush_config.downloader)
|
||||
if not downloader:
|
||||
return 0
|
||||
|
||||
torrents = downloader.get_downloading_torrents(tags=brush_config.brush_tag)
|
||||
if torrents is None:
|
||||
logger.warn("获取下载数量失败,可能是下载器连接发生异常")
|
||||
return 0
|
||||
|
||||
return len(torrents)
|
||||
except Exception as e:
|
||||
logger.error(f"获取下载数量发生异常: {e}")
|
||||
return 0
|
||||
torrents = downloader.get_downloading_torrents()
|
||||
return len(torrents) or 0
|
||||
|
||||
@staticmethod
|
||||
def __get_pubminutes(pubdate: str) -> float:
|
||||
|
||||
@@ -18,7 +18,7 @@ class CustomHosts(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "hosts.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.1"
|
||||
plugin_version = "1.2"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
@@ -235,6 +235,12 @@ class CustomHosts(_PluginBase):
|
||||
for host in hosts:
|
||||
if not host:
|
||||
continue
|
||||
host = host.strip()
|
||||
if host.startswith('#'): # 检查是否为注释行
|
||||
host_entry = HostsEntry(entry_type='comment', comment=host)
|
||||
new_entrys.append(host_entry)
|
||||
continue
|
||||
|
||||
host_arr = str(host).split()
|
||||
try:
|
||||
host_entry = HostsEntry(entry_type='ipv4' if IpUtils.is_ipv4(str(host_arr[0])) else 'ipv6',
|
||||
|
||||
@@ -330,7 +330,7 @@ class DirMonitor(_PluginBase):
|
||||
return
|
||||
|
||||
# 不是媒体文件不处理
|
||||
if file_path.suffix not in settings.RMT_MEDIAEXT:
|
||||
if file_path.suffix.casefold() not in map(str.casefold, settings.RMT_MEDIAEXT):
|
||||
logger.debug(f"{event_path} 不是媒体文件")
|
||||
return
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class IYUUAutoSeed(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "IYUU.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.9.3"
|
||||
plugin_version = "1.9.5"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
@@ -957,6 +957,10 @@ class IYUUAutoSeed(_PluginBase):
|
||||
if self._skipverify:
|
||||
# 跳过校验
|
||||
logger.info(f"{download_id} 跳过校验,请自行检查...")
|
||||
# 请注意这里是故意不自动开始的
|
||||
# 跳过校验存在直接失败、种子目录相同文件不同等异常情况
|
||||
# 必须要用户自行二次确认之后才能开始做种
|
||||
# 否则会出现反复下载刷掉分享率、做假种的情况
|
||||
else:
|
||||
# 追加校验任务
|
||||
logger.info(f"添加校验检查任务:{download_id} ...")
|
||||
|
||||
@@ -20,7 +20,7 @@ class MediaServerMsg(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "mediaplay.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.2"
|
||||
plugin_version = "1.3"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
@@ -40,6 +40,7 @@ class MediaServerMsg(_PluginBase):
|
||||
# 私有属性
|
||||
_enabled = False
|
||||
_types = []
|
||||
_webhook_msg_keys = {}
|
||||
|
||||
# 拼装消息内容
|
||||
_webhook_actions = {
|
||||
@@ -198,6 +199,13 @@ class MediaServerMsg(_PluginBase):
|
||||
logger.info(f"未开启 {event_info.event} 类型的消息通知")
|
||||
return
|
||||
|
||||
expiring_key = f"{event_info.item_id}-{event_info.client}-{event_info.user_name}"
|
||||
# 过滤停止播放重复消息
|
||||
if str(event_info.event) == "playback.stop" and expiring_key in self._webhook_msg_keys.keys():
|
||||
# 刷新过期时间
|
||||
self.__add_element(expiring_key)
|
||||
return
|
||||
|
||||
# 消息标题
|
||||
if event_info.item_type in ["TV", "SHOW"]:
|
||||
message_title = f"{self._webhook_actions.get(event_info.event)}剧集 {event_info.item_name}"
|
||||
@@ -255,10 +263,31 @@ class MediaServerMsg(_PluginBase):
|
||||
else:
|
||||
play_link = None
|
||||
|
||||
if str(event_info.event) == "playback.stop":
|
||||
# 停止播放消息,添加到过期字典
|
||||
self.__add_element(expiring_key)
|
||||
if str(event_info.event) == "playback.start":
|
||||
# 开始播放消息,删除过期字典
|
||||
self.__remove_element(expiring_key)
|
||||
|
||||
# 发送消息
|
||||
self.post_message(mtype=NotificationType.MediaServer,
|
||||
title=message_title, text=message_content, image=image_url, link=play_link)
|
||||
|
||||
def __add_element(self, key, duration=600):
|
||||
expiration_time = time.time() + duration
|
||||
# 如果元素已经存在,更新其过期时间
|
||||
self._webhook_msg_keys[key] = expiration_time
|
||||
|
||||
def __remove_element(self, key):
|
||||
self._webhook_msg_keys = {k: v for k, v in self._webhook_msg_keys.items() if k != key}
|
||||
|
||||
def __get_elements(self):
|
||||
current_time = time.time()
|
||||
# 过滤掉过期的元素
|
||||
self._webhook_msg_keys = {k: v for k, v in self._webhook_msg_keys.items() if v > current_time}
|
||||
return list(self._webhook_msg_keys.keys())
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
|
||||
@@ -11,11 +11,11 @@ class PushPlusMsg(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "PushPlus消息推送"
|
||||
# 插件描述
|
||||
plugin_desc = "支持使用PushPlus发送消息通知。"
|
||||
plugin_desc = "支持使用PushPlus发送消息通知(需实名认证)。"
|
||||
# 插件图标
|
||||
plugin_icon = "Pushplus_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
plugin_version = "1.1"
|
||||
# 插件作者
|
||||
plugin_author = "cheng"
|
||||
# 作者主页
|
||||
@@ -128,6 +128,27 @@ class PushPlusMsg(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VAlert',
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '由于pushplus规则更新,没有实名认证的用户无法发送消息,所以需要用户自己去官网进行认证。官网地址:https://www.pushplus.plus'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
|
||||
@@ -42,7 +42,7 @@ class SiteStatistic(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "statistic.png"
|
||||
# 插件版本
|
||||
plugin_version = "4.0"
|
||||
plugin_version = "4.0.1"
|
||||
# 插件作者
|
||||
plugin_author = "lightolly"
|
||||
# 作者主页
|
||||
|
||||
@@ -118,7 +118,7 @@ class NexusPhpSiteUserInfo(ISiteUserInfo):
|
||||
if bonus_match and bonus_match.group(1).strip():
|
||||
self.bonus = StringUtils.str_float(bonus_match.group(1))
|
||||
return
|
||||
bonus_match = re.search(r"mybonus.[\[\]::<>/a-zA-Z_\-=\"'\s#;.(使用魔力值豆]+\s*([\d,.]+)[<()&\s]", html_text)
|
||||
bonus_match = re.search(r"mybonus.[\[\]::<>/a-zA-Z_\-=\"'\s#;.(使用&说明魔力值豆]+\s*([\d,.]+)[\[<()&\s]", html_text)
|
||||
try:
|
||||
if bonus_match and bonus_match.group(1).strip():
|
||||
self.bonus = StringUtils.str_float(bonus_match.group(1))
|
||||
|
||||
@@ -22,7 +22,7 @@ class SyncDownloadFiles(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "Youtube-dl_A.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.1"
|
||||
plugin_version = "1.1.1"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
@@ -265,7 +265,7 @@ class SyncDownloadFiles(_PluginBase):
|
||||
if last_sync_time:
|
||||
# 获取种子时间
|
||||
if dl_tpe == "qbittorrent":
|
||||
torrent_date = time.gmtime(torrent.get("added_on")) # 将时间戳转换为时间元组
|
||||
torrent_date = time.localtime(torrent.get("added_on")) # 将时间戳转换为时间元组
|
||||
torrent_date = time.strftime("%Y-%m-%d %H:%M:%S", torrent_date) # 格式化时间
|
||||
else:
|
||||
torrent_date = torrent.added_date
|
||||
|
||||
@@ -27,7 +27,7 @@ class TorrentTransfer(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "seed.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.4"
|
||||
plugin_version = "1.5"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
@@ -724,6 +724,9 @@ class TorrentTransfer(_PluginBase):
|
||||
and fastresume_trackers[0]:
|
||||
# 重新赋值
|
||||
torrent_main['announce'] = fastresume_trackers[0][0]
|
||||
# 保留其他tracker,避免单一tracker无法连接
|
||||
if len(fastresume_trackers) > 1 or len(fastresume_trackers[0]) > 1:
|
||||
torrent_main['announce-list'] = fastresume_trackers
|
||||
# 替换种子文件路径
|
||||
torrent_file = settings.TEMP_PATH / f"{torrent_item.get('hash')}.torrent"
|
||||
# 编码并保存到临时文件
|
||||
|
||||
@@ -15,12 +15,11 @@ from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
from app import schemas
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
from app.core.context import MediaInfo
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.core.metainfo import MetaInfoPath
|
||||
from app.db.downloadhistory_oper import DownloadHistoryOper
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
from app.log import logger
|
||||
@@ -73,11 +72,11 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "整理VCB动漫压制组作品"
|
||||
# 插件描述
|
||||
plugin_desc = "提高部分VCB-Studio作品的识别准确率,将VCB-Studio的作品统一转移到指定目录同时进行刮削整理"
|
||||
plugin_desc = "一款辅助整理&提高识别VCB-Stuido动漫压制组作品的插件"
|
||||
# 插件图标
|
||||
plugin_icon = "vcbmonitor.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.8"
|
||||
plugin_version = "1.8.2.2"
|
||||
# 插件作者
|
||||
plugin_author = "pixel@qingwa"
|
||||
# 作者主页
|
||||
@@ -91,7 +90,6 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
|
||||
# 私有属性
|
||||
_switch_ova = False
|
||||
_high_mode = False
|
||||
_torrents_path = None
|
||||
new_save_path = None
|
||||
qb = None
|
||||
@@ -100,6 +98,7 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
downloadhis = None
|
||||
transferchian = None
|
||||
tmdbchain = None
|
||||
mediaChain = None
|
||||
_observer = []
|
||||
_enabled = False
|
||||
_notify = False
|
||||
@@ -126,6 +125,7 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
self.transferhis = TransferHistoryOper()
|
||||
self.downloadhis = DownloadHistoryOper()
|
||||
self.transferchian = TransferChain()
|
||||
self.mediaChain = MediaChain()
|
||||
self.tmdbchain = TmdbChain()
|
||||
# 清空配置
|
||||
self._dirconf = {}
|
||||
@@ -145,7 +145,6 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
self._size = config.get("size") or 0
|
||||
self._scrape = config.get("scrape")
|
||||
self._switch_ova = config.get("ova")
|
||||
self._high_mode = config.get("high_mode")
|
||||
self._torrents_path = config.get("torrents_path") or ""
|
||||
|
||||
# 停止现有任务
|
||||
@@ -164,13 +163,16 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
return
|
||||
|
||||
# 启用种子目录监控
|
||||
if self._torrents_path is not None and Path(self._torrents_path).exists() and self._enabled:
|
||||
if self._torrents_path and Path(self._torrents_path).exists() and self._enabled:
|
||||
# 只取第一个目录作为新的保存
|
||||
first_path = monitor_dirs[0]
|
||||
if SystemUtils.is_windows():
|
||||
self.new_save_path = first_path.split(':')[0] + ":" + first_path.split(':')[1]
|
||||
else:
|
||||
self.new_save_path = first_path.split(':')[0]
|
||||
try:
|
||||
first_path = monitor_dirs[0]
|
||||
if SystemUtils.is_windows():
|
||||
self.new_save_path = first_path.split(':')[0] + ":" + first_path.split(':')[1]
|
||||
else:
|
||||
self.new_save_path = first_path.split(':')[0]
|
||||
except Exception:
|
||||
logger.error(f"目录保存失败,请检查输入目录是否合法")
|
||||
# print(self.new_save_path)
|
||||
try:
|
||||
observer = Observer()
|
||||
@@ -181,7 +183,7 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
observer.start()
|
||||
logger.info(f"{self._torrents_path} 的种子目录监控服务启动,开启监控新增的VCB-Studio种子文件")
|
||||
except Exception as e:
|
||||
logger.error(f"{self._torrents_path} 启动种子目录监控失败:{str(e)}")
|
||||
logger.debug(f"{self._torrents_path} 启动种子目录监控失败:{str(e)}")
|
||||
else:
|
||||
logger.info("种子目录为空,不转移qb中正在下载的VCB-Studio文件")
|
||||
|
||||
@@ -224,7 +226,8 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
try:
|
||||
if target_path and target_path.is_relative_to(Path(mon_path)):
|
||||
logger.warn(f"{target_path} 是监控目录 {mon_path} 的子目录,无法监控")
|
||||
self.systemmessage.put(f"{target_path} 是下载目录 {mon_path} 的子目录,无法监控", title="整理VCB动漫压制组作品")
|
||||
self.systemmessage.put(f"{target_path} 是下载目录 {mon_path} 的子目录,无法监控",
|
||||
title="整理VCB动漫压制组作品")
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(str(e))
|
||||
@@ -290,7 +293,6 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
"size": self._size,
|
||||
"scrape": self._scrape,
|
||||
"ova": self._switch_ova,
|
||||
"high_mode": self._high_mode,
|
||||
"torrents_path": self._torrents_path
|
||||
})
|
||||
|
||||
@@ -376,33 +378,56 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
logger.debug(f"{event_path} 不是媒体文件")
|
||||
return
|
||||
|
||||
# 判断是不是蓝光目录
|
||||
bluray_flag = False
|
||||
if re.search(r"BDMV[/\\]STREAM", event_path, re.IGNORECASE):
|
||||
bluray_flag = True
|
||||
# 截取BDMV前面的路径
|
||||
blurray_dir = event_path[:event_path.find("BDMV")]
|
||||
file_path = Path(blurray_dir)
|
||||
logger.info(f"{event_path} 是蓝光目录,更正文件路径为:{str(file_path)}")
|
||||
|
||||
# 查询历史记录,已转移的不处理
|
||||
if self.transferhis.get_by_src(str(file_path)):
|
||||
logger.info(f"{file_path} 已整理过")
|
||||
return
|
||||
|
||||
# 元数据
|
||||
if file_path.parent.name == "SPs":
|
||||
logger.warn("位于SPs目录下,跳过处理")
|
||||
if file_path.parent.name.lower() in ["sps", "scans", "cds", "previews", "extras"]:
|
||||
logger.warn("位于特典或其他特殊目录下,跳过处理")
|
||||
return
|
||||
remeta = ReMeta(ova_switch=self._switch_ova, high_performance=self._high_mode)
|
||||
|
||||
if 'VCB-Studio' not in file_path.stem.strip():
|
||||
logger.warn("不属于VCB的作品,不处理!")
|
||||
return
|
||||
|
||||
remeta = ReMeta(ova_switch=self._switch_ova)
|
||||
file_meta = remeta.handel_file(file_path=file_path)
|
||||
if file_meta:
|
||||
if not file_meta.name:
|
||||
logger.error(f"{file_path.name} 无法识别有效信息")
|
||||
return
|
||||
if remeta.is_special and not self._switch_ova:
|
||||
if remeta.is_ova and not self._switch_ova:
|
||||
logger.warn(f"{file_path.name} 为OVA资源,未开启OVA开关,不处理")
|
||||
return
|
||||
if remeta.is_special and self._switch_ova:
|
||||
logger.info(f"{file_path.name} 为OVA资源,开始处理")
|
||||
if self.get_data(key=f"OVA_{file_meta.title}") is not None:
|
||||
ova_history_ep = int(self.get_data(key=f"OVA_{file_meta.title}")) + 1
|
||||
file_meta.begin_episode = ova_history_ep
|
||||
self.save_data(key=f"OVA_{file_meta.title}", value=ova_history_ep)
|
||||
if remeta.is_ova and self._switch_ova:
|
||||
logger.info(f"{file_path.name} 为OVA资源,开始历史记录处理")
|
||||
ova_history_ep_list = self.get_data(file_meta.title)
|
||||
if ova_history_ep_list and isinstance(ova_history_ep_list, list):
|
||||
ep = file_meta.begin_episode
|
||||
if ep in ova_history_ep_list:
|
||||
for i in range(1, 100):
|
||||
if ep + i not in ova_history_ep_list:
|
||||
ova_history_ep_list.append(ep + i)
|
||||
file_meta.begin_episode = ep + i
|
||||
logger.info(
|
||||
f"{file_path.name} 为OVA资源,历史记录中已存在,自动识别为第{ep + i}集")
|
||||
break
|
||||
else:
|
||||
ova_history_ep_list.append(ep)
|
||||
self.save_data(file_meta.title, ova_history_ep_list)
|
||||
else:
|
||||
file_meta.begin_episode = 1
|
||||
self.save_data(key=f"OVA_{file_meta.title}", value=1)
|
||||
self.save_data(file_meta.title, [file_meta.begin_episode])
|
||||
else:
|
||||
return
|
||||
|
||||
@@ -418,14 +443,23 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
|
||||
# 根据父路径获取下载历史
|
||||
download_history = None
|
||||
# 按文件全路径查询
|
||||
download_file = self.downloadhis.get_file_by_fullpath(str(file_path))
|
||||
if download_file:
|
||||
download_history = self.downloadhis.get_by_hash(download_file.download_hash)
|
||||
if bluray_flag:
|
||||
# 蓝光原盘,按目录名查询
|
||||
# FIXME 理论上DownloadHistory表中的path应该是全路径,但实际表中登记的数据只有目录名,暂按目录名查询
|
||||
download_history = self.downloadhis.get_by_path(file_path.name)
|
||||
else:
|
||||
# 按文件全路径查询
|
||||
download_file = self.downloadhis.get_file_by_fullpath(str(file_path))
|
||||
if download_file:
|
||||
download_history = self.downloadhis.get_by_hash(download_file.download_hash)
|
||||
|
||||
# 识别媒体信息
|
||||
mediainfo: MediaInfo = self.chain.recognize_media(meta=file_meta,
|
||||
tmdbid=download_history.tmdbid if download_history else None)
|
||||
if download_history and download_history.tmdbid:
|
||||
mediainfo: MediaInfo = self.mediaChain.recognize_media(mtype=MediaType(download_history.type),
|
||||
tmdbid=download_history.tmdbid,
|
||||
doubanid=download_history.doubanid)
|
||||
else:
|
||||
mediainfo: MediaInfo = self.mediaChain.recognize_by_meta(file_meta)
|
||||
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{file_meta.name}')
|
||||
@@ -615,13 +649,13 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
if not torrent_path.exists():
|
||||
return
|
||||
# 只处理刚刚添加的种子也就是获取正在下载的种子
|
||||
logger.info(f"开始转移qb中正在下载的VCB资源,转移目录为:{self.new_save_path}")
|
||||
# 等待种子文件下载完成
|
||||
time.sleep(5)
|
||||
with lock:
|
||||
torrents = self.qb.get_downloading_torrents()
|
||||
for torrent in torrents:
|
||||
if "VCB-Studio" in torrent.name:
|
||||
logger.info(f"开始转移qb中正在下载的VCB资源,转移目录为:{self.new_save_path}")
|
||||
# 原本存在的暂停的种子不处理
|
||||
if torrent.state_enum == qbittorrentapi.TorrentState.PAUSED_DOWNLOAD:
|
||||
continue
|
||||
@@ -813,22 +847,6 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'high_mode',
|
||||
'label': '高性能处理模式',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
@@ -983,7 +1001,7 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
'props': {
|
||||
'model': 'monitor_dirs',
|
||||
'label': '监控目录',
|
||||
'rows': 5,
|
||||
'rows': 4,
|
||||
'placeholder': '每一行一个目录,支持以下几种配置方式,转移方式支持 move、copy、link、softlink、rclone_copy、rclone_move:\n'
|
||||
'监控目录\n'
|
||||
'监控目录#转移方式\n'
|
||||
@@ -1031,8 +1049,10 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '核心用法与目录同步插件相同,不同点在于只识别处理VCB-Studio资源,\n'
|
||||
'不处理SPs目录下的文件,OVA/OAD集数根据入库顺序累加命名,不保证与TMDB集数匹配'
|
||||
'text': '核心用法与目录同步插件相同,不同点在于只识别处理VCB-Studio资源。'
|
||||
'默认不处理SPs、CDs、SCans目录下的文件,OVA/OAD集数暂时根据入库顺序累加命名,'
|
||||
'因此不保证与TMDB集数匹配。部分季度以罗马音音译为名的作品暂时无法识别出准确季度。'
|
||||
'有想法,有问题欢迎点击插件作者主页提issue!'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1053,9 +1073,9 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
'props': {
|
||||
'type': 'info',
|
||||
'variant': 'tonal',
|
||||
'text': '最佳使用方式:监控目录单独设置一个作为保存VCB-Studio资源的目录,\n'
|
||||
'填入监控种子目录,开启后会将正在QB(仅支持QB)下载器内的VCB-Studio资源转移到监控目录实现自动整理('
|
||||
'仅支持第一个监控目录),\n'
|
||||
'text': '最佳使用方式:监控目录单独设置一个作为保存VCB-Studio资源的目录,'
|
||||
'填入监控种子目录,开启后会将正在QB(仅支持QB)下载器内正在下载的VCB-Studio资源转移到监控目录实现自动整理('
|
||||
'仅支持第一个监控目录),'
|
||||
'监控种子目录为空则不转移文件'
|
||||
}
|
||||
}
|
||||
@@ -1077,7 +1097,6 @@ class VCBAnimeMonitor(_PluginBase):
|
||||
"cron": "",
|
||||
"size": 0,
|
||||
"ova": False,
|
||||
"high_mode": False,
|
||||
"torrents_path": "",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import concurrent
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from app.chain.media import MediaChain
|
||||
@@ -8,196 +9,276 @@ from app.core.metainfo import MetaInfoPath
|
||||
from app.log import logger
|
||||
from app.schemas import MediaType
|
||||
|
||||
season_patterns = [
|
||||
{"pattern": re.compile(r"S(\d+)$", re.IGNORECASE), "group": 1},
|
||||
{"pattern": re.compile(r"(\d+)$", re.IGNORECASE), "group": 1},
|
||||
{"pattern": re.compile(r"(\d+)(st|nd|rd|th)?\s*season", re.IGNORECASE), "group": 1},
|
||||
{"pattern": re.compile(r"(.*) ?\s*season (\d+)", re.IGNORECASE), "group": 2},
|
||||
{"pattern": re.compile(r"\s(II|III|IV|V|VI|VII|VIII|IX|X)$", re.IGNORECASE), "group": "1"}
|
||||
]
|
||||
episode_patterns = [
|
||||
{"pattern": re.compile(r"(\d+)\((\d+)\)", re.IGNORECASE), "group": 2},
|
||||
{"pattern": re.compile(r"(\d+)", re.IGNORECASE), "group": 1},
|
||||
{"pattern": re.compile(r'(\d+)v\d+', re.IGNORECASE), "group": 1},
|
||||
]
|
||||
|
||||
def roman_to_int(s) -> int:
|
||||
"""
|
||||
:param s: 罗马数字字符串
|
||||
罗马数字转整数
|
||||
"""
|
||||
roman_dict = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
|
||||
total = 0
|
||||
prev_value = 0
|
||||
ova_patterns = [
|
||||
re.compile(r".*?(OVA|OAD).*?", re.IGNORECASE),
|
||||
re.compile(r"\d+\.5"),
|
||||
re.compile(r"00")
|
||||
]
|
||||
|
||||
for char in reversed(s): # 反向遍历罗马数字字符串
|
||||
current_value = roman_dict[char]
|
||||
if current_value >= prev_value:
|
||||
total += current_value # 如果当前值大于等于前一个值,加上当前值
|
||||
else:
|
||||
total -= current_value # 如果当前值小于前一个值,减去当前值
|
||||
prev_value = current_value
|
||||
final_season_patterns = [
|
||||
re.compile('final season', re.IGNORECASE),
|
||||
re.compile('The Final', re.IGNORECASE),
|
||||
re.compile(r'\sFinal')
|
||||
]
|
||||
|
||||
return total
|
||||
movie_patterns = [
|
||||
re.compile("Movie", re.IGNORECASE),
|
||||
re.compile("the Movie", re.IGNORECASE),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class VCBMetaBase:
|
||||
# 转化为小写后的原始文件名称 (不含后缀)
|
||||
original_title: str = ""
|
||||
# 解析后不包含季度和集数的标题
|
||||
title: str = ""
|
||||
# 类型:TV / Movie (默认TV)
|
||||
type: str = "TV"
|
||||
# 可能含有季度的标题,一级解析后的标题
|
||||
season_title: str = ""
|
||||
# 可能含有集数的字符串列表
|
||||
ep_title: List[str] = None
|
||||
# 识别出来的季度
|
||||
season: int = None
|
||||
# 识别出来的集数
|
||||
ep: int = None
|
||||
# 是否是OVA/OAD
|
||||
is_ova: bool = False
|
||||
# TMDB ID
|
||||
tmdb_id: int = None
|
||||
|
||||
|
||||
blocked_words = ["vcb-studio", "360p", "480p", "720p", "1080p", "2160p", "hdr", "x265", "x264", "aac", "flac"]
|
||||
|
||||
|
||||
class ReMeta:
|
||||
# 解析之后的标题:
|
||||
title: str = None
|
||||
# 识别出来的集数
|
||||
ep: int = None
|
||||
# 识别出来的季度
|
||||
season: int = None
|
||||
# 特殊季识别开关
|
||||
is_special = False
|
||||
# OVA/OAD识别开关
|
||||
ova_switch: bool = False
|
||||
# 高性能处理开关
|
||||
high_performance = False
|
||||
|
||||
season_patterns = [
|
||||
{"pattern": re.compile(r"S(\d+)$"), "group": 1},
|
||||
{"pattern": re.compile(r"(\d+)$"), "group": 1},
|
||||
{"pattern": re.compile(r"(\d+)(st|nd|rd|th)?\s*[Ss][Ee][Aa][Ss][Oo][Nn]"), "group": 1},
|
||||
{"pattern": re.compile(r"(.*) ?\s*[Ss][Ee][Aa][Ss][Oo][Nn] (\d+)"), "group": 2},
|
||||
{"pattern": re.compile(r"\s(II|III|IV|V|VI|VII|VIII|IX|X)$"), "group": "1"}
|
||||
]
|
||||
episode_patterns = [
|
||||
{"pattern": re.compile(r"\[(\d+)\((\d+)\)]"), "group": 2},
|
||||
{"pattern": re.compile(r"\[(\d+)]"), "group": 1},
|
||||
{"pattern": re.compile(r'\[(\d+)v\d+]'), "group": 1},
|
||||
|
||||
]
|
||||
_ova_patterns = [re.compile(r"\[.*?(OVA|OAD).*?]"),
|
||||
re.compile(r"\[\d+\.5]"),
|
||||
re.compile(r"\[00\]")]
|
||||
|
||||
final_season_patterns = [re.compile('final season', re.IGNORECASE),
|
||||
re.compile('The Final', re.IGNORECASE),
|
||||
re.compile(r'\sFinal')
|
||||
]
|
||||
# 自定义添加的季度正则表达式
|
||||
_custom_season_patterns = []
|
||||
|
||||
def __init__(self, ova_switch: bool = False, high_performance: bool = False):
|
||||
def __init__(self, ova_switch: bool = False, custom_season_patterns: list[dict] = None):
|
||||
self.meta = None
|
||||
# TODO:自定义季度匹配规则
|
||||
self.custom_season_patterns = custom_season_patterns
|
||||
self.season_patterns = season_patterns
|
||||
self.ova_switch = ova_switch
|
||||
self.high_performance = high_performance
|
||||
self.vcb_meta = VCBMetaBase()
|
||||
self.is_ova = False
|
||||
|
||||
def is_tv(self, title: str) -> bool:
|
||||
"""
|
||||
判断是否是TV
|
||||
"""
|
||||
if title.count("[") != 4 and title.count("]") != 4:
|
||||
self.vcb_meta.type = "Movie"
|
||||
self.vcb_meta.title = re.sub(r'\[.*?\]', '', title).strip()
|
||||
return False
|
||||
return True
|
||||
|
||||
def handel_file(self, file_path: Path):
|
||||
file_name = file_path.stem.strip().lower()
|
||||
self.vcb_meta.original_title = file_name
|
||||
if not self.is_tv(file_name):
|
||||
logger.warn(
|
||||
"不符合VCB-Studio的剧集命名规范,归类为电影,跳过剧集模块处理。注意:年份较为久远的作品可能在此会判断错误")
|
||||
self.parse_movie()
|
||||
else:
|
||||
self.tv_mode()
|
||||
self.is_ova = self.vcb_meta.is_ova
|
||||
meta = MetaInfoPath(file_path)
|
||||
self.title = meta.title
|
||||
self.title = Path(self.title).stem.strip()
|
||||
if 'VCB-Studio' not in meta.title:
|
||||
logger.warn("不属于VCB的作品,不处理!")
|
||||
return None
|
||||
if meta.title.count("[") != 4 and meta.title.count("]") != 4:
|
||||
# 可能是电影,电影只有三组[],因此去除所有[]后只剩下电影名
|
||||
logger.warn("不符合VCB-Studio的剧集命名规范,跳过剧集模块处理!交给默认处理逻辑")
|
||||
meta.title = re.sub(r'\[.*?\]', '', meta.title).strip()
|
||||
meta.en_name = meta.title
|
||||
return meta
|
||||
split_title: List[str] | None = self.split_season_ep(self.title)
|
||||
if split_title:
|
||||
self.handle_season_ep(split_title)
|
||||
if self.season is not None:
|
||||
meta.begin_season = self.season
|
||||
else:
|
||||
logger.warn("未识别出季度,默认处理逻辑返回第一季")
|
||||
if self.ep is not None:
|
||||
meta.begin_episode = self.ep
|
||||
else:
|
||||
logger.warn("未识别出集数,默认处理逻辑返回第一集")
|
||||
meta.title = self.title
|
||||
meta.en_name = self.title
|
||||
logger.info(f"识别出季度为{self.season},集数为{self.ep},标题为:{self.title}")
|
||||
|
||||
meta.title = self.vcb_meta.title
|
||||
meta.en_name = self.vcb_meta.title
|
||||
if self.vcb_meta.type == "Movie":
|
||||
meta.type = MediaType.MOVIE
|
||||
else:
|
||||
meta.type = MediaType.TV
|
||||
if self.vcb_meta.ep is not None:
|
||||
meta.begin_episode = self.vcb_meta.ep
|
||||
if self.vcb_meta.season is not None:
|
||||
meta.begin_season = self.vcb_meta.season
|
||||
if self.vcb_meta.tmdb_id is not None:
|
||||
meta.tmdbid = self.vcb_meta.tmdb_id
|
||||
return meta
|
||||
|
||||
# 分离季度部分和集数部分
|
||||
def split_season_ep(self, pre_title: str):
|
||||
split_ep = re.findall(r"(\[.*?])", pre_title)[1]
|
||||
if not split_ep:
|
||||
logger.warn("未识别出集数位置信息,结束识别!")
|
||||
return None
|
||||
split_title = re.sub(r"\[.*?\]", "", pre_title).strip()
|
||||
logger.info(f"分离出包含季度的部分:{split_title} \n 分离出包含集数的部分: {split_ep}")
|
||||
return [split_title, split_ep]
|
||||
def split_season_ep(self):
|
||||
# 把所有的[] 里面的内容获取出来,不需要[]本身
|
||||
self.vcb_meta.ep_title = re.findall(r'\[(.*?)\]', self.vcb_meta.original_title)
|
||||
# 去除所有[]后只剩下剧名
|
||||
self.vcb_meta.season_title = re.sub(r"\[.*?\]", "", self.vcb_meta.original_title).strip()
|
||||
if self.vcb_meta.ep_title:
|
||||
self.culling_blocked_words()
|
||||
logger.info(
|
||||
f"分离出包含可能季度的内容部分:{self.vcb_meta.season_title} | 可能包含集数的内容部分: {self.vcb_meta.ep_title}")
|
||||
self.vcb_meta.title = self.vcb_meta.season_title
|
||||
if not self.vcb_meta.ep_title:
|
||||
self.vcb_meta.title = self.vcb_meta.season_title
|
||||
logger.warn("未识别出可能存在集数位置的信息,跳过剩余识别步骤!")
|
||||
|
||||
def handle_season_ep(self, title: List[str]):
|
||||
if self.high_performance:
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=2) as executor:
|
||||
title_season_result = executor.submit(self.handle_season, title[0])
|
||||
ep_result = executor.submit(self.re_ep, title[1], )
|
||||
try:
|
||||
title_season_result = title_season_result.result() # Blocks until the task is complete.
|
||||
ep_result = ep_result.result() # Blocks until the task is complete.
|
||||
except Exception as exc:
|
||||
print('Generated an exception: %s' % exc)
|
||||
else:
|
||||
title_season_result = self.handle_season(title[0])
|
||||
ep_result = self.re_ep(title[1])
|
||||
self.title = title_season_result["title"]
|
||||
is_ova = ep_result["is_ova"]
|
||||
if ep_result["ep"] is not None:
|
||||
self.ep = ep_result["ep"]
|
||||
if title_season_result["season"]:
|
||||
self.season = title_season_result["season"]
|
||||
if is_ova:
|
||||
self.season = 0
|
||||
self.is_special = True
|
||||
def tv_mode(self):
|
||||
logger.info("开始分离季度和集数部分")
|
||||
self.split_season_ep()
|
||||
if not self.vcb_meta.ep_title:
|
||||
return
|
||||
self.parse_season()
|
||||
self.parse_episode()
|
||||
|
||||
# 处理季度
|
||||
def handle_season(self, pre_title: str) -> dict:
|
||||
title_season = {"title": pre_title, "season": 1}
|
||||
for season_pattern in self.season_patterns:
|
||||
pattern = season_pattern["pattern"]
|
||||
group = season_pattern["group"]
|
||||
match = pattern.search(pre_title)
|
||||
def parse_season(self):
|
||||
"""
|
||||
从标题中解析季度
|
||||
"""
|
||||
flag = False
|
||||
for pattern in season_patterns:
|
||||
match = pattern["pattern"].search(self.vcb_meta.season_title)
|
||||
if match:
|
||||
if type(group) == str:
|
||||
title_season["season"] = roman_to_int(match.group(int(group)))
|
||||
title_season["title"] = re.sub(pattern, "", pre_title).strip()
|
||||
if isinstance(pattern["group"], int):
|
||||
self.vcb_meta.season = int(match.group(pattern["group"]))
|
||||
else:
|
||||
title_season["season"] = int(match.group(group))
|
||||
title_season["title"] = re.sub(pattern, "", pre_title).strip()
|
||||
return title_season
|
||||
for final_season_pattern in self.final_season_patterns:
|
||||
match = final_season_pattern.search(pre_title)
|
||||
if match:
|
||||
logger.info("识别出最终季度,开始处理!")
|
||||
title_season["title"] = re.sub(final_season_pattern, "", pre_title).strip()
|
||||
title_season["season"] = self.handle_final_season(title=pre_title)
|
||||
break
|
||||
return title_season
|
||||
self.vcb_meta.season = self.roman_to_int(match.group(pattern["group"]))
|
||||
# 匹配成功后,标题中去除季度信息
|
||||
self.vcb_meta.title = pattern["pattern"].sub("", self.vcb_meta.season_title).strip
|
||||
logger.info(f"识别出季度为{self.vcb_meta.season}")
|
||||
return
|
||||
logger.info(f"正常匹配季度失败,开始匹配ova/oad/最终季度")
|
||||
if not flag:
|
||||
# 匹配是否为最终季
|
||||
for pattern in final_season_patterns:
|
||||
if pattern.search(self.vcb_meta.season_title):
|
||||
logger.info("命中到最终季匹配规则")
|
||||
self.vcb_meta.title = pattern.sub("", self.vcb_meta.season_title).strip()
|
||||
self.handle_final_season()
|
||||
return
|
||||
logger.info("未识别出最终季度,开始匹配OVA/OAD")
|
||||
# 匹配是否为OVA/OAD
|
||||
if "ova" in self.vcb_meta.season_title or "oad" in self.vcb_meta.season_title:
|
||||
logger.info("季度部分命中到OVA/OAD匹配规则")
|
||||
if self.ova_switch:
|
||||
logger.info("开启OVA/OAD处理逻辑")
|
||||
self.vcb_meta.is_ova = True
|
||||
for pattern in ova_patterns:
|
||||
if pattern.search(self.vcb_meta.season_title):
|
||||
self.vcb_meta.title = pattern.sub("", self.vcb_meta.season_title).strip()
|
||||
self.vcb_meta.title = re.sub("ova|oad", "", self.vcb_meta.season_title).strip()
|
||||
self.vcb_meta.season = 0
|
||||
return
|
||||
logger.warn("未识别出季度,默认处理逻辑返回第一季")
|
||||
self.vcb_meta.title = self.vcb_meta.season_title
|
||||
self.vcb_meta.season = 1
|
||||
|
||||
# 处理存在“Final”字样命名的季度
|
||||
def handle_final_season(self, title: str) -> int | None:
|
||||
medias = MediaChain().search(title=title)[1]
|
||||
if not medias:
|
||||
logger.warn("没有找到对应的媒体信息!")
|
||||
return
|
||||
# 根据类型进行过滤,只取类型是电视剧和动漫的media
|
||||
medias = [media for media in medias if media.type == MediaType.TV]
|
||||
if not medias:
|
||||
logger.warn("没有找到动漫或电视剧的媒体信息!")
|
||||
return
|
||||
media = sorted(medias, key=lambda x: x.popularity, reverse=True)[0]
|
||||
media_tmdb_id = media.tmdb_id
|
||||
seasons_info = TmdbChain().tmdb_seasons(tmdbid=media_tmdb_id)
|
||||
if seasons_info is None:
|
||||
logger.warn("无法获取最终季")
|
||||
else:
|
||||
logger.info(f"获取到最终季,季度为{len(seasons_info)}")
|
||||
return len(seasons_info)
|
||||
def parse_episode(self):
|
||||
"""
|
||||
从标题中解析集数
|
||||
"""
|
||||
# 从ep_title中剔除不相关的内容之后只剩下存在集数的字符串
|
||||
ep = self.vcb_meta.ep_title[0]
|
||||
for pattern in episode_patterns:
|
||||
match = pattern["pattern"].search(ep)
|
||||
if match:
|
||||
self.vcb_meta.ep = int(match.group(pattern["group"]))
|
||||
logger.info(f"识别出集数为{self.vcb_meta.ep}")
|
||||
return
|
||||
# 直接进入判断是否为OVA/OAD
|
||||
for pattern in ova_patterns:
|
||||
if pattern.search(ep):
|
||||
self.vcb_meta.is_ova = True
|
||||
# 直接获取数字
|
||||
self.vcb_meta.ep = int(re.search(r"\d+", ep).group()) or 1
|
||||
logger.info(f"OVA模式下识别出集数为{self.vcb_meta.ep}")
|
||||
self.vcb_meta.season = 0
|
||||
return
|
||||
|
||||
def re_ep(self, ep_title: str, ) -> dict:
|
||||
def culling_blocked_words(self):
|
||||
"""
|
||||
# 集数匹配处理模块
|
||||
:param ep_title: 从title解析出的集数,ep_title固定格式[集数]
|
||||
1.先判断是否存在OVA/OAD,形如:[OVA],[12(OVA)],[12.5]这种形式都是属于OVA/OAD,交给处理OVA模块处理
|
||||
2.集数通常有两种情况一种:[12]直接性,另一种:[12(24)],这一种应该去括号内的为集数
|
||||
:return: 集数(int)
|
||||
从ep_title中剔除不相关的内容
|
||||
"""
|
||||
ep_ova = {"ep": None, "is_ova": False}
|
||||
for ova_pattern in self._ova_patterns:
|
||||
match = ova_pattern.search(ep_title)
|
||||
if match:
|
||||
ep_ova["is_ova"] = True
|
||||
ep_ova["ep"] = 1
|
||||
return ep_ova
|
||||
for ep_pattern in self.episode_patterns:
|
||||
pattern = ep_pattern["pattern"]
|
||||
group = ep_pattern["group"]
|
||||
match = pattern.search(ep_title)
|
||||
if match:
|
||||
ep_ova["ep"] = int(match.group(group))
|
||||
return ep_ova
|
||||
return ep_ova
|
||||
blocked_set = set(blocked_words) # 将阻止词列表转换为集合
|
||||
result = [ep for ep in self.vcb_meta.ep_title if not any(word in ep for word in blocked_set)]
|
||||
self.vcb_meta.ep_title = result
|
||||
|
||||
def handle_final_season(self):
|
||||
|
||||
_, medias = MediaChain().search(title=self.vcb_meta.title)
|
||||
if not medias:
|
||||
logger.warning("匹配到最终季时无法找到对应的媒体信息!季度返回默认值:1")
|
||||
self.vcb_meta.season = 1
|
||||
return
|
||||
|
||||
filter_medias = [media for media in medias if media.type == MediaType.TV]
|
||||
if not filter_medias:
|
||||
logger.warning("匹配到最终季时无法找到对应的媒体信息!季度返回默认值:1")
|
||||
self.vcb_meta.season = 1
|
||||
return
|
||||
medias = [media for media in filter_medias if media.popularity or media.vote_average]
|
||||
if not medias:
|
||||
logger.warning("匹配到最终季时无法找到对应的媒体信息!季度返回默认值:1")
|
||||
self.vcb_meta.season = 1
|
||||
return
|
||||
# 获取欢迎度最高或者评分最高的媒体
|
||||
medias_sorted = sorted(medias, key=lambda x: x.popularity or x.vote_average, reverse=True)[0]
|
||||
self.vcb_meta.tmdb_id = medias_sorted.tmdb_id
|
||||
if medias_sorted.tmdb_id:
|
||||
seasons_info = TmdbChain().tmdb_seasons(tmdbid=medias_sorted.tmdb_id)
|
||||
if seasons_info:
|
||||
self.vcb_meta.season = len(seasons_info)
|
||||
logger.info(f"获取到最终季度,季度为{self.vcb_meta.season}")
|
||||
return
|
||||
logger.warning("无法获取到最终季度信息,季度返回默认值:1")
|
||||
self.vcb_meta.season = 1
|
||||
|
||||
|
||||
|
||||
def parse_movie(self):
|
||||
logger.info("开始尝试剧场版模式解析")
|
||||
for pattern in movie_patterns:
|
||||
if pattern.search(self.vcb_meta.title):
|
||||
logger.info("命中剧场版匹配规则,加上剧场版标识辅助识别")
|
||||
self.vcb_meta.type = "Movie"
|
||||
self.vcb_meta.title = pattern.sub("", self.vcb_meta.title).strip()
|
||||
self.vcb_meta.title = self.vcb_meta.title
|
||||
return
|
||||
|
||||
def find_ova_episode(self):
|
||||
"""
|
||||
搜索OVA的集数
|
||||
TODO:模糊匹配OVA的集数
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@staticmethod
|
||||
def roman_to_int(s) -> int:
|
||||
"""
|
||||
:param s: 罗马数字字符串
|
||||
罗马数字转整数
|
||||
"""
|
||||
roman_dict = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
|
||||
total = 0
|
||||
prev_value = 0
|
||||
|
||||
for char in reversed(s): # 反向遍历罗马数字字符串
|
||||
current_value = roman_dict[char]
|
||||
if current_value >= prev_value:
|
||||
total += current_value # 如果当前值大于等于前一个值,加上当前值
|
||||
else:
|
||||
total -= current_value # 如果当前值小于前一个值,减去当前值
|
||||
prev_value = current_value
|
||||
|
||||
return total
|
||||
|
||||
|
||||
|
||||
# if __name__ == '__main__':
|
||||
# ReMeta(
|
||||
# ova_switch=True,
|
||||
# ).handel_file(Path(
|
||||
# r"[Airota&Nekomoe kissaten&VCB-Studio] Yuru Camp [Heya Camp EP00][Ma10p_1080p][x265_flac].mkv"))
|
||||
|
||||
Reference in New Issue
Block a user