diff --git a/.vscode/launch.json b/.vscode/launch.json index 21b889b4..7566a31e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,11 +4,23 @@ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Python: 当前文件", + "type": "python", + "request": "launch", + "program": "${file}", + "cwd": "${workspaceFolder}/AutoBangumi/app", + "console": "integratedTerminal", + "justMyCode": true + }, { "name": "Python: docker_main", "type": "python", "request": "launch", - "program": "${workspaceFolder}/AutoBangumi/app/docker_main.py", + "program": "${workspaceFolder}/AutoBangumi/app/app.py", + "args": [ + "-d" + ], "cwd": "${workspaceFolder}/AutoBangumi/app", "console": "integratedTerminal", "justMyCode": true diff --git a/AutoBangumi/Dockerfile b/AutoBangumi/Dockerfile index d7a641a0..2a21fa96 100644 --- a/AutoBangumi/Dockerfile +++ b/AutoBangumi/Dockerfile @@ -18,4 +18,4 @@ ENV RULE_DEBUG=False COPY ./app /app COPY ./config /config -CMD [ "python3", "/app/docker_main.py"] \ No newline at end of file +CMD [ "python3", "/app/app.py"] \ No newline at end of file diff --git a/AutoBangumi/app/RSSFilter.py b/AutoBangumi/app/RssFilter/RSSFilter.py similarity index 81% rename from AutoBangumi/app/RSSFilter.py rename to AutoBangumi/app/RssFilter/RSSFilter.py index e3c4825e..1c93350d 100644 --- a/AutoBangumi/app/RSSFilter.py +++ b/AutoBangumi/app/RssFilter/RSSFilter.py @@ -2,13 +2,11 @@ import re import json import zhconv import logging -from RssFilter.fliter_base import * +from fliter_base import * logger = logging.getLogger(__name__) handler = logging.FileHandler( - filename="RssFilter/rename_log.txt", - mode="w", - encoding="utf-8" + filename="RssFilter/rename_log.txt", mode="w", encoding="utf-8" ) handler.setFormatter( logging.Formatter( @@ -18,36 +16,8 @@ handler.setFormatter( logger.level = logging.WARNING logger.addHandler(handler) - -class RSSInfoCleaner: - class Name: - raw = None - conv = None - zh = None - en = None - jp = None - clean = None - - class Info: - group = None - season = None - episode = None - vision = None - - class Tag: - dpi = None - ass = None - lang = None - type = None - code = None - source = None - - def __init__(self, file_name): - self.file_name = file_name - self.Name.raw = file_name # 接收文件名参数 - self.clean() # 清理广告等杂质 - # 匹配特征等 - self.group_character = [ +const = { + "group_character":[ "字幕社", "字幕组", "字幕屋", @@ -78,8 +48,8 @@ class RSSInfoCleaner: "百合组", "慕留人", "行动组", - ] - self.group_char = [ + ], + "group_char" : [ "dmhy", "澄空学园", "c.c动漫", @@ -109,25 +79,62 @@ class RSSInfoCleaner: "cxraw", "witex.io", ] - with open("../config/clean_rule.json", encoding="utf-8") as file_obj: +} +const["all_charactor"] = const["group_character"] + const["group_char"] + +class RSSInfoCleaner: + class Name: + def __init__(self) -> None: + self.raw = None + self.conv = None + self.zh = None + self.en = None + self.jp = None + self.clean = None + + class Info: + def __init__(self) -> None: + self.group = None + self.season = None + self.episode = None + self.vision = None + + class Tag: + def __init__(self) -> None: + self.dpi = None + self.ass = None + self.lang = None + self.type = None + self.code = None + self.source = None + + def __init__(self, file_name): + self.name = RSSInfoCleaner.Name() + self.info = RSSInfoCleaner.Info() + self.tag = RSSInfoCleaner.Tag() + self.file_name = file_name + self.name.raw = file_name # 接收文件名参数 + self.clean() # 清理广告等杂质 + # 匹配特征等 + with open("RssFilter/clean_rule.json", encoding="utf-8") as file_obj: rule_json = json.load(file_obj)[0]["group_name"] - self.group_rule = [zhconv.convert(x, "zh-cn") for x in rule_json] + const["group_rule"] = [zhconv.convert(x, "zh-cn") for x in rule_json] self.file_info = {} self.pre_analyse = None # 匹配字幕组特征 self.recognize_group() - self.Info.group = self.get_group() - self.Tag.dpi = self.get_dpi() - self.Info.season = self.get_season() - self.Info.episode = self.get_episode() - self.Info.vision = self.get_vision() - self.Tag.lang = self.get_language() - self.Tag.ass = self.get_ass() - self.Tag.type = self.get_type() - self.Tag.code = self.get_code() - self.Tag.source = self.get_source() - self.Name.clean = self.get_clean_name() + self.info.group = self.get_group() + self.tag.dpi = self.get_dpi() + self.info.season = self.get_season() + self.info.episode = self.get_episode() + self.info.vision = self.get_vision() + self.tag.lang = self.get_language() + self.tag.ass = self.get_ass() + self.tag.type = self.get_type() + self.tag.code = self.get_code() + self.tag.source = self.get_source() + self.name.clean = self.get_clean_name() self.zh_list = [] self.jp_list = [] self.en_list = [] @@ -136,7 +143,7 @@ class RSSInfoCleaner: # 清理原链接(中文字符替换为英文) def clean(self): - file_name = zhconv.convert(self.Name.raw, "zh-cn") + file_name = zhconv.convert(self.name.raw, "zh-cn") # 去广告 file_name = re.sub( "[((\[【]?(字幕)?[\u4e00-\u9fa5、]{0,3}(新人|招募?新?)[\u4e00-\u9fa5、]{0,8}[))\]】]?", @@ -198,7 +205,7 @@ class RSSInfoCleaner: f_res.group(1), "%s/" % f_res.group(1).strip(".") ) - self.Name.raw = ( + self.name.raw = ( str(file_name) .replace(":", ":") .replace("【", "[") @@ -215,11 +222,9 @@ class RSSInfoCleaner: # 检索字幕组特征 def recognize_group(self): - character = self.group_character - group = self.group_char - rule = self.group_rule + rule = const["group_rule"] # 字幕组(特例)特征优先级大于通用特征 - character = group + character + character = const["all_charactor"] # !强规则,人工录入标准名,区分大小写,优先匹配 for char in rule: if ("&" + char) in self.file_name or (char + "&") in self.file_name: @@ -237,8 +242,8 @@ class RSSInfoCleaner: self.pre_analyse = char.lower() return "enforce" # 如果文件名以 [字幕组名] 开头 - if self.Name.raw[0] == "[": - str_split = self.Name.raw.lower().split("]") + if self.name.raw[0] == "[": + str_split = self.name.raw.lower().split("]") # 检索特征值是否位于文件名第1、2、最后一段 for char in character: if ( @@ -259,16 +264,16 @@ class RSSInfoCleaner: self.pre_analyse = None return False # 文件名以 -字幕组名 结尾 - elif "-" in self.Name.raw: + elif "-" in self.name.raw: for char in character: - if char in self.Name.raw.lower().split("-")[-1]: - self.pre_analyse = self.Name.raw.lower().split("-")[-1] + if char in self.name.raw.lower().split("-")[-1]: + self.pre_analyse = self.name.raw.lower().split("-")[-1] return "reserve" self.pre_analyse = None return False # 文件名以空格分隔 字幕组名为第一段 else: - first_str = self.Name.raw.lower().split(" ")[0] + first_str = self.name.raw.lower().split(" ")[0] for char in character: if char in first_str: self.pre_analyse = first_str @@ -289,16 +294,16 @@ class RSSInfoCleaner: # 大部分情况 elif status == "success": # 如果是 [字幕组名] ,这么标准的格式直接else送走吧,剩下的匹配一下 - if "[%s]" % res_char not in self.Name.raw.lower(): - if self.Name.raw[0] == "[": + if "[%s]" % res_char not in self.name.raw.lower(): + if self.name.raw[0] == "[": try: # 以特征值为中心,匹配最近的中括号,八成就这个了 - gp = get_gp(res_char, self.Name.raw.lower()) + gp = get_gp(res_char, self.name.raw.lower()) return gp except Exception as e: logger.warning( "bug -- res_char:%s,%s,%s" - % (res_char, self.Name.raw.lower(), e) + % (res_char, self.name.raw.lower(), e) ) else: return res_char @@ -307,7 +312,7 @@ class RSSInfoCleaner: # 扒了6W数据,硬找的参数,没啥说的 def get_dpi(self): - file_name = self.Name.raw + file_name = self.name.raw dpi_list = [ "4k", "2160p", @@ -356,7 +361,7 @@ class RSSInfoCleaner: # 获取语种 def get_language(self): - file_name = self.Name.raw + file_name = self.name.raw lang = [] # 中文标示 try: @@ -394,7 +399,7 @@ class RSSInfoCleaner: # 文件种类 def get_type(self): - file_name = self.Name.raw + file_name = self.name.raw type_list = [] # 英文标示 try: @@ -415,7 +420,7 @@ class RSSInfoCleaner: # 编码格式 def get_code(self): - file_name = self.Name.raw + file_name = self.name.raw code = [] # 视频编码 try: @@ -447,7 +452,7 @@ class RSSInfoCleaner: # 来源 def get_source(self): - file_name = str(self.Name.raw).lower() + file_name = str(self.name.raw).lower() type_list = [] # 英文标示 for _ in range(3): @@ -474,7 +479,7 @@ class RSSInfoCleaner: # 获取季度 def get_season(self): - file_name = self.Name.raw.lower() + file_name = self.name.raw.lower() season = [] # 中文标示 try: @@ -504,7 +509,7 @@ class RSSInfoCleaner: # 获取集数 def get_episode(self): - file_name = self.Name.raw.lower() + file_name = self.name.raw.lower() episode = [] # _集,国漫 try: @@ -562,7 +567,7 @@ class RSSInfoCleaner: # 获取版本 def get_vision(self): - file_name = self.Name.raw.lower() + file_name = self.name.raw.lower() vision = [] # 中文 try: @@ -596,7 +601,7 @@ class RSSInfoCleaner: # 获取字幕类型 def get_ass(self): - file_name = self.Name.raw.lower() + file_name = self.name.raw.lower() ass = [] # 中文标示 try: @@ -639,7 +644,7 @@ class RSSInfoCleaner: elif has_zh(k_i) and has_en(k_i): # 如果还是同时包含中英文的情况,递龟一下 if " " not in k_i: - res = re.search(k_i, self.Name.raw.lower()) + res = re.search(k_i, self.name.raw.lower()) if res is not None: zh_list.append(res.group()) else: @@ -658,7 +663,7 @@ class RSSInfoCleaner: elif has_en(k_i) is False: zh_list.append(k_i.strip(" ")) elif has_zh(k_i) and has_en(k_i): - res = re.search(k_i, self.Name.raw.lower()) + res = re.search(k_i, self.name.raw.lower()) if res is not None: zh_list.append(res.group()) @@ -678,19 +683,19 @@ class RSSInfoCleaner: def get_clean_name(self): # 获取到的信息 info = { - "group": self.Info.group, - "dpi": self.Tag.dpi, - "season": self.Info.season, - "episode": self.Info.episode, - "vision": self.Info.vision, - "lang": self.Tag.lang, - "ass": self.Tag.ass, - "type": self.Tag.type, - "code": self.Tag.code, - "source": self.Tag.source, + "group": self.info.group, + "dpi": self.tag.dpi, + "season": self.info.season, + "episode": self.info.episode, + "vision": self.info.vision, + "lang": self.tag.lang, + "ass": self.tag.ass, + "type": self.tag.type, + "code": self.tag.code, + "source": self.tag.source, } # 字母全部小写 - clean_name = self.Name.raw.lower() + clean_name = self.name.raw.lower() # 去除拿到的有效信息 for k, v in info.items(): @@ -743,108 +748,108 @@ class RSSInfoCleaner: # 提取标题 def get_title(self): - self.Name.zh, self.Name.en, self.Name.jp = None, None, None + self.name.zh, self.name.en, self.name.jp = None, None, None # 国漫筛选 - if "国漫" in self.Name.raw: + if "国漫" in self.name.raw: zh = re.search( - "-?([\u4e00-\u9fa5]{2,10})_?", self.Name.raw.replace("[国漫]", "") + "-?([\u4e00-\u9fa5]{2,10})_?", self.name.raw.replace("[国漫]", "") ) if zh is not None: - self.Name.zh = clean_list([zh.group()]) + self.name.zh = clean_list([zh.group()]) return - if "/" not in self.Name.clean: - if has_jp(self.Name.clean) is False: - if has_zh(self.Name.clean) is False: - en = re.search(self.Name.clean, self.Name.raw.lower()) + if "/" not in self.name.clean: + if has_jp(self.name.clean) is False: + if has_zh(self.name.clean) is False: + en = re.search(self.name.clean, self.name.raw.lower()) if en is not None: - self.Name.en = clean_list([en.group()]) + self.name.en = clean_list([en.group()]) return elif ( re.search( "(^[\u4e00-\u9fa5\u3040-\u31ff\d:\-·??、.。,!]{1,20}[a-z\d]{,3} ?!?)([a-z\d:\-.。,,!! ]* ?)", - self.Name.clean, + self.name.clean, ) is not None ): res = re.search( "(^[\u4e00-\u9fa5\u3040-\u31ff\d:\-·??、.。,!]{1,20}[a-z\d]{,3} ?!?)[._&]?([a-z\d:\-.。,,!! ]* ?)", - self.Name.clean, + self.name.clean, ) zh = res.group(1) en = res.group(2) - zh = re.search(zh, self.Name.raw.lower()) + zh = re.search(zh, self.name.raw.lower()) if zh is not None: - self.Name.zh = clean_list([zh.group()]) - en = re.search(en, self.Name.raw.lower()) + self.name.zh = clean_list([zh.group()]) + en = re.search(en, self.name.raw.lower()) if en is not None: - self.Name.en = clean_list([en.group()]) + self.name.en = clean_list([en.group()]) return # 英中 elif ( re.search( "(^([a-z\d:\-_.。,,!! ]* ?) ?)[._&]?([\u4e00-\u9fa5\u3040-\u31ffa-z\d:\-_·??、.。,!! ]{1,20})", - self.Name.clean, + self.name.clean, ) is not None ): res = re.search( "(^([a-z\d:\-_.。,,!! ]* ?) ?)[._&]?([\u4e00-\u9fa5\u3040-\u31ffa-z\d:\-_·??、.。,!! ]{1,20})", - self.Name.clean, + self.name.clean, ) zh = res.group(3) en = res.group(1) - zh = re.search(zh, self.Name.raw.lower()) + zh = re.search(zh, self.name.raw.lower()) if zh is not None: - self.Name.zh = clean_list([zh.group()]) - en = re.search(en, self.Name.raw.lower()) + self.name.zh = clean_list([zh.group()]) + en = re.search(en, self.name.raw.lower()) if en is not None: - self.Name.en = clean_list([en.group()]) + self.name.en = clean_list([en.group()]) return - elif len(re.findall("[a-zA-Z]", self.Name.clean.lower())) < 10: - zh = re.search(self.Name.clean, self.Name.raw.lower()) + elif len(re.findall("[a-zA-Z]", self.name.clean.lower())) < 10: + zh = re.search(self.name.clean, self.name.raw.lower()) if zh is not None: - self.Name.zh = clean_list([zh.group()]) + self.name.zh = clean_list([zh.group()]) return if debug > 0: print("初筛:\r\n%s\r\n%s\r\n%s" % (self.zh_list, self.en_list, self.jp_list)) - if (has_zh(self.Name.clean) or has_jp(self.Name.clean)) and has_en( - self.Name.clean + if (has_zh(self.name.clean) or has_jp(self.name.clean)) and has_en( + self.name.clean ): - self.Name.clean = add_separator(self.Name.clean) - self.easy_split(self.Name.clean, self.zh_list, self.en_list, self.jp_list) + self.name.clean = add_separator(self.name.clean) + self.easy_split(self.name.clean, self.zh_list, self.en_list, self.jp_list) if debug > 0: print("二筛:\r\n%s\r\n%s\r\n%s" % (self.zh_list, self.en_list, self.jp_list)) # 结果反代入原名验证 - self.all_verity([self.Name.raw, self.Name.clean]) + self.all_verity([self.name.raw, self.name.clean]) # 去除正确结果后,重新识别其他部分 if self.jp_list: - temp_name = del_rules(self.Name.clean, self.jp_list) + temp_name = del_rules(self.name.clean, self.jp_list) self.easy_split(temp_name, self.zh_list, self.en_list, self.jp_list) if self.zh_list and self.en_list == []: - temp_name = del_rules(self.Name.clean, self.zh_list) + temp_name = del_rules(self.name.clean, self.zh_list) self.easy_split(temp_name, self.zh_list, self.en_list, self.jp_list) elif self.zh_list == [] and self.en_list: - temp_name = del_rules(self.Name.clean, self.en_list) + temp_name = del_rules(self.name.clean, self.en_list) self.easy_split(temp_name, self.zh_list, self.en_list, self.jp_list) while "" in self.en_list: self.en_list.remove("") if debug > 0: print("三筛:\r\n%s\r\n%s\r\n%s" % (self.zh_list, self.en_list, self.jp_list)) # 一步一验 - self.all_verity([self.Name.raw, self.Name.clean]) + self.all_verity([self.name.raw, self.name.clean]) for _ in range(5): # 拼合碎片 - splicing(self.zh_list, self.zh_list, self.Name.clean) - splicing(self.en_list, self.en_list, self.Name.clean) - splicing(self.jp_list, self.jp_list, self.Name.clean) + splicing(self.zh_list, self.zh_list, self.name.clean) + splicing(self.en_list, self.en_list, self.name.clean) + splicing(self.jp_list, self.jp_list, self.name.clean) try: # 拼合中英文碎片 for i in self.en_list: for j in self.zh_list: - res = re.search("%s +%s" % (i, j), self.Name.raw.lower()) + res = re.search("%s +%s" % (i, j), self.name.raw.lower()) if res is not None: self.en_list.remove(i) self.zh_list.append(res.group()) @@ -853,16 +858,16 @@ class RSSInfoCleaner: if debug > 0: print("拼合:\r\n%s\r\n%s\r\n%s" % (self.zh_list, self.en_list, self.jp_list)) # 再次验证,这里只能验raw名 - self.all_verity(self.Name.raw) + self.all_verity(self.name.raw) # 灌装 - self.Name.zh = clean_list(self.zh_list) + self.name.zh = clean_list(self.zh_list) bug_list = ["不白吃话山海经"] for i in bug_list: - if i in self.Name.raw.lower(): + if i in self.name.raw.lower(): if has_zh(i): - self.Name.zh = [i] - self.Name.en = clean_list(self.en_list) - self.Name.jp = clean_list(self.jp_list) + self.name.zh = [i] + self.name.en = clean_list(self.en_list) + self.name.jp = clean_list(self.jp_list) if __name__ == "__main__": @@ -873,7 +878,8 @@ if __name__ == "__main__": row = 1 if debug else 200 name_list = read_data("mikan", num, row) for i in range(0, len(name_list)): - title = RSSInfoCleaner(name_list[i]).Name + info = RSSInfoCleaner(name_list[i]) + title = info.Name print("%s:%s" % (num + i, name_list[i])) print("raw_name:%s" % title.raw) print("clean_name:%s" % title.clean) diff --git a/AutoBangumi/app/RssFilter/__init__.py b/AutoBangumi/app/RssFilter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/AutoBangumi/config/clean_rule.json b/AutoBangumi/app/RssFilter/clean_rule.json similarity index 100% rename from AutoBangumi/config/clean_rule.json rename to AutoBangumi/app/RssFilter/clean_rule.json diff --git a/AutoBangumi/app/__init__.py b/AutoBangumi/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/AutoBangumi/app/app.py b/AutoBangumi/app/app.py index 144461b9..0f71850f 100644 --- a/AutoBangumi/app/app.py +++ b/AutoBangumi/app/app.py @@ -1,35 +1,30 @@ import os import time -import json import logging from collect_info import CollectRSS from set_rule import SetRule from rename_qb import qBittorrentRename -from env import EnvInfo - - -def setup_logger(): - DATE_FORMAT = "%Y-%m-%d %X" - LOGGING_FORMAT = "%(asctime)s %(levelname)s: %(message)s" - logging.basicConfig( - level=logging.DEBUG, - datefmt=DATE_FORMAT, - format=LOGGING_FORMAT, - encoding="utf-8", - ) +from conf import settings +from argument_parser import parse +from log import setup_logger +from utils import json_config def create_data_file(): - if not os.path.exists(EnvInfo.info_path): + if not os.path.exists(settings.info_path): bangumi_info = {"rss_link": "", "bangumi_info": []} - with open(EnvInfo.info_path, "w") as i: - json.dump( - bangumi_info, i, indent=4, separators=(",", ": "), ensure_ascii=False - ) + json_config.save(settings.info_path, bangumi_info) if __name__ == "__main__": + args = parse() + if args.debug: + from const_dev import DEV_SETTINGS + + settings.init(DEV_SETTINGS) + else: + settings.init() setup_logger() create_data_file() SetRule().rss_feed() @@ -37,4 +32,4 @@ if __name__ == "__main__": CollectRSS().run() SetRule().run() qBittorrentRename().run() - time.sleep(EnvInfo.sleep_time) + time.sleep(settings.sleep_time) diff --git a/AutoBangumi/app/argument_parser.py b/AutoBangumi/app/argument_parser.py new file mode 100644 index 00000000..8e4b15c9 --- /dev/null +++ b/AutoBangumi/app/argument_parser.py @@ -0,0 +1,15 @@ +import argparse + + +def parse(): + parser = argparse.ArgumentParser( + prog="Auto Bangumi", + description=""" + 本项目是基于 Mikan Project、qBittorrent 的全自动追番整理下载工具。 + 只需要在 Mikan Project 上订阅番剧,就可以全自动追番。 + 并且整理完成的名称和目录可以直接被 Plex、Jellyfin 等媒体库软件识别, + 无需二次刮削。""", + ) + + parser.add_argument("-d", "--debug",action="store_true", help="debug mode") + return parser.parse_args() diff --git a/AutoBangumi/app/collect_info.py b/AutoBangumi/app/collect_info.py index c9ca6fc1..58c217d5 100644 --- a/AutoBangumi/app/collect_info.py +++ b/AutoBangumi/app/collect_info.py @@ -5,8 +5,10 @@ import requests from bs4 import BeautifulSoup import json import re -from env import EnvInfo, BColors -from RSSFilter import RSSInfoCleaner as Filter +from conf import settings +from utils import json_config + +# from RssFilter.RSSFilter import RSSInfoCleaner as Filter logger = logging.getLogger(__name__) @@ -23,32 +25,27 @@ class MatchRule: class CollectRSS: def __init__(self): self.bangumi_list = [] - with open(EnvInfo.rule_path, encoding="utf-8") as r: - self.rules = json.load(r) + self.rules = json_config.load(settings.rule_path) try: - self.rules = requests.get(EnvInfo.rule_url).json() - with open(EnvInfo.rule_path, "w", encoding="utf-8") as f: - json.dump( - self.rules, f, indent=4, separators=(",", ": "), ensure_ascii=False - ) - except: - with open(EnvInfo.rule_path, encoding="utf-8") as r: - self.rules = json.load(r) + self.rules = requests.get(settings.rule_url).json() + except Exception as e: + logger.exception(e) + json_config.save(settings.rule_path, self.rules) try: - rss = requests.get(EnvInfo.rss_link, "utf-8") - except: - logger.debug("ERROR with DNS/Connection.") + rss = requests.get(settings.rss_link, "utf-8") + except Exception as e: + logger.exception(e) + logger.error("ERROR with DNS/Connection.") quit() soup = BeautifulSoup(rss.text, "xml") self.items = soup.find_all("item") - with open(EnvInfo.info_path, encoding="utf-8") as i: - self.info = json.load(i) + self.info = json_config.load(settings.info_path) def get_info_list(self): for item in self.items: name = item.title.string # debug 用 - if EnvInfo.get_rule_debug: + if settings.get_rule_debug: logger.debug(f"Raw {name}") exit_flag = False for rule in self.rules: @@ -64,8 +61,7 @@ class CollectRSS: bangumi_title = n[rule["name_position"]].strip() except IndexError: continue - sub_title = re.sub( - MatchRule.sub_title, "", bangumi_title) + sub_title = re.sub(MatchRule.sub_title, "", bangumi_title) b = re.split(r"\/|\_", sub_title) while "" in b: b.remove("") @@ -78,8 +74,7 @@ class CollectRSS: ) if match_obj is not None: bangumi_title = match_obj.group(1).strip() - match_obj = re.match( - MatchRule.match_rule, bangumi_title, re.I) + match_obj = re.match(MatchRule.match_rule, bangumi_title, re.I) if match_obj is not None: bangumi_title = match_obj.group(2).strip() if bangumi_title not in self.bangumi_list: @@ -97,11 +92,11 @@ class CollectRSS: def put_info_json(self): had_data = [] - if self.info["rss_link"] == EnvInfo.rss_link: + if self.info["rss_link"] == settings.rss_link: for data in self.info["bangumi_info"]: had_data.append(data["title"]) else: - self.info = {"rss_link": EnvInfo.rss_link, "bangumi_info": []} + self.info = {"rss_link": settings.rss_link, "bangumi_info": []} for item in self.bangumi_list: title = item["title"] match_title_season = re.match(MatchRule.season_match, title, re.I) @@ -134,10 +129,7 @@ class CollectRSS: ) had_data.append(json_title) logger.debug("add {json_title} {json_season}") - with open(EnvInfo.info_path, "w", encoding="utf8") as f: - json.dump( - self.info, f, indent=4, separators=(",", ": "), ensure_ascii=False - ) + json_config.save(settings.info_path, self.info) def run(self): self.get_info_list() @@ -145,14 +137,15 @@ class CollectRSS: if __name__ == "__main__": - # rss = requests.get(EnvInfo.rss_link, 'utf-8') + # from const import BCOLORS + # rss = requests.get(settings.rss_link, 'utf-8') # soup = BeautifulSoup(rss.text, 'xml') # items = soup.find_all('item') # for item in items: # name = item.title.string # pn = Filter(name).Name - # print(BColors.HEADER + name) - # print(BColors.OKGREEN + str(pn.zh)) + # print(BCOLORS.HEADER + name) + # print(BCOLORS.OKGREEN + str(pn.zh)) # print(str(pn.en)) print(__file__) print(os.path.dirname(__file__)) diff --git a/AutoBangumi/app/conf.py b/AutoBangumi/app/conf.py new file mode 100644 index 00000000..ec7e626c --- /dev/null +++ b/AutoBangumi/app/conf.py @@ -0,0 +1,35 @@ +import os + +import const + +class Settings(dict): + def __getattr__(self, item): + return self.get(item) + + def __setattr__(self, key, value): + self[key] = value + + def init(self, args=None): + self.update(self._settings_from_env()) + if args: + self.update(args) + + def _val_from_env(self, env, attr): + """Transforms env-strings to python.""" + val = os.environ[env] + if isinstance(attr, tuple): + conv_func = attr[1] + val = conv_func(val) + return val + + + def _settings_from_env(self): + """Loads settings from env.""" + return { + attr: self._val_from_env(env, attr) + for env, attr in const.ENV_TO_ATTR.items() + if env in os.environ + } + + +settings = Settings(const.DEFAULT_SETTINGS) diff --git a/AutoBangumi/app/const.py b/AutoBangumi/app/const.py new file mode 100644 index 00000000..81a98398 --- /dev/null +++ b/AutoBangumi/app/const.py @@ -0,0 +1,51 @@ +# -*- encoding: utf-8 -*- + +from math import fabs + + +DEFAULT_SETTINGS = { + "host_ip": "localhost:8080", + "sleep_time": 1800, + "user_name": "admin", + "password": "adminadmin", + "rss_link": "https://mikanani.me/RSS/classic", + "download_path": "/downloads/Bangumi", + "method": "pn", + "enable_group_tag": True, + "info_path": "config/bangumi.json", + "rule_path": "config/rule.json", + "not_contain": "720", + "get_rule_debug": False, + "rule_url": "https://raw.githubusercontent.com/EstrellaXD/Bangumi_Auto_Collector/main/AutoBangumi/config/rule.json", + "rule_name_re": r"\:|\/|\.", + "connect_retry_interval": 5, + "enable_eps_complete": False, +} + +ENV_TO_ATTR = { + "HOST": "host_ip", + "TIME": ("sleep_time", lambda e: float(e)), + "USER": "user_name", + "PASSWORD": "password", + "RSS": "rss_link", + "DOWNLOAD_PATH": "download_path", + "METHOD": "method", + "GROUP_TAG": ("enable_group_tag", lambda e: e.lower() in ("true", "1", "t")), + "NOT_CONTAIN": "not_contain", + "RULE_DEBUG": ("get_rule_debug", lambda e: e.lower() in ("true", "1", "t")), + "EP_COMPLETE": ("enable_eps_complete", lambda e: e.lower() in ("true", "1", "t")), +} + +FULL_SEASON_SUPPORT_GROUP = ["Lilith-Raws"] + +BCOLORS = { + "HEADER": "\033[95m", + "OKBLUE": "\033[94m", + "OKCYAN": "\033[96m", + "OKGREEN": "\033[92m", + "WARNING": "\033[93m", + "FAIL": "\033[91m", + "ENDC": "\033[0m", + "BOLD": "\033[1m", + "UNDERLINE": "\033[4m", +} diff --git a/AutoBangumi/app/const_dev.py b/AutoBangumi/app/const_dev.py new file mode 100644 index 00000000..82b6af1b --- /dev/null +++ b/AutoBangumi/app/const_dev.py @@ -0,0 +1,7 @@ +DEV_SETTINGS = { + "host_ip": "localhost:8181", + "sleep_time": 10, + "info_path": "../config/bangumi.json", + "rule_path": "../config/rule.json", + "enable_eps_complete": True, +} diff --git a/AutoBangumi/app/downloader/__init__.py b/AutoBangumi/app/downloader/__init__.py new file mode 100644 index 00000000..4fa6810e --- /dev/null +++ b/AutoBangumi/app/downloader/__init__.py @@ -0,0 +1,10 @@ +from conf import settings + +def getClient(): + host=settings.host_ip + username=settings.user_name + password=settings.password + # TODO 多下载器支持 + # 从 settings 里读取下载器名称,然后返回对应 Client + from downloader.qb_downloader import QbDownloader + return QbDownloader(host, username, password) \ No newline at end of file diff --git a/AutoBangumi/app/downloader/exceptions.py b/AutoBangumi/app/downloader/exceptions.py new file mode 100644 index 00000000..ba0d18a4 --- /dev/null +++ b/AutoBangumi/app/downloader/exceptions.py @@ -0,0 +1,2 @@ +class ConflictError(Exception): + pass \ No newline at end of file diff --git a/AutoBangumi/app/downloader/qb_downloader.py b/AutoBangumi/app/downloader/qb_downloader.py new file mode 100644 index 00000000..eecdc4ea --- /dev/null +++ b/AutoBangumi/app/downloader/qb_downloader.py @@ -0,0 +1,60 @@ +import logging +import time + +from qbittorrentapi import Client, LoginFailed +from qbittorrentapi.exceptions import Conflict409Error +from tomlkit import item + +from conf import settings + +from downloader.exceptions import ConflictError + +logger = logging.getLogger(__name__) + + +class QbDownloader: + def __init__(self, host, username, password): + self._client = Client( + host=host, + username=username, + password=password, + ) + while True: + try: + self._client.auth_log_in() + break + except LoginFailed: + logger.warning( + f"Can't log in qBittorrent Server {host} by {username}, retry in {settings.connect_retry_interval}" + ) + time.sleep(settings.connect_retry_interval) + + def torrents_info(self, status_filter, category): + return self._client.torrents_info(status_filter, category) + + def torrents_add(self, urls, save_path, category): + return self._client.torrents_add( + urls=urls, + save_path=save_path, + category=category, + ) + + def torrents_rename_file(self, torrent_hash, old_path, new_path): + self._client.torrents_rename_file(torrent_hash=torrent_hash, old_path=old_path, new_path=new_path) + + def rss_add_feed(self, url, item_path): + try: + self._client.rss_add_feed(url, item_path) + except Conflict409Error as e: + logger.exception(e) + raise ConflictError() + + def rss_remove_item(self, item_path): + try: + self._client.rss_remove_item(item_path) + except Conflict409Error as e: + logger.exception(e) + raise ConflictError() + + def rss_set_rule(self, rule_name, rule_def): + self._client.rss_set_rule(rule_name, rule_def) diff --git a/AutoBangumi/app/env.py b/AutoBangumi/app/env.py deleted file mode 100644 index bf9b9aec..00000000 --- a/AutoBangumi/app/env.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -import time -from datetime import datetime - - -class EnvInfo: - debug_mode = True - # Docker Env - if not debug_mode: - host_ip = os.environ["HOST"] - sleep_time = float(os.environ["TIME"]) - user_name = os.environ["USER"] - password = os.environ["PASSWORD"] - rss_link = os.environ["RSS"] - download_path = os.environ["DOWNLOAD_PATH"] - method = os.environ["METHOD"] - enable_group_tag = os.getenv("GROUP_TAG", 'False').lower() in ('true', '1', 't') - info_path = "/config/bangumi.json" - rule_path = "/config/clean_rule.json" - not_contain = os.environ["NOT_CONTAIN"] - get_rule_debug = os.getenv("RULE_DEBUG", 'False').lower() in ('true', '1', 't') - enable_eps_complete = os.getenv("EP_COMPLETE", 'False').lower() in ('true', '1', 't') - else: - # Debug ENV - host_ip = "localhost:8181" - sleep_time = 10 - user_name = "admin" - password = "adminadmin" - rss_link = "https://mikanani.me/RSS/classic" - download_path = "/downloads/Bangumi" - method = "pn" - enable_group_tag = True - info_path = "../config/bangumi.json" - rule_path = "../config/rule_beta.json" - not_contain = "720" - get_rule_debug = True - enable_eps_complete = True - # Static ENV - rule_url = "https://raw.githubusercontent.com/EstrellaXD/Bangumi_Auto_Collector/main/AutoBangumi/config/rule.json" - - rule_name_re = r"\:|\/|\." - - -class BColors: - HEADER = '\033[95m' - OKBLUE = '\033[94m' - OKCYAN = '\033[96m' - OKGREEN = '\033[92m' - WARNING = '\033[93m' - FAIL = '\033[91m' - ENDC = '\033[0m' - BOLD = '\033[1m' - UNDERLINE = '\033[4m' - - -class Other: - full_season_support_group = [ - "Lilith-Raws" - ] diff --git a/AutoBangumi/app/eps_complete.py b/AutoBangumi/app/eps_complete.py index 7cc58576..9f70d158 100644 --- a/AutoBangumi/app/eps_complete.py +++ b/AutoBangumi/app/eps_complete.py @@ -1,10 +1,13 @@ import os.path import requests from qbittorrentapi import Client -from env import EnvInfo, Other from bs4 import BeautifulSoup import logging +from conf import settings +from const import FULL_SEASON_SUPPORT_GROUP +from downloader import getClient + logger = logging.getLogger(__name__) @@ -14,6 +17,7 @@ class FullSeasonGet: self.bangumi_name = bangumi_name self.group = group self.season = season + self.client = getClient() def get_season_rss(self): if self.season == "S01": @@ -21,30 +25,23 @@ class FullSeasonGet: else: season = self.season season = requests.get( - f"https://mikanani.me/RSS/Search?searchstr={self.group}+{self.bangumi_name}+{season}") + f"https://mikanani.me/RSS/Search?searchstr={self.group}+{self.bangumi_name}+{season}" + ) soup = BeautifulSoup(season.content, "xml") self.torrents = soup.find_all("enclosure") def add_torrents(self): - qb = Client( - host=EnvInfo.host_ip, username=EnvInfo.user_name, password=EnvInfo.password - ) - try: - qb.auth_log_in() - except: - logger.error("Error") for torrent in self.torrents: - qb.torrents_add( + self.client.torrents_add( urls=torrent["url"], save_path=str( - os.path.join(EnvInfo.download_path, - self.bangumi_name, self.season) + os.path.join(settings.download_path, self.bangumi_name, self.season) ), category="Bangumi", ) def run(self): - if self.group in Other.full_season_support_group: + if self.group in FULL_SEASON_SUPPORT_GROUP: self.get_season_rss() self.add_torrents() diff --git a/AutoBangumi/app/log.py b/AutoBangumi/app/log.py new file mode 100644 index 00000000..f0918bf0 --- /dev/null +++ b/AutoBangumi/app/log.py @@ -0,0 +1,14 @@ +import logging + + +def setup_logger(): + DATE_FORMAT = "%Y-%m-%d %X" + LOGGING_FORMAT = "%(asctime)s %(levelname)s: %(message)s" + logging.basicConfig( + level=logging.DEBUG, + datefmt=DATE_FORMAT, + format=LOGGING_FORMAT, + encoding="utf-8", + ) + +setup_logger() \ No newline at end of file diff --git a/AutoBangumi/app/parser/parser.py b/AutoBangumi/app/parser/parser.py new file mode 100644 index 00000000..ee4b227e --- /dev/null +++ b/AutoBangumi/app/parser/parser.py @@ -0,0 +1,4 @@ +class Parser: + def parse(filename): + # TODO 番剧名称识别 + pass diff --git a/AutoBangumi/app/rename_qb.py b/AutoBangumi/app/rename_qb.py index 474c2da1..b60b33b2 100644 --- a/AutoBangumi/app/rename_qb.py +++ b/AutoBangumi/app/rename_qb.py @@ -2,21 +2,17 @@ import re import qbittorrentapi import logging -from env import EnvInfo +from downloader import getClient + +from conf import settings logger = logging.getLogger(__name__) class qBittorrentRename: def __init__(self): - self.qbt_client = qbittorrentapi.Client( - host=EnvInfo.host_ip, username=EnvInfo.user_name, password=EnvInfo.password - ) - try: - self.qbt_client.auth_log_in() - except qbittorrentapi.LoginFailed as e: - logger.exception(e) - self.recent_info = self.qbt_client.torrents_info( + self.client = getClient() + self.recent_info = self.client.torrents_info( status_filter="completed", category="Bangumi" ) self.count = 0 @@ -55,7 +51,7 @@ class qBittorrentRename: def rename_torrent_file(self, hash, path_name, new_name): if path_name != new_name: - self.qbt_client.torrents_rename_file( + self.client.torrents_rename_file( torrent_hash=hash, old_path=path_name, new_path=new_name ) logger.debug(f"{path_name} >> {new_name}") @@ -68,7 +64,7 @@ class qBittorrentRename: def run(self): method_dict = {"pn": self.rename_pn, "normal": self.rename_normal} - if EnvInfo.method not in method_dict: + if settings.method not in method_dict: logger.error(f"error method") else: for i in range(0, self.torrent_count): @@ -77,7 +73,7 @@ class qBittorrentRename: name = info.name hash = info.hash path_name = info.content_path.split("/")[-1] - new_name = method_dict[EnvInfo.method](name) + new_name = method_dict[settings.method](name) self.rename_torrent_file(hash, path_name, new_name) except: logger.warning(f"{name} rename fail") diff --git a/AutoBangumi/app/set_rule.py b/AutoBangumi/app/set_rule.py index c0108fcd..da4cf712 100644 --- a/AutoBangumi/app/set_rule.py +++ b/AutoBangumi/app/set_rule.py @@ -1,63 +1,64 @@ import re import logging -from env import EnvInfo -import qbittorrentapi import json import os +from downloader import getClient +from downloader.exceptions import ConflictError + +from conf import settings +from utils import json_config + logger = logging.getLogger(__name__) class SetRule: def __init__(self): - with open(EnvInfo.info_path, encoding="utf-8") as f: - self.info = json.load(f) - self.bangumi_info = self.info["bangumi_info"] - self.rss_link = EnvInfo.rss_link - self.host_ip = EnvInfo.host_ip - self.user_name = EnvInfo.user_name - self.password = EnvInfo.password - self.download_path = EnvInfo.download_path - self.qb = qbittorrentapi.Client( - host=self.host_ip, username=self.user_name, password=self.password) - try: - self.qb.auth_log_in() - except qbittorrentapi.LoginFailed as e: - logger.exception(e) + self.info = json_config.load(settings.info_path) + self.bangumi_info = self.info["bangumi_info"] + self.rss_link = settings.rss_link + self.download_path = settings.download_path + self.client = getClient() def set_rule(self, bangumi_name, group, season): rule = { - 'enable': True, - 'mustContain': bangumi_name, - 'mustNotContain': EnvInfo.not_contain, - 'useRegx': True, - 'episodeFilter': '', - 'smartFilter': False, - 'previouslyMatchedEpisodes': [], - 'affectedFeeds': [self.rss_link], - 'ignoreDays': 0, - 'lastMatch': '', - 'addPaused': False, - 'assignedCategory': 'Bangumi', - 'savePath': str(os.path.join(EnvInfo.download_path, re.sub(EnvInfo.rule_name_re, " ", bangumi_name).strip(), season)) + "enable": True, + "mustContain": bangumi_name, + "mustNotContain": settings.not_contain, + "useRegx": True, + "episodeFilter": "", + "smartFilter": False, + "previouslyMatchedEpisodes": [], + "affectedFeeds": [self.rss_link], + "ignoreDays": 0, + "lastMatch": "", + "addPaused": False, + "assignedCategory": "Bangumi", + "savePath": str( + os.path.join( + settings.download_path, + re.sub(settings.rule_name_re, " ", bangumi_name).strip(), + season, + ) + ), } - if EnvInfo.enable_group_tag: + if settings.enable_group_tag: rule_name = f"[{group}] {bangumi_name}" else: rule_name = bangumi_name - self.qb.rss_set_rule(rule_name=rule_name, rule_def=rule) + self.client.rss_set_rule(rule_name=rule_name, rule_def=rule) def rss_feed(self): try: - self.qb.rss_remove_item(item_path="Mikan_RSS") - except qbittorrentapi.exceptions.Conflict409Error: + self.client.rss_remove_item(item_path="Mikan_RSS") + except ConflictError: logger.debug("No feed exists, starting adding feed.") try: - self.qb.rss_add_feed(url=self.rss_link, item_path="Mikan_RSS") + self.client.rss_add_feed(url=self.rss_link, item_path="Mikan_RSS") logger.debug("Successes adding RSS Feed.") except ConnectionError: logger.debug("Error with adding RSS Feed.") - except qbittorrentapi.exceptions.Conflict409Error: + except ConflictError: logger.debug("RSS Already exists.") def run(self): @@ -66,9 +67,7 @@ class SetRule: if not info["added"]: self.set_rule(info["title"], info["group"], info["season"]) info["added"] = True - with open(EnvInfo.info_path, 'w', encoding='utf8') as f: - json.dump(self.info, f, indent=4, separators=( - ',', ': '), ensure_ascii=False) + json_config.save(settings.info_path, self.info) logger.debug("Finished.") diff --git a/AutoBangumi/app/utils/__init__.py b/AutoBangumi/app/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/AutoBangumi/app/utils/json_config.py b/AutoBangumi/app/utils/json_config.py new file mode 100644 index 00000000..b6bdaf6e --- /dev/null +++ b/AutoBangumi/app/utils/json_config.py @@ -0,0 +1,12 @@ +import json + + +def load(filename): + with open(filename, "r", encoding="utf-8") as f: + return json.load(f) + + +def save(filename, obj): + with open(filename, "w", encoding="utf8") as f: + json.dump(obj, f, indent=4, separators=(",", ": "), ensure_ascii=False) + pass diff --git a/AutoBangumi/config/rule.json b/AutoBangumi/config/rule.json index 8f6a3031..b612d558 100644 --- a/AutoBangumi/config/rule.json +++ b/AutoBangumi/config/rule.json @@ -56,4 +56,4 @@ ], "name_position": 3 } -] +] \ No newline at end of file