请注意备份好配置,改错了可能会导致无法运行
5 | 12 | 13 |
5 | by Nriver
6 | Github 项目主页
7 |
快速管理qBittorrent的RSS订阅数据. 实时过滤匹配到的数据, 方便管理你的qb订阅. 可以和 Episode-ReName 联动实现自动改名.
5 |暂时没有找到相关的feed
') 94 | 95 | -------------------------------------------------------------------------------- /pyqt5-ver/ui/custom_tab_bar.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | from PyQt5 import QtCore 4 | from PyQt5.QtWidgets import QTabBar, QLineEdit 5 | 6 | import g 7 | 8 | 9 | class CustomTabBar(QTabBar): 10 | """可编辑的QTabBar""" 11 | 12 | def __init__(self, parent): 13 | QTabBar.__init__(self, parent) 14 | self.editor = QLineEdit(self) 15 | 16 | # 标签宽度 17 | self.setStyleSheet("QTabBar::tab {min-width: 120px;}") 18 | 19 | if platform.system() == 'Windows': 20 | # windows 特有的输入法bug, 必须要用Dialog才能切换输入法, 再设置成无边框模式就能看上去和Popup一样了 21 | self.editor.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.FramelessWindowHint) 22 | else: 23 | self.editor.setWindowFlags(QtCore.Qt.Popup) 24 | 25 | # 加上这个的话,只有回车才会使输入生效 26 | # self.editor.setFocusProxy(self) 27 | self.editor.editingFinished.connect(self.handleEditingFinished) 28 | self.editor.installEventFilter(self) 29 | # self.editor.activateWindow() 30 | 31 | def eventFilter(self, widget, event): 32 | if ((event.type() == QtCore.QEvent.MouseButtonPress and not self.editor.geometry().contains( 33 | event.globalPos())) or ( 34 | event.type() == QtCore.QEvent.KeyPress and event.key() == QtCore.Qt.Key_Escape)): 35 | self.editor.hide() 36 | return True 37 | return QTabBar.eventFilter(self, widget, event) 38 | 39 | def mouseDoubleClickEvent(self, event): 40 | index = self.tabAt(event.pos()) 41 | if index >= 0: 42 | self.editTab(index) 43 | 44 | def editTab(self, index): 45 | rect = self.tabRect(index) 46 | self.editor.setFixedSize(rect.size()) 47 | self.editor.move(self.parent().mapToGlobal(rect.topLeft())) 48 | self.editor.setText(self.tabText(index)) 49 | if not self.editor.isVisible(): 50 | self.editor.show() 51 | 52 | def handleEditingFinished(self): 53 | index = self.currentIndex() 54 | if index >= 0: 55 | self.editor.hide() 56 | self.setTabText(index, self.editor.text()) 57 | g.data_groups[index]['name'] = self.editor.text() 58 | -------------------------------------------------------------------------------- /pyqt5-ver/ui/search_window.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtGui 2 | from PyQt5.QtCore import Qt 3 | from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QTabWidget, QLineEdit 4 | from loguru import logger 5 | 6 | from utils.path_util import resource_path 7 | 8 | 9 | class SearchWindow(QWidget): 10 | 11 | def __init__(self, parent): 12 | super().__init__() 13 | self.parent = parent 14 | # 记录上一次搜索的关键字 用于多个搜索结果跳转 15 | self.last_search_keyword = '' 16 | # 记录上一次搜索的结果位置 17 | self.last_search_index = 0 18 | # 检查数据是否有更新, 如果有更新, 需要重新搜索 19 | self.last_data_update_timestamp = None 20 | self.search_result = [] 21 | 22 | self.current_tab = 0 23 | self.last_tab = 0 24 | 25 | self.setWindowIcon(QtGui.QIcon(resource_path('QBRssManager.ico'))) 26 | 27 | self.tabs = QTabWidget() 28 | # self.tabs.resize(300, 200) 29 | 30 | # 搜索tab 31 | self.lineEdit = QLineEdit() 32 | self.lineEdit.setPlaceholderText('输入搜索关键字...') 33 | do_search_button = QPushButton("搜索") 34 | do_search_button.clicked.connect(self.parent.do_search) 35 | tab = QWidget() 36 | tab.layout = QVBoxLayout(self) 37 | tab.layout.addWidget(self.lineEdit) 38 | tab.layout.addWidget(do_search_button) 39 | tab.setLayout(tab.layout) 40 | self.tabs.addTab(tab, "搜索") 41 | 42 | # 替换tab 43 | self.lineEditReplaceSource = QLineEdit() 44 | self.lineEditReplaceSource.setPlaceholderText('输入搜索关键字...') 45 | self.lineEditReplaceTarget = QLineEdit() 46 | self.lineEditReplaceTarget.setPlaceholderText('替换为...') 47 | do_search_button2 = QPushButton("搜索") 48 | do_search_button2.clicked.connect(self.parent.do_search) 49 | do_replace_button = QPushButton("替换") 50 | do_replace_button.clicked.connect(self.parent.do_replace) 51 | do_replace_all_button = QPushButton("全部替换") 52 | do_replace_all_button.clicked.connect(self.parent.do_replace_all) 53 | tab = QWidget() 54 | tab.layout = QVBoxLayout(self) 55 | tab.layout.addWidget(self.lineEditReplaceSource) 56 | tab.layout.addWidget(self.lineEditReplaceTarget) 57 | tab.layout.addWidget(do_search_button2) 58 | tab.layout.addWidget(do_replace_button) 59 | tab.layout.addWidget(do_replace_all_button) 60 | tab.setLayout(tab.layout) 61 | self.tabs.addTab(tab, "替换") 62 | 63 | # 绑定tab切换事件 64 | self.tabs.currentChanged.connect(self.parent.search_tab_change) 65 | 66 | layout = QVBoxLayout() 67 | layout.addWidget(self.tabs) 68 | self.setLayout(layout) 69 | 70 | # 这个是搜索输入框 切换tab时跟据index把之前的数据带过来覆盖 71 | self.text_edit_list = [self.lineEdit, self.lineEditReplaceSource] 72 | 73 | self.setWindowTitle("搜索和替换") 74 | 75 | flags = Qt.WindowFlags() 76 | # 窗口永远在最前面 77 | flags |= Qt.WindowStaysOnTopHint 78 | self.setWindowFlags(flags) 79 | 80 | # 按键绑定 81 | self.keyPressEvent = self.handle_key_press 82 | 83 | self.resize(250, 100) 84 | 85 | def closeEvent(self, event): 86 | # 搜索窗口的关闭按钮事件 87 | logger.info('关闭搜索窗口') 88 | self.parent.text_browser.clear() 89 | 90 | def handle_key_press(self, event): 91 | if event.key() in (Qt.Key_Enter, Qt.Key_Return): 92 | logger.info('搜索') 93 | self.parent.do_search() 94 | elif event.key() in (Qt.Key_Escape,): 95 | self.close() 96 | elif event.key() == Qt.Key_F and (event.modifiers() & Qt.ControlModifier): 97 | self.tabs.setCurrentIndex(0) 98 | elif event.key() == Qt.Key_H and (event.modifiers() & Qt.ControlModifier): 99 | self.tabs.setCurrentIndex(1) 100 | -------------------------------------------------------------------------------- /pyqt5-ver/ui/tray_icon.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from PyQt5 import QtGui, QtCore 4 | from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction 5 | from PyQt5.uic.properties import QtCore 6 | from loguru import logger 7 | 8 | from utils.path_util import resource_path 9 | 10 | 11 | class TrayIcon(QSystemTrayIcon): 12 | 13 | def __init__(self, parent=None): 14 | super(TrayIcon, self).__init__(parent) 15 | self.showMenu() 16 | self.activated.connect(self.iconClicked) 17 | self.setIcon(QtGui.QIcon(resource_path('QBRssManager.ico'))) 18 | 19 | def showMenu(self): 20 | self.menu = QMenu() 21 | 22 | self.showWindowAction = QAction("显示程序窗口", self, triggered=self.show_main_window) 23 | self.quitAction = QAction("退出", self, triggered=self.quit) 24 | 25 | self.menu.addAction(self.showWindowAction) 26 | self.menu.addAction(self.quitAction) 27 | 28 | self.setContextMenu(self.menu) 29 | 30 | def iconClicked(self, reason): 31 | # 1是表示单击右键 32 | # 2是双击 33 | # 3是单击左键 34 | # 4是用鼠标中键点击 35 | if reason in (2, 3, 4): 36 | pw = self.parent() 37 | if pw.isVisible(): 38 | pw.hide() 39 | else: 40 | pw.show() 41 | logger.info(reason) 42 | 43 | def show_main_window(self): 44 | self.parent().setWindowState(QtCore.Qt.WindowActive) 45 | self.parent().show() 46 | 47 | def quit(self): 48 | # 退出程序 49 | self.setVisible(False) 50 | sys.exit() 51 | -------------------------------------------------------------------------------- /pyqt5-ver/utils/data_util.py: -------------------------------------------------------------------------------- 1 | import g 2 | from g import headers 3 | 4 | 5 | def legacy_data_parser(): 6 | """旧数据解析""" 7 | result = [] 8 | return result 9 | 10 | 11 | def fill_up_list(data_list, row_count, col_count): 12 | # 补到 max_row_size 个数据 13 | if len(data_list) < g.config['max_row_size']: 14 | for _ in range(g.config['max_row_size'] - len(data_list)): 15 | data_list.append(['' for x in range(len(headers))]) 16 | -------------------------------------------------------------------------------- /pyqt5-ver/utils/path_util.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | 5 | 6 | def resource_path(relative_path): 7 | # 兼容pyinstaller的文件资源访问 8 | if hasattr(sys, '_MEIPASS'): 9 | return os.path.join(sys._MEIPASS, relative_path) 10 | return os.path.join(os.path.abspath('.'), relative_path) 11 | 12 | 13 | def format_path(s): 14 | return s.replace('\\', '/').replace('//', '/') 15 | 16 | 17 | def format_path_by_system(s): 18 | # 保存路径格式化 兼容linux路径 19 | # 由于有远程调用api的需求, 所以这里不能限制斜杠格式 20 | # 简单判断一下吧 21 | if not s: 22 | return '' 23 | if s[0] != '/': 24 | return format_path(s).replace('/', '\\') 25 | else: 26 | return format_path(s) 27 | 28 | def remove_tail_slash(s): 29 | """去除末尾斜杠""" 30 | return s.rstrip('/') 31 | 32 | 33 | def get_series_from_season_path(season_path): 34 | """ 35 | 修正系列名称获取 去掉结尾的年份 36 | 来自 Episode-ReName 项目, 做了一些修改 37 | """ 38 | season_path = remove_tail_slash(format_path(season_path)) 39 | try: 40 | series = os.path.basename(os.path.dirname(season_path)) 41 | pat = '\(\d{4}\)$' 42 | res = re.search(pat, series) 43 | if res: 44 | year = res[0][1:-1] 45 | series = series[:-6].strip() 46 | else: 47 | year = '' 48 | return series, year 49 | except: 50 | return '' 51 | 52 | -------------------------------------------------------------------------------- /pyqt5-ver/utils/pyqt_util.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | from PyQt5 import QtWidgets 4 | from loguru import logger 5 | 6 | 7 | def catch_exceptions(exc_type, exc_value, exc_tb): 8 | """获取pyqt5的exception""" 9 | 10 | # 输出Traceback信息 11 | tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) 12 | logger.error(f"error catched!:") 13 | logger.error(f"error message:\n{tb}") 14 | 15 | # 界面提示报错 16 | QtWidgets.QMessageBox.critical(None, 17 | "An exception was raised", 18 | "Exception type: {}".format(exc_type)) 19 | # 这里的要去掉 不然可能无限出发弹窗 20 | # old_hook = sys.excepthook 21 | # old_hook(t, val, tb) 22 | -------------------------------------------------------------------------------- /pyqt5-ver/utils/qb_util.py: -------------------------------------------------------------------------------- 1 | import re 2 | import socket 3 | 4 | from loguru import logger 5 | 6 | 7 | def check_qb_port_open(qb_api_ip, qb_api_port): 8 | # 检查端口可用性 9 | a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 10 | # 若填写域名则去掉协议名 11 | qb_api_ip = qb_api_ip.replace("https://", "") 12 | location = (qb_api_ip, int(qb_api_port)) 13 | result_of_check = a_socket.connect_ex(location) 14 | if result_of_check == 0: 15 | logger.info('qb端口可用') 16 | return True 17 | else: 18 | logger.info('qb端口不可用') 19 | return False 20 | 21 | 22 | def parse_feed_url(s): 23 | # 多个feed数据解析 24 | # 多行文本也可以解析 25 | feeds = re.split(', |\||\s', s) 26 | res = [] 27 | for x in feeds: 28 | # 去除空格 29 | x = x.strip() 30 | # 顺便去重 31 | if x and x not in res: 32 | res.append(x) 33 | return res 34 | 35 | 36 | def parse_articles_for_type_hint(articles, source_name): 37 | article_titles = [] 38 | article_details = [] 39 | for article in articles: 40 | url = '' 41 | # feed的链接, 有的在id里面, 有的在url里面, 有的在link里面 42 | for y in ['id', 'link', 'url']: 43 | if y in article and str(article[y]).startswith('http'): 44 | url = article[y] 45 | break 46 | 47 | article_titles.append(article['title']) 48 | article_details.append({ 49 | 'title': article['title'], 50 | 'url': url, 51 | 'source_name': source_name, 52 | 'torrent_url': article['torrentURL'], 53 | }) 54 | return article_titles, article_details 55 | 56 | def parse_feeds_url(feeds): 57 | """ 58 | 提取feed的订阅链接 59 | feed可能包含文件夹, 这里要处理嵌套的多层feed格式 60 | """ 61 | results = [] 62 | for x in feeds: 63 | feed = feeds[x] 64 | if 'url' in feed and 'url' in feed: 65 | # 普通feed 66 | results.append(feed['url']) 67 | else: 68 | # 文件夹 69 | tmp = parse_feeds_url(feed) 70 | results.extend(tmp) 71 | return results 72 | 73 | 74 | def convert_feeds_to_one_level_dict(feeds): 75 | """ 76 | 转换成1层的dict方便解析 77 | """ 78 | res = {} 79 | for x in feeds: 80 | feed = feeds[x] 81 | if 'uid' in feed and 'url' in feed: 82 | res[x] = feed 83 | else: 84 | tmp = convert_feeds_to_one_level_dict(feed) 85 | res.update(tmp) 86 | return res 87 | 88 | if __name__ == '__main__': 89 | print(parse_feed_url('h, s|v a')) 90 | print(parse_feed_url('http')) 91 | -------------------------------------------------------------------------------- /pyqt5-ver/utils/string_util.py: -------------------------------------------------------------------------------- 1 | import fnmatch 2 | import re 3 | 4 | 5 | def try_split_date_and_name(s): 6 | if not s or ' ' not in s: 7 | return '', s 8 | tmp_date, tmp_name = s.split(' ', 1) 9 | pat = '^\d{4}年\d{1,2}月$' 10 | res = re.match(pat, tmp_date) 11 | if res: 12 | return res[0], tmp_name 13 | return '', s 14 | 15 | 16 | def wildcard_match_check(s, keywords_groups_string): 17 | # 多组关键字用 | 隔开 18 | # 单组关键字内 多个条件用空格隔开 19 | # 支持通配符匹配 20 | 21 | # logger.info(f'测试字符 {s}') 22 | # logger.info(f'匹配关键字 {keywords_groups_string}') 23 | 24 | # 关键字分割,不对 \| 进行分割 25 | # https://stackoverflow.com/questions/18092354/python-split-string-without-splitting-escaped-character 26 | keywords_groups = re.split(r'(?