From 1d84af852c431947b6459ae45dbf4f34b98f2874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=89=E9=BB=98=E3=81=AE=E9=87=91?= <110812055+chenmozhijin@users.noreply.github.com> Date: Fri, 22 Nov 2024 23:06:08 +0800 Subject: [PATCH] feat: Refactored local matching & AutoLyricsFetcher to support file names as search keywords --- LDDC/backend/song_info.py | 30 +- LDDC/backend/worker.py | 560 +++++++++++++++---------- LDDC/ui/custom_widgets.py | 2 + LDDC/ui/local_match.ui | 848 ++++++++++++++++++++------------------ LDDC/ui/local_match_ui.py | 443 ++++++++++---------- LDDC/utils/enum.py | 6 + LDDC/view/local_match.py | 493 +++++++++++++++++----- LDDC/view/main_window.py | 9 +- 8 files changed, 1412 insertions(+), 979 deletions(-) diff --git a/LDDC/backend/song_info.py b/LDDC/backend/song_info.py index fea88fb..1a32b4b 100644 --- a/LDDC/backend/song_info.py +++ b/LDDC/backend/song_info.py @@ -3,6 +3,7 @@ import os import re import struct +from collections.abc import Callable from typing import Literal, overload from mutagen import File, FileType, MutagenError # type: ignore[reportPrivateImportUsage] mutagen中的File被误定义为私有 quodlibet/mutagen#647 @@ -44,8 +45,7 @@ def get_audio_file_infos(file_path: str) -> list[dict]: elif "TIT2" in audio and "�" not in str(audio["TIT2"][0]): title = str(audio["TIT2"][0]) else: - msg = f"{file_path} 无法获取歌曲标题" - raise GetSongInfoError(msg) + title = None if "artist" in audio and "�" not in str(audio["artist"][0]): artist = str(audio["artist"][0]) @@ -77,9 +77,6 @@ def get_audio_file_infos(file_path: str) -> list[dict]: "type": "audio", "file_path": file_path, } - if metadata["title"] is None: - msg = f"{file_path} 无法获取歌曲标题" - raise GetSongInfoError(msg) else: msg = f"{file_path} 无法获取歌曲信息" raise GetSongInfoError(msg) @@ -335,20 +332,31 @@ def parse_cue(data: str, file_dir: str, file_path: str | None = None) -> tuple[l @overload -def parse_drop_infos(mime: QMimeData, first: Literal[True] = True) -> dict: +def parse_drop_infos(mime: QMimeData, + first: Literal[True] = True, + progress: Callable[[str, int, int], None] | None = None, + running: Callable[[], bool] | None = None) -> dict: ... @overload -def parse_drop_infos(mime: QMimeData, first: Literal[False] = False) -> list[dict]: +def parse_drop_infos(mime: QMimeData, + first: Literal[False] = False, + progress: Callable[[str, int, int], None] | None = None, + running: Callable[[], bool] | None = None) -> list[dict]: ... -def parse_drop_infos(mime: QMimeData, first: bool = True) -> list[dict] | dict: +def parse_drop_infos(mime: QMimeData, + first: bool = True, + progress: Callable[[str, int, int], None] | None = None, + running: Callable[[], bool] | None = None) -> list[dict] | dict: """解析拖拽的文件 :param mime: 拖拽的文件信息 :param first: 是否只获取第一个文件 + :param progress: 进度回调函数 + :param running: 是否正在运行 :return: 解析后的文件信息 """ paths, tracks, indexs = [], [], [] @@ -402,13 +410,19 @@ def parse_drop_infos(mime: QMimeData, first: bool = True) -> list[dict] | dict: else: msg = "没有获取到任何文件信息" raise DropError(msg) + paths_count = len(paths) for i, path in enumerate(paths): + if running is not None and running() is False: + break track, index = None, None if tracks and len(tracks) > i: track = tracks[i] elif indexs and len(indexs) > i: index = indexs[i] logger.info(f"解析文件 {path}, track={track}, index={index}") + if progress is not None: + msg = f"正在解析文件 {path}" + progress(msg, i + 1, paths_count) if path.lower().endswith('.cue') and (isinstance(track, str | int) or index is not None): try: diff --git a/LDDC/backend/worker.py b/LDDC/backend/worker.py index a99f72d..bcfff38 100644 --- a/LDDC/backend/worker.py +++ b/LDDC/backend/worker.py @@ -5,15 +5,17 @@ import re import time from itertools import zip_longest -from typing import Any, Literal +from typing import Any, ClassVar, Literal from PySide6.QtCore import ( QCoreApplication, QEventLoop, + QMimeData, + QMutex, + QMutexLocker, QObject, QRunnable, Qt, - QThread, QTimer, Signal, Slot, @@ -22,24 +24,20 @@ from LDDC.utils.cache import cache from LDDC.utils.data import cfg, local_song_lyrics from LDDC.utils.enum import ( - LocalMatchFileNameMode, - LocalMatchSaveMode, - LyricsFormat, + LocalMatchSave2TagMode, LyricsProcessingError, LyricsType, SearchType, Source, ) from LDDC.utils.error import LyricsRequestError, LyricsUnavailableError -from LDDC.utils.logger import DEBUG, logger +from LDDC.utils.logger import logger from LDDC.utils.thread import in_main_thread, threadpool from LDDC.utils.utils import ( - escape_filename, - escape_path, get_artist_str, + get_local_match_save_path, get_lyrics_format_ext, get_save_path, - replace_info_placeholders, ) from LDDC.utils.version import compare_versions from LDDC.view.msg_box import MsgBox @@ -57,7 +55,7 @@ from .fetcher import get_lyrics from .lyrics import Lyrics from .searcher import search -from .song_info import audio_formats, get_audio_file_infos, parse_cue_from_file +from .song_info import audio_formats, get_audio_file_infos, parse_cue_from_file, parse_drop_infos, write_lyrics class CheckUpdateSignal(QObject): @@ -133,6 +131,7 @@ def run(self) -> None: if isinstance(self.source, Source): try: result = search(self.keyword, self.search_type, self.source, self.info, self.page) + logger.debug("%s (%s)[%s]搜索结果: %s", self.keyword, self.search_type, self.source, result) except Exception as e: logger.exception("搜索时遇到错误") msg = f"搜索时遇到错误:\n{e.__class__.__name__}: {e!s}" @@ -342,219 +341,294 @@ def run(self) -> None: class LocalMatchSignal(QObject): - massage = Signal(str) - error = Signal(str, int) - finished = Signal() - progress = Signal(int, int) + progress = Signal(dict) + finished = Signal(dict) # 是否成功, 错误信息 class LocalMatchWorker(QRunnable): + get_infos_progress: ClassVar[dict] = {"mutex": QMutex(), "progress": {}, "total": {}} def __init__(self, - infos: dict, - ) -> None: + task: Literal["get_infos", "match_lyrics"], + taskid: int, + **kwargs: Any) -> None: + """本地匹配歌词 + + Args: + task (Literal["get_infos", "match_lyrics"]): 任务类型 + taskid (int): 任务id + + kwargs (Any): 以下参数 + mime (QMimeData): 文件信息 + + infos (list[dict]): 要匹配的歌曲信息 + save_mode (LocalMatchSaveMode): 保存模式 + file_name_mode (LocalMatchFileNameMode): 文件名模式 + save2tag_mode (LocalMatchSave2TagMode): 保存到标签模式 + lyrics_format (LyricsFormat): 歌词格式 + langs (list[str]): 语言 + save_root_path (str): 保存根路径 + min_score (int): 最小匹配分数 + source (list[Source]): 歌词源 + + """ super().__init__() self.signals = LocalMatchSignal() - self.song_path: str = infos["song_path"] - self.save_path: str = infos["save_path"] - self.save_mode: LocalMatchSaveMode = infos["save_mode"] - self.fliename_mode: LocalMatchFileNameMode = infos["flienmae_mode"] - self.langs: list[str] = infos["langs_order"] - self.lyrics_format: LyricsFormat = infos["lyrics_format"] - self.source: list[Source] = infos["source"] - - self.is_running = True - self.current_index = 0 - self.total_index = 0 - - self.skip_inst_lyrics = cfg["skip_inst_lyrics"] - self.file_name_format = cfg["lyrics_file_name_fmt"] - - self.min_score = infos["min_score"] - - def lyric_processing_error(self, error: str) -> None: - self.signals.error.emit(error, 0) - - def stop(self) -> None: - self.is_running = False - - def fetch_next_lyrics(self) -> None: - info = self.song_infos[self.current_index] - self.current_index += 1 - worker = AutoLyricsFetcher(info, self.min_score, self.source) - worker.signals.result.connect(self.handle_fetch_result, Qt.ConnectionType.BlockingQueuedConnection) - threadpool.start(worker) + self.task = task + self.taskid = taskid + self.kwargs = kwargs + self.errors: list[str] = [] + self.running = True def run(self) -> None: - self.thread = QThread.currentThread() - logger.info("开始本地匹配歌词,源:%s", self.source) - try: - self.start_time = time.time() - # Setp 1 处理cue 与 遍历歌曲文件 - self.signals.massage.emit(QCoreApplication.translate("LocalMatch", "处理 cue 并 遍历歌曲文件...")) - song_infos = [] - cue_audio_files = [] - cue_count = 0 - audio_file_paths = [] - for root, _dirs, files in os.walk(self.song_path): - for file in files: - if not self.is_running: - self.signals.finished.emit() - return - if file.lower().endswith('.cue'): - file_path = os.path.join(root, file) - try: - songs, cue_audio_file_paths = parse_cue_from_file(file_path) - if len(songs) > 0: - song_infos.extend(songs) - cue_audio_files.extend(cue_audio_file_paths) - cue_count += 1 - else: - logger.warning("没有在cue文件 %s 解析到歌曲", file_path) - self.signals.error.emit(QCoreApplication.translate("LocalMatch", "没有在cue文件 {0} 解析到歌曲").format(file_path), 0) - except Exception as e: - logger.exception("处理cue文件时错误 file:%s", file_path) - self.signals.error.emit(f"处理cue文件时错误:{e}", 0) - elif file.lower().split(".")[-1] in audio_formats: - file_path = os.path.join(root, file) - audio_file_paths.append(file_path) - - for cue_audio_file in cue_audio_files: # 去除cue中有的文件 - if cue_audio_file in audio_file_paths: - audio_file_paths.remove(cue_audio_file) - msg = QCoreApplication.translate("LocalMatch", "共找到{0}首歌曲").format(f"{len(audio_file_paths) + len(song_infos)}") - if cue_count > 0: - msg += QCoreApplication.translate("LocalMatch", ",其中{0}首在{1}个cue文件中找到").format(f"{len(song_infos)}", str(cue_count)) - - self.signals.massage.emit(msg) - - # Step 2 读取歌曲文件信息 - self.signals.massage.emit(QCoreApplication.translate("LocalMatch", "正在读取歌曲文件信息...")) - total_paths = len(audio_file_paths) - for i, audio_file_path in enumerate(audio_file_paths): - self.signals.progress.emit(i, total_paths) - if not self.is_running: - self.signals.finished.emit() - return + match self.task: + case "get_infos": try: - song_infos.extend(get_audio_file_infos(audio_file_path)) + self.get_infos() except Exception as e: - logger.exception("读取歌曲文件信息时错误") - self.signals.error.emit(f"读取歌曲文件信息时错误:{e}", 0) - continue + logger.exception("获取歌曲信息时出错") + self.errors.append(f"{e.__class__.__name__}: {e!s}") + self.del_get_infos_progress() + self.signals.finished.emit({"taskid": self.taskid, "status": "error", "error": f"{e.__class__.__name__}: {e!s}", "errors": self.errors}) - # Step 3 根据信息搜索并获取歌词 - if logger.level <= DEBUG: - logger.debug("song_infos: %s", json.dumps(song_infos, indent=4, ensure_ascii=False)) - self.total_index = len(song_infos) - self.song_infos: list[dict] = song_infos - if self.total_index > 0: - self.signals.massage.emit(QCoreApplication.translate("LocalMatch", "正在搜索并获取歌词...")) - self.fetch_next_lyrics() - else: - self.signals.massage.emit(QCoreApplication.translate("LocalMatch", "没有找到可查找歌词的歌曲")) - self.signals.finished.emit() - except Exception as e: - logger.exception("搜索歌词时错误") - self.signals.error.emit(str(e), 1) - finally: - self.thread.exec() - self.signals.finished.emit() + case "match_lyrics": + try: + self.match_lyrics() + except Exception as e: + logger.exception("匹配歌词时出错") + self.signals.finished.emit({"taskid": self.taskid, "status": "error", "error": f"{e.__class__.__name__}: {e!s}"}) - @Slot(dict) - def handle_fetch_result(self, result: dict[str, dict | Lyrics | str]) -> None: - try: - song_info = result["orig_info"] - if not isinstance(song_info, dict) or isinstance(song_info, Lyrics): - return + def is_running(self) -> bool: + return self.running - simple_song_info_str = f"{get_artist_str(song_info['artist'])} - {song_info['title']}" if "artist" in song_info else song_info["title"] - progress_str = f"[{self.current_index}/{self.total_index}]" + def stop(self) -> None: + self.running = False + + def update_get_infos_progress(self, msg: str, progress: int, total: int) -> None: + with QMutexLocker(self.get_infos_progress["mutex"]): + self.get_infos_progress["progress"][self.taskid] = progress + self.get_infos_progress["total"][self.taskid] = total + progress = sum(self.get_infos_progress["progress"].values()) + total = sum(self.get_infos_progress["total"].values()) + self.signals.progress.emit({"msg": msg, "progress": progress, "total": total}) + + def del_get_infos_progress(self) -> None: + with QMutexLocker(self.get_infos_progress["mutex"]): + del self.get_infos_progress["progress"][self.taskid] + del self.get_infos_progress["total"][self.taskid] + if not self.get_infos_progress["progress"]: + self.signals.progress.emit({"msg": "", "progress": 0, "total": 0}) + + def get_infos(self) -> None: + mime: QMimeData = self.kwargs["mime"] + formats = mime.formats() + if ('application/x-qt-windows-mime;value="ACL.FileURIs"' in formats or + 'application/x-qt-windows-mime;value="foobar2000_playable_location_format"' in formats): + infos = parse_drop_infos(mime, + first=False, + progress=self.update_get_infos_progress, + running=self.is_running) + self.del_get_infos_progress() + if self.running: + self.signals.finished.emit({"taskid": self.taskid, "status": "success", "infos": infos}) + else: + self.signals.finished.emit({"taskid": self.taskid, "status": "cancelled"}) - match result["status"]: - case "成功": - if self.skip_inst_lyrics is False or result.get("is_inst") is False: - # 不是纯音乐或不要跳过纯音乐 - lrc_info = result["result_info"] - lyrics = result["lyrics"] - if not isinstance(lrc_info, dict) or not isinstance(lyrics, Lyrics): + else: + self.update_get_infos_progress(QCoreApplication.translate("LocalMatch", "遍历文件..."), 0, 0) + mix_paths = [url.toLocalFile() for url in mime.urls()] + infos: list[dict] = [] + dirs: list[str] = [] + audio_paths: list[tuple[str | None, str]] = [] # (遍历的目录, 文件路径) + cue_paths: list[tuple[str | None, str]] = [] # (遍历的目录, 文件路径) + exclude_audio_paths: list[str] = [] + for path in mix_paths: + if not self.running: + self.del_get_infos_progress() + self.signals.finished.emit({"taskid": self.taskid, "status": "cancelled"}) + return + if os.path.isdir(path): + dirs.append(path) + elif os.path.isfile(path): + if path.lower().endswith(".cue"): + cue_paths.append((None, path)) + elif path.lower().split(".")[-1] in audio_formats: + audio_paths.append((None, path)) + + for directory in dirs: + if not self.running: + self.del_get_infos_progress() + self.signals.finished.emit({"taskid": self.taskid, "status": "cancelled"}) + return + for root, _, files in os.walk(directory): + for file in files: + if not self.running: + self.del_get_infos_progress() + self.signals.finished.emit({"taskid": self.taskid, "status": "cancelled"}) return - - # Step 4 合并歌词 - converted_lyrics = convert2(lyrics, self.langs, self.lyrics_format) - # Step 5 保存歌词 - match self.save_mode: - case LocalMatchSaveMode.MIRROR: - save_folder = os.path.join(self.save_path, - os.path.dirname(os.path.relpath(song_info["file_path"], self.song_path))) - case LocalMatchSaveMode.SONG: - save_folder = os.path.dirname(song_info["file_path"]) - - case LocalMatchSaveMode.SPECIFY: - save_folder = self.save_path - - save_folder = escape_path(save_folder).strip() - ext = get_lyrics_format_ext(self.lyrics_format) - - match self.fliename_mode: - case LocalMatchFileNameMode.SONG: - if song_info["type"] == "cue": - save_folder = os.path.join(save_folder, os.path.splitext(os.path.basename(song_info["file_path"]))[0]) - save_filename = escape_filename( - replace_info_placeholders(self.file_name_format, lrc_info, self.langs)) + ext - else: - save_filename = os.path.splitext(os.path.basename(song_info["file_path"]))[0] + ext - - case LocalMatchFileNameMode.FORMAT: - save_filename = escape_filename( - replace_info_placeholders(self.file_name_format, lrc_info, self.langs)) + ext - - save_path = os.path.join(save_folder, save_filename) - try: + if file.lower().endswith(".cue"): + cue_paths.append((directory, os.path.join(root, file))) + elif file.lower().split(".")[-1] in audio_formats: + audio_paths.append((directory, os.path.join(root, file))) + + total = len(audio_paths) + len(cue_paths) + + # 解析cue文件 + for i, (root_path, cue_path) in enumerate(cue_paths, start=1): + if not self.running: + self.del_get_infos_progress() + self.signals.finished.emit({"taskid": self.taskid, "status": "cancelled"}) + return + self.update_get_infos_progress(f"解析cue{cue_path}...", i, total) + try: + songs, cue_audio_file_paths = parse_cue_from_file(cue_path) + exclude_audio_paths.extend(cue_audio_file_paths) + for song in songs: # 添加遍历根目录到歌曲信息 + song["root_path"] = root_path + infos.extend(songs) + except Exception as e: + logger.exception("解析cue文件失败: %s", cue_path) + self.errors.append(f"{e.__class__.__name__}: {e!s}") + + # 排除cue文件中的音频文件 + audio_paths = [path for path in audio_paths if path not in exclude_audio_paths] + total = len(audio_paths) + len(cue_paths) + # 解析音频文件 + for i, (root_path, audio_path) in enumerate(audio_paths, start=1 + len(cue_paths)): + if not self.running: + self.del_get_infos_progress() + self.signals.finished.emit({"taskid": self.taskid, "status": "cancelled"}) + return + self.update_get_infos_progress(f"解析歌曲文件{audio_path}...", i, total) + try: + songs = get_audio_file_infos(audio_path) + for song in songs: # 添加遍历根目录到歌曲信息 + song["root_path"] = root_path + infos.extend(songs) + except Exception as e: + logger.exception("解析歌曲文件失败: %s", audio_path) + self.errors.append(f"{e.__class__.__name__}: {e!s}") + + self.del_get_infos_progress() + self.signals.finished.emit({"taskid": self.taskid, "status": "success", "infos": infos, "errors": self.errors}) + + def match_lyrics(self) -> None: + infos: list[dict] = self.kwargs["infos"] + save_mode = self.kwargs["save_mode"] + file_name_mode = self.kwargs["file_name_mode"] + save2tag_mode = self.kwargs["save2tag_mode"] + lyrics_format = self.kwargs["lyrics_format"] + langs = self.kwargs["langs"] + save_root_path = self.kwargs["save_root_path"] + min_score = self.kwargs["min_score"] + source = self.kwargs["source"] + skip_inst_lyrics = cfg["skip_inst_lyrics"] + file_name_format = cfg["lyrics_file_name_fmt"] + + total = len(infos) + current = 0 + + success_count = 0 + fail_count = 0 + skip_count = 0 + + loop = QEventLoop() + + def update_progress(extra: dict | None = None) -> None: + """更新进度 + + :param extra: 额外信息 + """ + info = infos[current] + artist, title, file_path = info.get("artist"), info.get("title"), info.get("file_path") + song_str = (f"{get_artist_str(artist)} - {title.strip()}" if artist else title.strip()) if title and title.strip() else file_path + self.signals.progress.emit({"msg": f"正在匹配 {song_str} 的歌词...", + "progress": current + 1, "total": total, **(extra if extra else {})}) + + def next_song() -> None: + if current == 0: + update_progress() + info = infos[current] + worker = AutoLyricsFetcher(info, min_score, source, current) + worker.signals.result.connect(handle_result, Qt.ConnectionType.BlockingQueuedConnection) + threadpool.start(worker) + + def handle_result(result: dict[str, dict | Lyrics | str]) -> None: + nonlocal current, success_count, fail_count, skip_count + + if "status" not in result: + logger.error("result中没有status字段: %s", result) + try: + extra: dict = {"status": result["status"], "current": current} # 更新进度的额外信息 + except Exception as e: + logger.exception("处理歌词匹配结果失败: %s", result) + raise e from e + song_info = infos[current] + + if result["status"] == "成功": + if skip_inst_lyrics is False or result.get("is_inst") is False: + # 不是纯音乐或不要跳过纯音乐 + lrc_info = result["result_info"] + lyrics = result["lyrics"] + if not isinstance(lrc_info, dict) or not isinstance(lyrics, Lyrics): + msg = "lrc_info and lyrics must be dict and Lyrics" + raise ValueError(msg) + + # 合并并转换歌词到指定格式 + converted_lyrics = convert2(lyrics, langs, lyrics_format) + + save_path = None + if (song_info["type"] == "cue" or save2tag_mode != LocalMatchSave2TagMode.ONLY_TAG): + save_path = get_local_match_save_path(save_mode=save_mode, + file_name_mode=file_name_mode, + song_info=song_info, + lyrics_format=lyrics_format, + file_name_format=file_name_format, + langs=langs, + save_root_path=save_root_path, + lrc_info=lrc_info) + if isinstance(save_path, int): + msg = f"获取歌词保存路径失败,错误码: {save_path}" + raise ValueError(msg) + extra["save_path"] = save_path + + # 保存歌词 + try: + if save_path: if not os.path.exists(os.path.dirname(save_path)): os.makedirs(os.path.dirname(save_path)) with open(save_path, "w", encoding="utf-8") as f: f.write(converted_lyrics) - msg = (f"{progress_str}" + - QCoreApplication.translate("LocalMatch", "本地") + f": {simple_song_info_str} " + - QCoreApplication.translate("LocalMatch", "匹配") + f": {get_artist_str(lrc_info['artist'])} - {lrc_info['title']} " + - QCoreApplication.translate("LocalMatch", "成功保存到") + f"{save_path}") - self.signals.massage.emit(msg) - except Exception as e: - self.signals.error.emit(str(e), 0) - else: - # 是纯音乐并要跳过纯音乐 - msg = (f"{progress_str}" + - QCoreApplication.translate("LocalMatch", "本地") + f": {simple_song_info_str} " + - QCoreApplication.translate("LocalMatch", "搜索结果") + - f":{simple_song_info_str} " + QCoreApplication.translate("LocalMatch", "跳过纯音乐")) - self.signals.massage.emit(msg) - case "没有找到符合要求的歌曲": - msg = (f"{progress_str} {simple_song_info_str}:" + QCoreApplication.translate("LocalMatch", "没有找到符合要求的歌曲")) - self.signals.massage.emit(msg) - case "搜索结果处理失败": - msg = (f"{progress_str} {simple_song_info_str}:" + QCoreApplication.translate("LocalMatch", "搜索结果处理失败")) - self.signals.massage.emit(msg) - case "没有足够的信息用于搜索": - msg = (f"{progress_str} {simple_song_info_str}:" + QCoreApplication.translate("LocalMatch", "没有足够的信息用于搜索")) - self.signals.massage.emit(msg) - case "超时": - msg = (f"{progress_str} {simple_song_info_str}:" + QCoreApplication.translate("LocalMatch", "超时")) - self.signals.massage.emit(msg) + success_count += 1 + if save2tag_mode in (LocalMatchSave2TagMode.ONLY_TAG, LocalMatchSave2TagMode.BOTH) and song_info["type"] != "cue": + write_lyrics(song_info["file_path"], converted_lyrics, lyrics) + except Exception: + extra["status"] = "保存歌词失败" + logger.exception("保存歌词失败") + fail_count += 1 + else: + extra["status"] = "跳过纯音乐" + skip_count += 1 + else: + fail_count += 1 - except Exception: - logger.exception("合并或保存时出错") - msg = (f"{progress_str} {simple_song_info_str}:" + QCoreApplication.translate("LocalMatch", "合并或保存时出错")) - self.signals.massage.emit(msg) - finally: - self.signals.progress.emit(self.current_index, self.total_index) - if self.current_index == self.total_index: - self.signals.massage.emit(QCoreApplication.translate("LocalMatch", "匹配完成,耗时{0}秒").format(f"{time.time() - self.start_time}")) - self.thread.quit() - elif self.is_running: - self.fetch_next_lyrics() + if current + 1 < total: + current += 1 + update_progress(extra) + + if not self.running: + self.signals.finished.emit({"taskid": self.taskid, "status": "cancelled", + "success": success_count, "fail": fail_count, "skip": skip_count, "total": total}) + loop.quit() + else: + next_song() else: - self.thread.quit() + update_progress(extra) + self.signals.finished.emit({"taskid": self.taskid, "status": "success", + "success": success_count, "fail": fail_count, "skip": skip_count, "total": total}) + loop.quit() + next_song() + loop.exec() class AutoLyricsFetcherSignal(QObject): @@ -591,10 +665,15 @@ def __init__(self, info: dict, self.result = None - def new_search_work(self, keyword: str, search_type: SearchType, source: Source, info: dict | None = None) -> None: + def new_search_work(self, + keyword: str, + search_type: SearchType, + source: Source, + keyword_type: Literal["artist-title", "title", "file_name"], + info: dict | None = None) -> None: task_id = len(self.search_task) - self.search_task[task_id] = (keyword, search_type, source) - worker = SearchWorker(task_id, *self.search_task[task_id], info=info) + self.search_task[task_id] = (keyword, search_type, source, keyword_type) + worker = SearchWorker(task_id, *self.search_task[task_id][:3], info=info) worker.signals.result.connect(self.handle_search_result, Qt.ConnectionType.BlockingQueuedConnection) worker.signals.error.connect(self.handle_search_error, Qt.ConnectionType.BlockingQueuedConnection) threadpool.start(worker) @@ -608,9 +687,20 @@ def new_get_work(self, song_info: dict) -> None: def search(self) -> None: artist: list | str | None = self.info.get('artist') - keyword = f"{get_artist_str(artist)} - {self.info['title'].strip()}" if artist else self.info["title"].strip() + title: str | None = self.info.get('title') + if title and title.strip(): + if artist: + keyword = f"{get_artist_str(artist)} - {self.info['title'].strip()}" + keyword_type = "artist-title" + else: + keyword = self.info["title"].strip() + keyword_type = "title" + else: + keyword: str = os.path.splitext(os.path.basename(self.info['file_path']))[0] + keyword_type = "file_name" + for source in self.source: - self.new_search_work(keyword, SearchType.SONG, source) + self.new_search_work(keyword, SearchType.SONG, source, keyword_type) @Slot(str) def handle_search_error(self, error: str) -> None: @@ -624,7 +714,7 @@ def handle_search_result(self, taskid: int, search_type: SearchType, infos: list try: self.search_task_finished += 1 - keyword, _search_type, source = self.search_task[taskid] + keyword, _search_type, source, keyword_type = self.search_task[taskid] if source == Source.KG and search_type == SearchType.LYRICS: if infos: self.new_get_work(infos[0]) @@ -642,18 +732,23 @@ def handle_search_result(self, taskid: int, search_type: SearchType, infos: list if not duration: logger.warning("没有获取到 %s - %s 的时长, 跳过时长匹配检查", get_artist_str(self.info.get('artist')), self.info['title'].strip()) - artist_score = None - title_score = calculate_title_score(self.info['title'], info.get('title', '')) - album_score = max(text_difference(self.info.get('album', '').lower(), info.get('album', '').lower()) * 100, 0) - if (isinstance(artist, str) and artist.strip()) or isinstance(artist, list) and [a for a in artist if a]: - artist_score = calculate_artist_score(artist, info.get('artist', '')) - - if artist_score is not None: - score = max(title_score * 0.5 + artist_score * 0.5, title_score * 0.5 + artist_score * 0.35 + album_score * 0.15) - elif self.info.get('album', '').strip() and info.get('album', '').strip(): - score = max(title_score * 0.7 + album_score * 0.3, title_score * 0.8) - else: - score = title_score + if keyword_type in ("artist-title", "title"): + artist_score = None + title_score = calculate_title_score(self.info['title'], info.get('title', '')) + album_score = max(text_difference(self.info.get('album', '').lower(), info.get('album', '').lower()) * 100, 0) + if (isinstance(artist, str) and artist.strip()) or isinstance(artist, list) and [a for a in artist if a]: + artist_score = calculate_artist_score(artist, info.get('artist', '')) + + if artist_score is not None: + score = max(title_score * 0.5 + artist_score * 0.5, title_score * 0.5 + artist_score * 0.35 + album_score * 0.15) + elif self.info.get('album', '').strip() and info.get('album', '').strip(): + score = max(title_score * 0.7 + album_score * 0.3, title_score * 0.8) + else: + score = title_score + else: # file_name + score = max(text_difference(keyword, info.get('title', '')) * 100, + text_difference(keyword, f"{get_artist_str(info.get('artist', ''))} - {info.get('title', '')}") * 100, + text_difference(keyword, f"{info.get('title', '')} - {get_artist_str(info.get('artist', ''))}") * 100) if score > self.min_score: score_info.append((score, info)) @@ -668,15 +763,17 @@ def handle_search_result(self, taskid: int, search_type: SearchType, infos: list self.search_result[source] = (keyword, infos) if source == Source.KG and search_type == SearchType.SONG: # 对酷狗音乐搜索到的歌曲搜索歌词 - self.new_search_work(f"{get_artist_str(self.info.get('artist'), '、')} - {self.info['title'].strip()}", + self.new_search_work(f"{get_artist_str(best_info['artist'], '、')} - {best_info['title'].strip()}", SearchType.LYRICS, Source.KG, - info=best_info) + keyword_type, + info=best_info, + ) else: self.new_get_work(best_info) - elif keyword == f"{get_artist_str(self.info.get('artist'))} - {self.info['title'].strip()}": + elif keyword_type == "artist-title" and search_type == SearchType.SONG: # 尝试只搜索歌曲名 - self.new_search_work(self.info['title'].strip(), SearchType.SONG, source) + self.new_search_work(self.info['title'].strip(), SearchType.SONG, source, "title") else: logger.warning("无法从源:%s找到符合要求的歌曲:%s,", source, self.info) except Exception: @@ -713,6 +810,7 @@ def get_result(self) -> None: len(self.get_task) != self.get_task_finished or len(self.get_task) == 0 or self.get_task_finished == 0): + logger.debug("processEvents") self.loop.processEvents() return @@ -769,7 +867,8 @@ def get_result(self) -> None: # 判断是否为纯音乐 if ((info["source"] == Source.KG and info['language'] in ["纯音乐", '伴奏']) or result.is_inst() or - re.findall(r"伴奏|纯音乐|inst\.?(?:rumental)|off ?vocal(?: ?[Vv]er.)?", info.get('title', '') + self.info['title'])): + re.findall(r"伴奏|纯音乐|inst\.?(?:rumental)|off ?vocal(?: ?[Vv]er.)?", + info.get('title', '') + self.info['title'] if self.info.get('title') else '')): is_inst = True else: is_inst = False @@ -783,20 +882,25 @@ def send_result(self, result: dict) -> None: self.result = result if self.tastid: self.result["taskid"] = self.tastid + logger.info("AutoLyricsFetcher 搜索结果:%s", result) self.loop.exit() def run(self) -> None: self.loop = QEventLoop() - if not self.info.get("title") or not self.info["title"].strip(): - self.send_result({"status": "没有足够的信息用于搜索", "orig_info": self.info}) - return + title, file_path = self.info.get("title"), self.info.get("file_path") + if (not title or title.isspace()) and (not file_path or file_path.isspace()): + logger.warning("没有足够的信息用于搜索:%s", self.info) + self.result = {"status": "没有足够的信息用于搜索", "orig_info": self.info} + if self.tastid: + self.result["taskid"] = self.tastid + self.signals.result.emit(self.result) self.search() self.timer = QTimer() self.timer.start(30 * 1000) self.timer.timeout.connect(lambda: self.send_result({"status": "超时", "orig_info": self.info})) self.loop.exec() self.timer.stop() - self.signals.result.emit(self.result) # 最后发送结果[不然容易出错(闪退、无响应)] + self.signals.result.emit(self.result) class LocalSongLyricsDBSignals(QObject): diff --git a/LDDC/ui/custom_widgets.py b/LDDC/ui/custom_widgets.py index 24dbc69..2737914 100644 --- a/LDDC/ui/custom_widgets.py +++ b/LDDC/ui/custom_widgets.py @@ -76,6 +76,8 @@ def event(self, event: QEvent) -> bool: if event.type() == QEvent.Type.ToolTip and isinstance(event, QHelpEvent): item = self.itemAt(event.x(), event.y() - self.horizontalHeader().height()) if isinstance(item, QTableWidgetItem): + if item.toolTip(): + return super().event(event) text = item.text().strip() # 考虑表头的高度 if text: QToolTip.showText(QCursor.pos(), text, self) diff --git a/LDDC/ui/local_match.ui b/LDDC/ui/local_match.ui index f806d3b..dfddb01 100644 --- a/LDDC/ui/local_match.ui +++ b/LDDC/ui/local_match.ui @@ -6,426 +6,459 @@ 0 0 - 1050 - 600 + 851 + 400 - + + + + + + + + + + + + 0 + 0 + + + + 最低匹配度(0~100): + + + + + + + QComboBox::SizeAdjustPolicy::AdjustToContents + + + + 只保存到文件 + + + + + 只保存到歌词标签(非cue) + + + + + 保存到歌词标签(非cue)与文件 + + + + + + + + + 0 + 0 + + + + 歌词格式: + + + + + + + + 0 + 0 + + + + 歌词文件名: + + + + + + + + 0 + 0 + + + + 选择保存路径 + + + + + + + + 70 + 0 + + + + 100 + + + 60 + + + + + + + + LRC(逐字) + + + + + LRC(逐行) + + + + + 增强型LRC(ESLyric) + + + + + SRT + + + + + ASS + + + + + + + + + 0 + 0 + + + + + 133 + 0 + + + + + 与设置中的格式相同 + + + + + 与歌曲文件名相同 + + + + + + + + + 0 + 0 + + + + 歌词文件保存模式: + + + + + + + + 0 + 0 + + + + + 194 + 0 + + + + QComboBox::SizeAdjustPolicy::AdjustToContents + + + + 保存到歌曲文件夹的镜像文件夹 + + + + + 保存到歌曲文件夹 + + + + + 保存到指定文件夹 + + + + + + + + + 0 + 0 + + + + 歌词来源: + + + + + + + + 0 + 0 + + + + 选择文件 + + + + + + + 保存到歌词标签: + + + + + + + + 0 + 0 + + + + 选择文件夹 + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 96 + 78 + + + + false + + + QAbstractItemView::DragDropMode::InternalMove + + + Qt::DropAction::TargetMoveAction + + + + + + + 歌词类型: + + + + + + + 原文 + + + true + + + + + + + + 0 + 0 + + + + 译文 + + + true + + + + + + + + 0 + 0 + + + + 罗马音 + + + + + + + + 0 + 0 + + + + 开始匹配 + + + + + + - + 0 0 - - Qt::Orientation::Horizontal + + Qt::ScrollBarPolicy::ScrollBarAsNeeded + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + false + + + QAbstractItemView::SelectionMode::ExtendedSelection + + + QAbstractItemView::SelectionBehavior::SelectRows + + + false + + + 0 - + true + + + false + + + 100 + + + true + + + false + + + false + + + true + + + + 歌曲名 + + + + + 艺术家 + + + + + 专辑 + + + + + 歌曲路径 + + + + + 时长 + + + + + 保存路径 + + + + + 状态 + + + + + + + + + 0 + 0 + - - 5 + + + + + + Qt::Orientation::Horizontal - + false - - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - - 18 - - - - 本地匹配 - - - - - - - 为本地歌曲文件匹配歌词 - - - - - - - - - - - - 选择要遍历的文件夹 - - - - - - - - - 保存 - - - - - - - - - - 0 - 0 - - - - 选择文件夹路径 - - - - - - - - - 歌词文件名: - - - - - - - - 0 - 0 - - - - 歌词保存模式: - - - - - - - - 0 - 0 - - - - - 194 - 0 - - - - - 保存到歌曲文件夹的镜像文件夹 - - - - - 保存到歌曲文件夹 - - - - - 保存到指定文件夹 - - - - - - - - - 0 - 0 - - - - - 133 - 0 - - - - - 与设置中的格式相同 - - - - - 与歌曲文件名相同 - - - - - - - - - - - - - 歌词 - - - - - - - - - - - 70 - 0 - - - - 100 - - - 60 - - - - - - - - 0 - 0 - - - - 最低匹配度(0~100): - - - - - - - - 0 - 0 - - - - 歌词来源: - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 96 - 64 - - - - QAbstractItemView::DragDropMode::DragDrop - - - Qt::DropAction::MoveAction - - - - - - - - - - - - - 歌词类型: - - - - - - - - - 原文 - - - true - - - - - - - - 0 - 0 - - - - 译文 - - - true - - - - - - - - 0 - 0 - - - - 罗马音 - - - - - - - - - - 0 - 0 - - - - 歌词格式: - - - - - - - - LRC(逐字) - - - - - LRC(逐行) - - - - - 增强型LRC(ESLyric) - - - - - SRT - - - - - ASS - - - - - - - - - - - - - 开始匹配 - - - - - - - Qt::Orientation::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - 0 - 0 - - - - true - - - - - - - 0 - - - 0 - - - Qt::Orientation::Horizontal - - - false - - - - - + + %v/%m %p% + @@ -436,6 +469,11 @@ QListWidget
LDDC.ui.custom_widgets
+ + ProportionallyStretchedTableWidget + QTableWidget +
LDDC.ui.custom_widgets
+
diff --git a/LDDC/ui/local_match_ui.py b/LDDC/ui/local_match_ui.py index 126db6f..1b1659d 100644 --- a/LDDC/ui/local_match_ui.py +++ b/LDDC/ui/local_match_ui.py @@ -1,290 +1,271 @@ ################################################################################ ## Form generated from reading UI file 'local_match.ui' ## -## Created by: Qt User Interface Compiler version 6.7.2 +## Created by: Qt User Interface Compiler version 6.8.0 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ from PySide6.QtCore import QCoreApplication, QMetaObject, QSize, Qt -from PySide6.QtGui import QFont from PySide6.QtWidgets import ( QAbstractItemView, + QAbstractScrollArea, QCheckBox, QComboBox, QGridLayout, - QGroupBox, - QHBoxLayout, QLabel, - QLineEdit, - QPlainTextEdit, QProgressBar, QPushButton, QSizePolicy, - QSpacerItem, QSpinBox, - QSplitter, + QTableWidgetItem, QVBoxLayout, QWidget, ) -from LDDC.ui.custom_widgets import CheckBoxListWidget +from LDDC.ui.custom_widgets import CheckBoxListWidget, ProportionallyStretchedTableWidget class Ui_local_match: def setupUi(self, local_match): if not local_match.objectName(): local_match.setObjectName("local_match") - local_match.resize(1050, 600) - self.horizontalLayout_6 = QHBoxLayout(local_match) - self.horizontalLayout_6.setObjectName("horizontalLayout_6") - self.splitter = QSplitter(local_match) - self.splitter.setObjectName("splitter") - sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + local_match.resize(851, 400) + self.verticalLayout = QVBoxLayout(local_match) + self.verticalLayout.setObjectName("verticalLayout") + self.control_bar = QWidget(local_match) + self.control_bar.setObjectName("control_bar") + self.gridLayout = QGridLayout(self.control_bar) + self.gridLayout.setObjectName("gridLayout") + self.label_3 = QLabel(self.control_bar) + self.label_3.setObjectName("label_3") + sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.splitter.sizePolicy().hasHeightForWidth()) - self.splitter.setSizePolicy(sizePolicy) - self.splitter.setOrientation(Qt.Orientation.Horizontal) - self.splitter.setOpaqueResize(True) - self.splitter.setHandleWidth(5) - self.splitter.setChildrenCollapsible(False) - self.layoutWidget_2 = QWidget(self.splitter) - self.layoutWidget_2.setObjectName("layoutWidget_2") - self.verticalLayout_3 = QVBoxLayout(self.layoutWidget_2) - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) - self.verticalLayout = QVBoxLayout() - self.verticalLayout.setObjectName("verticalLayout") - self.label = QLabel(self.layoutWidget_2) - self.label.setObjectName("label") - sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) - sizePolicy1.setHorizontalStretch(0) - sizePolicy1.setVerticalStretch(0) - sizePolicy1.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) - self.label.setSizePolicy(sizePolicy1) - self.label.setMaximumSize(QSize(16777215, 16777215)) - font = QFont() - font.setPointSize(18) - self.label.setFont(font) - - self.verticalLayout.addWidget(self.label) + sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) + self.label_3.setSizePolicy(sizePolicy) - self.label_4 = QLabel(self.layoutWidget_2) - self.label_4.setObjectName("label_4") + self.gridLayout.addWidget(self.label_3, 0, 7, 1, 2) - self.verticalLayout.addWidget(self.label_4) + self.save2tag_mode_comboBox = QComboBox(self.control_bar) + self.save2tag_mode_comboBox.addItem("") + self.save2tag_mode_comboBox.addItem("") + self.save2tag_mode_comboBox.addItem("") + self.save2tag_mode_comboBox.setObjectName("save2tag_mode_comboBox") + self.save2tag_mode_comboBox.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) - self.horizontalLayout_7 = QHBoxLayout() - self.horizontalLayout_7.setObjectName("horizontalLayout_7") - self.song_path_lineEdit = QLineEdit(self.layoutWidget_2) - self.song_path_lineEdit.setObjectName("song_path_lineEdit") + self.gridLayout.addWidget(self.save2tag_mode_comboBox, 1, 3, 1, 1) - self.horizontalLayout_7.addWidget(self.song_path_lineEdit) + self.label_10 = QLabel(self.control_bar) + self.label_10.setObjectName("label_10") + sizePolicy.setHeightForWidth(self.label_10.sizePolicy().hasHeightForWidth()) + self.label_10.setSizePolicy(sizePolicy) - self.song_path_pushButton = QPushButton(self.layoutWidget_2) - self.song_path_pushButton.setObjectName("song_path_pushButton") + self.gridLayout.addWidget(self.label_10, 4, 2, 1, 1) - self.horizontalLayout_7.addWidget(self.song_path_pushButton) + self.label_8 = QLabel(self.control_bar) + self.label_8.setObjectName("label_8") + sizePolicy.setHeightForWidth(self.label_8.sizePolicy().hasHeightForWidth()) + self.label_8.setSizePolicy(sizePolicy) - self.verticalLayout.addLayout(self.horizontalLayout_7) + self.gridLayout.addWidget(self.label_8, 3, 2, 1, 1) - self.groupBox = QGroupBox(self.layoutWidget_2) - self.groupBox.setObjectName("groupBox") - self.gridLayout = QGridLayout(self.groupBox) - self.gridLayout.setObjectName("gridLayout") - self.save_path_lineEdit = QLineEdit(self.groupBox) - self.save_path_lineEdit.setObjectName("save_path_lineEdit") + self.save_path_button = QPushButton(self.control_bar) + self.save_path_button.setObjectName("save_path_button") + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.save_path_button.sizePolicy().hasHeightForWidth()) + self.save_path_button.setSizePolicy(sizePolicy1) - self.gridLayout.addWidget(self.save_path_lineEdit, 1, 0, 1, 1) + self.gridLayout.addWidget(self.save_path_button, 0, 1, 5, 1) - self.save_path_pushButton = QPushButton(self.groupBox) - self.save_path_pushButton.setObjectName("save_path_pushButton") - sizePolicy1.setHeightForWidth(self.save_path_pushButton.sizePolicy().hasHeightForWidth()) - self.save_path_pushButton.setSizePolicy(sizePolicy1) + self.min_score_spinBox = QSpinBox(self.control_bar) + self.min_score_spinBox.setObjectName("min_score_spinBox") + self.min_score_spinBox.setMinimumSize(QSize(70, 0)) + self.min_score_spinBox.setMaximum(100) + self.min_score_spinBox.setValue(60) - self.gridLayout.addWidget(self.save_path_pushButton, 1, 1, 1, 1) + self.gridLayout.addWidget(self.min_score_spinBox, 0, 9, 1, 2) - self.gridLayout_7 = QGridLayout() - self.gridLayout_7.setObjectName("gridLayout_7") - self.label_8 = QLabel(self.groupBox) - self.label_8.setObjectName("label_8") + self.lyricsformat_comboBox = QComboBox(self.control_bar) + self.lyricsformat_comboBox.addItem("") + self.lyricsformat_comboBox.addItem("") + self.lyricsformat_comboBox.addItem("") + self.lyricsformat_comboBox.addItem("") + self.lyricsformat_comboBox.addItem("") + self.lyricsformat_comboBox.setObjectName("lyricsformat_comboBox") - self.gridLayout_7.addWidget(self.label_8, 1, 0, 1, 1) + self.gridLayout.addWidget(self.lyricsformat_comboBox, 4, 3, 1, 1) - self.label_11 = QLabel(self.groupBox) - self.label_11.setObjectName("label_11") - sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) + self.filename_mode_comboBox = QComboBox(self.control_bar) + self.filename_mode_comboBox.addItem("") + self.filename_mode_comboBox.addItem("") + self.filename_mode_comboBox.setObjectName("filename_mode_comboBox") + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) sizePolicy2.setHorizontalStretch(0) sizePolicy2.setVerticalStretch(0) - sizePolicy2.setHeightForWidth(self.label_11.sizePolicy().hasHeightForWidth()) - self.label_11.setSizePolicy(sizePolicy2) + sizePolicy2.setHeightForWidth(self.filename_mode_comboBox.sizePolicy().hasHeightForWidth()) + self.filename_mode_comboBox.setSizePolicy(sizePolicy2) + self.filename_mode_comboBox.setMinimumSize(QSize(133, 0)) + + self.gridLayout.addWidget(self.filename_mode_comboBox, 3, 3, 1, 1) - self.gridLayout_7.addWidget(self.label_11, 0, 0, 1, 1) + self.label_11 = QLabel(self.control_bar) + self.label_11.setObjectName("label_11") + sizePolicy1.setHeightForWidth(self.label_11.sizePolicy().hasHeightForWidth()) + self.label_11.setSizePolicy(sizePolicy1) + + self.gridLayout.addWidget(self.label_11, 0, 2, 1, 1) - self.save_mode_comboBox = QComboBox(self.groupBox) + self.save_mode_comboBox = QComboBox(self.control_bar) self.save_mode_comboBox.addItem("") self.save_mode_comboBox.addItem("") self.save_mode_comboBox.addItem("") self.save_mode_comboBox.setObjectName("save_mode_comboBox") - sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - sizePolicy3.setHorizontalStretch(0) - sizePolicy3.setVerticalStretch(0) - sizePolicy3.setHeightForWidth(self.save_mode_comboBox.sizePolicy().hasHeightForWidth()) - self.save_mode_comboBox.setSizePolicy(sizePolicy3) + sizePolicy2.setHeightForWidth(self.save_mode_comboBox.sizePolicy().hasHeightForWidth()) + self.save_mode_comboBox.setSizePolicy(sizePolicy2) self.save_mode_comboBox.setMinimumSize(QSize(194, 0)) + self.save_mode_comboBox.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToContents) - self.gridLayout_7.addWidget(self.save_mode_comboBox, 0, 1, 1, 1) - - self.lyrics_filename_mode_comboBox = QComboBox(self.groupBox) - self.lyrics_filename_mode_comboBox.addItem("") - self.lyrics_filename_mode_comboBox.addItem("") - self.lyrics_filename_mode_comboBox.setObjectName("lyrics_filename_mode_comboBox") - sizePolicy3.setHeightForWidth(self.lyrics_filename_mode_comboBox.sizePolicy().hasHeightForWidth()) - self.lyrics_filename_mode_comboBox.setSizePolicy(sizePolicy3) - self.lyrics_filename_mode_comboBox.setMinimumSize(QSize(133, 0)) - - self.gridLayout_7.addWidget(self.lyrics_filename_mode_comboBox, 1, 1, 1, 1) + self.gridLayout.addWidget(self.save_mode_comboBox, 0, 3, 1, 2) - self.gridLayout.addLayout(self.gridLayout_7, 0, 0, 1, 1) + self.label_2 = QLabel(self.control_bar) + self.label_2.setObjectName("label_2") + sizePolicy.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth()) + self.label_2.setSizePolicy(sizePolicy) - self.verticalLayout.addWidget(self.groupBox) + self.gridLayout.addWidget(self.label_2, 0, 5, 1, 1) - self.groupBox_2 = QGroupBox(self.layoutWidget_2) - self.groupBox_2.setObjectName("groupBox_2") - self.gridLayout_2 = QGridLayout(self.groupBox_2) - self.gridLayout_2.setObjectName("gridLayout_2") - self.horizontalLayout_5 = QHBoxLayout() - self.horizontalLayout_5.setObjectName("horizontalLayout_5") - self.gridLayout_4 = QGridLayout() - self.gridLayout_4.setObjectName("gridLayout_4") - self.min_score_spinBox = QSpinBox(self.groupBox_2) - self.min_score_spinBox.setObjectName("min_score_spinBox") - self.min_score_spinBox.setMinimumSize(QSize(70, 0)) - self.min_score_spinBox.setMaximum(100) - self.min_score_spinBox.setValue(60) + self.select_files_button = QPushButton(self.control_bar) + self.select_files_button.setObjectName("select_files_button") + sizePolicy1.setHeightForWidth(self.select_files_button.sizePolicy().hasHeightForWidth()) + self.select_files_button.setSizePolicy(sizePolicy1) - self.gridLayout_4.addWidget(self.min_score_spinBox, 0, 3, 1, 1, Qt.AlignmentFlag.AlignTop) + self.gridLayout.addWidget(self.select_files_button, 0, 0, 3, 1) - self.label_3 = QLabel(self.groupBox_2) - self.label_3.setObjectName("label_3") - sizePolicy2.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) - self.label_3.setSizePolicy(sizePolicy2) + self.label = QLabel(self.control_bar) + self.label.setObjectName("label") - self.gridLayout_4.addWidget(self.label_3, 0, 2, 1, 1, Qt.AlignmentFlag.AlignTop) + self.gridLayout.addWidget(self.label, 1, 2, 1, 1) - self.label_2 = QLabel(self.groupBox_2) - self.label_2.setObjectName("label_2") - sizePolicy2.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth()) - self.label_2.setSizePolicy(sizePolicy2) + self.select_dirs_button = QPushButton(self.control_bar) + self.select_dirs_button.setObjectName("select_dirs_button") + sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred) + sizePolicy3.setHorizontalStretch(0) + sizePolicy3.setVerticalStretch(0) + sizePolicy3.setHeightForWidth(self.select_dirs_button.sizePolicy().hasHeightForWidth()) + self.select_dirs_button.setSizePolicy(sizePolicy3) - self.gridLayout_4.addWidget(self.label_2, 0, 0, 1, 1, Qt.AlignmentFlag.AlignTop) + self.gridLayout.addWidget(self.select_dirs_button, 3, 0, 2, 1) - self.source_listWidget = CheckBoxListWidget(self.groupBox_2) + self.source_listWidget = CheckBoxListWidget(self.control_bar) self.source_listWidget.setObjectName("source_listWidget") - sizePolicy3.setHeightForWidth(self.source_listWidget.sizePolicy().hasHeightForWidth()) - self.source_listWidget.setSizePolicy(sizePolicy3) + sizePolicy.setHeightForWidth(self.source_listWidget.sizePolicy().hasHeightForWidth()) + self.source_listWidget.setSizePolicy(sizePolicy) self.source_listWidget.setMinimumSize(QSize(0, 0)) - self.source_listWidget.setMaximumSize(QSize(96, 64)) - self.source_listWidget.setDragDropMode(QAbstractItemView.DragDropMode.DragDrop) - self.source_listWidget.setDefaultDropAction(Qt.DropAction.MoveAction) - - self.gridLayout_4.addWidget(self.source_listWidget, 0, 1, 1, 1) + self.source_listWidget.setMaximumSize(QSize(96, 78)) + self.source_listWidget.setDragEnabled(False) + self.source_listWidget.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) + self.source_listWidget.setDefaultDropAction(Qt.DropAction.TargetMoveAction) - self.horizontalLayout_5.addLayout(self.gridLayout_4) + self.gridLayout.addWidget(self.source_listWidget, 1, 5, 4, 1) - self.gridLayout_2.addLayout(self.horizontalLayout_5, 0, 0, 1, 1) - - self.gridLayout_5 = QGridLayout() - self.gridLayout_5.setObjectName("gridLayout_5") - self.label_9 = QLabel(self.groupBox_2) + self.label_9 = QLabel(self.control_bar) self.label_9.setObjectName("label_9") - self.gridLayout_5.addWidget(self.label_9, 0, 0, 1, 1) + self.gridLayout.addWidget(self.label_9, 1, 7, 1, 1) - self.horizontalLayout_3 = QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.original_checkBox = QCheckBox(self.groupBox_2) + self.original_checkBox = QCheckBox(self.control_bar) self.original_checkBox.setObjectName("original_checkBox") self.original_checkBox.setChecked(True) - self.horizontalLayout_3.addWidget(self.original_checkBox) + self.gridLayout.addWidget(self.original_checkBox, 1, 8, 1, 1) - self.translate_checkBox = QCheckBox(self.groupBox_2) + self.translate_checkBox = QCheckBox(self.control_bar) self.translate_checkBox.setObjectName("translate_checkBox") - sizePolicy1.setHeightForWidth(self.translate_checkBox.sizePolicy().hasHeightForWidth()) - self.translate_checkBox.setSizePolicy(sizePolicy1) + sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + sizePolicy4.setHorizontalStretch(0) + sizePolicy4.setVerticalStretch(0) + sizePolicy4.setHeightForWidth(self.translate_checkBox.sizePolicy().hasHeightForWidth()) + self.translate_checkBox.setSizePolicy(sizePolicy4) self.translate_checkBox.setChecked(True) - self.horizontalLayout_3.addWidget(self.translate_checkBox, 0, Qt.AlignmentFlag.AlignLeft) + self.gridLayout.addWidget(self.translate_checkBox, 1, 9, 1, 1) - self.romanized_checkBox = QCheckBox(self.groupBox_2) + self.romanized_checkBox = QCheckBox(self.control_bar) self.romanized_checkBox.setObjectName("romanized_checkBox") - sizePolicy1.setHeightForWidth(self.romanized_checkBox.sizePolicy().hasHeightForWidth()) - self.romanized_checkBox.setSizePolicy(sizePolicy1) - - self.horizontalLayout_3.addWidget(self.romanized_checkBox, 0, Qt.AlignmentFlag.AlignLeft) + sizePolicy4.setHeightForWidth(self.romanized_checkBox.sizePolicy().hasHeightForWidth()) + self.romanized_checkBox.setSizePolicy(sizePolicy4) - self.gridLayout_5.addLayout(self.horizontalLayout_3, 0, 1, 1, 1) + self.gridLayout.addWidget(self.romanized_checkBox, 1, 10, 1, 1) - self.label_10 = QLabel(self.groupBox_2) - self.label_10.setObjectName("label_10") - sizePolicy2.setHeightForWidth(self.label_10.sizePolicy().hasHeightForWidth()) - self.label_10.setSizePolicy(sizePolicy2) - - self.gridLayout_5.addWidget(self.label_10, 1, 0, 1, 1) - - self.lyricsformat_comboBox = QComboBox(self.groupBox_2) - self.lyricsformat_comboBox.addItem("") - self.lyricsformat_comboBox.addItem("") - self.lyricsformat_comboBox.addItem("") - self.lyricsformat_comboBox.addItem("") - self.lyricsformat_comboBox.addItem("") - self.lyricsformat_comboBox.setObjectName("lyricsformat_comboBox") - - self.gridLayout_5.addWidget(self.lyricsformat_comboBox, 1, 1, 1, 1) - - self.gridLayout_2.addLayout(self.gridLayout_5, 1, 0, 1, 1) - - self.verticalLayout.addWidget(self.groupBox_2) - - self.start_cancel_pushButton = QPushButton(self.layoutWidget_2) + self.start_cancel_pushButton = QPushButton(self.control_bar) self.start_cancel_pushButton.setObjectName("start_cancel_pushButton") - - self.verticalLayout.addWidget(self.start_cancel_pushButton) - - self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) - - self.verticalLayout.addItem(self.verticalSpacer) - - self.verticalLayout_3.addLayout(self.verticalLayout) - - self.splitter.addWidget(self.layoutWidget_2) - self.layoutWidget_3 = QWidget(self.splitter) - self.layoutWidget_3.setObjectName("layoutWidget_3") - self.verticalLayout_8 = QVBoxLayout(self.layoutWidget_3) - self.verticalLayout_8.setObjectName("verticalLayout_8") - self.verticalLayout_8.setContentsMargins(0, 0, 0, 0) - self.plainTextEdit = QPlainTextEdit(self.layoutWidget_3) - self.plainTextEdit.setObjectName("plainTextEdit") - sizePolicy.setHeightForWidth(self.plainTextEdit.sizePolicy().hasHeightForWidth()) - self.plainTextEdit.setSizePolicy(sizePolicy) - self.plainTextEdit.setReadOnly(True) - - self.verticalLayout_8.addWidget(self.plainTextEdit) - - self.progressBar = QProgressBar(self.layoutWidget_3) + sizePolicy1.setHeightForWidth(self.start_cancel_pushButton.sizePolicy().hasHeightForWidth()) + self.start_cancel_pushButton.setSizePolicy(sizePolicy1) + + self.gridLayout.addWidget(self.start_cancel_pushButton, 3, 7, 2, 4) + + self.verticalLayout.addWidget(self.control_bar) + + self.songs_table = ProportionallyStretchedTableWidget(local_match) + if self.songs_table.columnCount() < 7: + self.songs_table.setColumnCount(7) + __qtablewidgetitem = QTableWidgetItem() + self.songs_table.setHorizontalHeaderItem(0, __qtablewidgetitem) + __qtablewidgetitem1 = QTableWidgetItem() + self.songs_table.setHorizontalHeaderItem(1, __qtablewidgetitem1) + __qtablewidgetitem2 = QTableWidgetItem() + self.songs_table.setHorizontalHeaderItem(2, __qtablewidgetitem2) + __qtablewidgetitem3 = QTableWidgetItem() + self.songs_table.setHorizontalHeaderItem(3, __qtablewidgetitem3) + __qtablewidgetitem4 = QTableWidgetItem() + self.songs_table.setHorizontalHeaderItem(4, __qtablewidgetitem4) + __qtablewidgetitem5 = QTableWidgetItem() + self.songs_table.setHorizontalHeaderItem(5, __qtablewidgetitem5) + __qtablewidgetitem6 = QTableWidgetItem() + self.songs_table.setHorizontalHeaderItem(6, __qtablewidgetitem6) + self.songs_table.setObjectName("songs_table") + sizePolicy5 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + sizePolicy5.setHorizontalStretch(0) + sizePolicy5.setVerticalStretch(0) + sizePolicy5.setHeightForWidth(self.songs_table.sizePolicy().hasHeightForWidth()) + self.songs_table.setSizePolicy(sizePolicy5) + self.songs_table.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.songs_table.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored) + self.songs_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.songs_table.setDragDropOverwriteMode(False) + self.songs_table.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.songs_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.songs_table.setShowGrid(False) + self.songs_table.setRowCount(0) + self.songs_table.horizontalHeader().setVisible(True) + self.songs_table.horizontalHeader().setCascadingSectionResizes(False) + self.songs_table.horizontalHeader().setDefaultSectionSize(100) + self.songs_table.horizontalHeader().setHighlightSections(True) + self.songs_table.horizontalHeader().setProperty("showSortIndicator", False) + self.songs_table.horizontalHeader().setStretchLastSection(False) + self.songs_table.verticalHeader().setVisible(True) + + self.verticalLayout.addWidget(self.songs_table) + + self.status_label = QLabel(local_match) + self.status_label.setObjectName("status_label") + sizePolicy6 = QSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred) + sizePolicy6.setHorizontalStretch(0) + sizePolicy6.setVerticalStretch(0) + sizePolicy6.setHeightForWidth(self.status_label.sizePolicy().hasHeightForWidth()) + self.status_label.setSizePolicy(sizePolicy6) + + self.verticalLayout.addWidget(self.status_label) + + self.progressBar = QProgressBar(local_match) self.progressBar.setObjectName("progressBar") - self.progressBar.setMaximum(0) - self.progressBar.setValue(0) self.progressBar.setOrientation(Qt.Orientation.Horizontal) self.progressBar.setInvertedAppearance(False) - self.verticalLayout_8.addWidget(self.progressBar) - - self.splitter.addWidget(self.layoutWidget_3) - - self.horizontalLayout_6.addWidget(self.splitter) + self.verticalLayout.addWidget(self.progressBar) self.retranslateUi(local_match) @@ -293,38 +274,58 @@ def setupUi(self, local_match): # setupUi def retranslateUi(self, local_match): - self.label.setText(QCoreApplication.translate("local_match", "\u672c\u5730\u5339\u914d", None)) - self.label_4.setText(QCoreApplication.translate("local_match", "\u4e3a\u672c\u5730\u6b4c\u66f2\u6587\u4ef6\u5339\u914d\u6b4c\u8bcd", None)) - self.song_path_pushButton.setText(QCoreApplication.translate("local_match", "\u9009\u62e9\u8981\u904d\u5386\u7684\u6587\u4ef6\u5939", None)) - self.groupBox.setTitle(QCoreApplication.translate("local_match", "\u4fdd\u5b58", None)) - self.save_path_pushButton.setText(QCoreApplication.translate("local_match", "\u9009\u62e9\u6587\u4ef6\u5939\u8def\u5f84", None)) + local_match.setWindowTitle("") + self.label_3.setText(QCoreApplication.translate("local_match", "\u6700\u4f4e\u5339\u914d\u5ea6(0~100):", None)) + self.save2tag_mode_comboBox.setItemText(0, QCoreApplication.translate("local_match", "\u53ea\u4fdd\u5b58\u5230\u6587\u4ef6", None)) + self.save2tag_mode_comboBox.setItemText( + 1, QCoreApplication.translate("local_match", "\u53ea\u4fdd\u5b58\u5230\u6b4c\u8bcd\u6807\u7b7e(\u975ecue)", None) + ) + self.save2tag_mode_comboBox.setItemText( + 2, QCoreApplication.translate("local_match", "\u4fdd\u5b58\u5230\u6b4c\u8bcd\u6807\u7b7e(\u975ecue)\u4e0e\u6587\u4ef6", None) + ) + + self.label_10.setText(QCoreApplication.translate("local_match", "\u6b4c\u8bcd\u683c\u5f0f:", None)) self.label_8.setText(QCoreApplication.translate("local_match", "\u6b4c\u8bcd\u6587\u4ef6\u540d:", None)) - self.label_11.setText(QCoreApplication.translate("local_match", "\u6b4c\u8bcd\u4fdd\u5b58\u6a21\u5f0f:", None)) + self.save_path_button.setText(QCoreApplication.translate("local_match", "\u9009\u62e9\u4fdd\u5b58\u8def\u5f84", None)) + self.lyricsformat_comboBox.setItemText(0, QCoreApplication.translate("local_match", "LRC(\u9010\u5b57)", None)) + self.lyricsformat_comboBox.setItemText(1, QCoreApplication.translate("local_match", "LRC(\u9010\u884c)", None)) + self.lyricsformat_comboBox.setItemText(2, QCoreApplication.translate("local_match", "\u589e\u5f3a\u578bLRC(ESLyric)", None)) + self.lyricsformat_comboBox.setItemText(3, QCoreApplication.translate("local_match", "SRT", None)) + self.lyricsformat_comboBox.setItemText(4, QCoreApplication.translate("local_match", "ASS", None)) + + self.filename_mode_comboBox.setItemText(0, QCoreApplication.translate("local_match", "\u4e0e\u8bbe\u7f6e\u4e2d\u7684\u683c\u5f0f\u76f8\u540c", None)) + self.filename_mode_comboBox.setItemText(1, QCoreApplication.translate("local_match", "\u4e0e\u6b4c\u66f2\u6587\u4ef6\u540d\u76f8\u540c", None)) + + self.label_11.setText(QCoreApplication.translate("local_match", "\u6b4c\u8bcd\u6587\u4ef6\u4fdd\u5b58\u6a21\u5f0f:", None)) self.save_mode_comboBox.setItemText( 0, QCoreApplication.translate("local_match", "\u4fdd\u5b58\u5230\u6b4c\u66f2\u6587\u4ef6\u5939\u7684\u955c\u50cf\u6587\u4ef6\u5939", None) ) self.save_mode_comboBox.setItemText(1, QCoreApplication.translate("local_match", "\u4fdd\u5b58\u5230\u6b4c\u66f2\u6587\u4ef6\u5939", None)) self.save_mode_comboBox.setItemText(2, QCoreApplication.translate("local_match", "\u4fdd\u5b58\u5230\u6307\u5b9a\u6587\u4ef6\u5939", None)) - self.lyrics_filename_mode_comboBox.setItemText( - 0, QCoreApplication.translate("local_match", "\u4e0e\u8bbe\u7f6e\u4e2d\u7684\u683c\u5f0f\u76f8\u540c", None) - ) - self.lyrics_filename_mode_comboBox.setItemText(1, QCoreApplication.translate("local_match", "\u4e0e\u6b4c\u66f2\u6587\u4ef6\u540d\u76f8\u540c", None)) - - self.groupBox_2.setTitle(QCoreApplication.translate("local_match", "\u6b4c\u8bcd", None)) - self.label_3.setText(QCoreApplication.translate("local_match", "\u6700\u4f4e\u5339\u914d\u5ea6(0~100):", None)) self.label_2.setText(QCoreApplication.translate("local_match", "\u6b4c\u8bcd\u6765\u6e90:", None)) + self.select_files_button.setText(QCoreApplication.translate("local_match", "\u9009\u62e9\u6587\u4ef6", None)) + self.label.setText(QCoreApplication.translate("local_match", "\u4fdd\u5b58\u5230\u6b4c\u8bcd\u6807\u7b7e:", None)) + self.select_dirs_button.setText(QCoreApplication.translate("local_match", "\u9009\u62e9\u6587\u4ef6\u5939", None)) self.label_9.setText(QCoreApplication.translate("local_match", "\u6b4c\u8bcd\u7c7b\u578b:", None)) self.original_checkBox.setText(QCoreApplication.translate("local_match", "\u539f\u6587", None)) self.translate_checkBox.setText(QCoreApplication.translate("local_match", "\u8bd1\u6587", None)) self.romanized_checkBox.setText(QCoreApplication.translate("local_match", "\u7f57\u9a6c\u97f3", None)) - self.label_10.setText(QCoreApplication.translate("local_match", "\u6b4c\u8bcd\u683c\u5f0f:", None)) - self.lyricsformat_comboBox.setItemText(0, QCoreApplication.translate("local_match", "LRC(\u9010\u5b57)", None)) - self.lyricsformat_comboBox.setItemText(1, QCoreApplication.translate("local_match", "LRC(\u9010\u884c)", None)) - self.lyricsformat_comboBox.setItemText(2, QCoreApplication.translate("local_match", "\u589e\u5f3a\u578bLRC(ESLyric)", None)) - self.lyricsformat_comboBox.setItemText(3, QCoreApplication.translate("local_match", "SRT", None)) - self.lyricsformat_comboBox.setItemText(4, QCoreApplication.translate("local_match", "ASS", None)) - self.start_cancel_pushButton.setText(QCoreApplication.translate("local_match", "\u5f00\u59cb\u5339\u914d", None)) + ___qtablewidgetitem = self.songs_table.horizontalHeaderItem(0) + ___qtablewidgetitem.setText(QCoreApplication.translate("local_match", "\u6b4c\u66f2\u540d", None)) + ___qtablewidgetitem1 = self.songs_table.horizontalHeaderItem(1) + ___qtablewidgetitem1.setText(QCoreApplication.translate("local_match", "\u827a\u672f\u5bb6", None)) + ___qtablewidgetitem2 = self.songs_table.horizontalHeaderItem(2) + ___qtablewidgetitem2.setText(QCoreApplication.translate("local_match", "\u4e13\u8f91", None)) + ___qtablewidgetitem3 = self.songs_table.horizontalHeaderItem(3) + ___qtablewidgetitem3.setText(QCoreApplication.translate("local_match", "\u6b4c\u66f2\u8def\u5f84", None)) + ___qtablewidgetitem4 = self.songs_table.horizontalHeaderItem(4) + ___qtablewidgetitem4.setText(QCoreApplication.translate("local_match", "\u65f6\u957f", None)) + ___qtablewidgetitem5 = self.songs_table.horizontalHeaderItem(5) + ___qtablewidgetitem5.setText(QCoreApplication.translate("local_match", "\u4fdd\u5b58\u8def\u5f84", None)) + ___qtablewidgetitem6 = self.songs_table.horizontalHeaderItem(6) + ___qtablewidgetitem6.setText(QCoreApplication.translate("local_match", "\u72b6\u6001", None)) + self.progressBar.setFormat(QCoreApplication.translate("local_match", "%v/%m %p%", None)) # retranslateUi diff --git a/LDDC/utils/enum.py b/LDDC/utils/enum.py index 03ac388..180eecf 100644 --- a/LDDC/utils/enum.py +++ b/LDDC/utils/enum.py @@ -87,6 +87,12 @@ class LocalMatchFileNameMode(Enum): SONG = 1 +class LocalMatchSave2TagMode(Enum): + ONLY_FILE = 0 # 不保存到歌词标签 + ONLY_TAG = 1 # 保存到歌词标签(非cue) + BOTH = 2 # 保存到歌词标签(非cue)与文件 + + class Direction(Enum): LEFT = 1 RIGHT = 2 diff --git a/LDDC/view/local_match.py b/LDDC/view/local_match.py index bdf3ccf..16ac85d 100644 --- a/LDDC/view/local_match.py +++ b/LDDC/view/local_match.py @@ -1,97 +1,258 @@ # SPDX-FileCopyrightText: Copyright (c) 2024 沉默の金 # SPDX-License-Identifier: GPL-3.0-only + import os -from PySide6.QtCore import Qt, Slot -from PySide6.QtWidgets import ( - QCheckBox, - QComboBox, - QFileDialog, - QLineEdit, - QListWidget, - QPushButton, - QWidget, -) +from PySide6.QtCore import QMimeData, Qt, QUrl, Signal, Slot +from PySide6.QtGui import QContextMenuEvent, QDesktopServices, QDragEnterEvent, QDropEvent, QKeyEvent +from PySide6.QtWidgets import QFileDialog, QMenu, QTableWidgetItem, QWidget from LDDC.backend.worker import LocalMatchWorker from LDDC.ui.local_match_ui import Ui_local_match from LDDC.utils.data import cfg -from LDDC.utils.enum import LocalMatchFileNameMode, LocalMatchSaveMode, LyricsFormat, Source +from LDDC.utils.enum import LocalMatchFileNameMode, LocalMatchSave2TagMode, LocalMatchSaveMode, LyricsFormat, Source +from LDDC.utils.paths import default_save_lyrics_dir from LDDC.utils.thread import threadpool +from LDDC.utils.utils import get_artist_str, get_local_match_save_path from .msg_box import MsgBox class LocalMatchWidget(QWidget, Ui_local_match): + search_song = Signal(dict) + def __init__(self) -> None: super().__init__() - - self.running = False - self.setupUi(self) + self.setAcceptDrops(True) # 启用拖放功能 self.connect_signals() - self.worker = None + self.songs_table.set_proportions([0.2, 0.1, 0.1, 0.3, 2, 0.3, 2]) # 设置列宽比例 + self.source_listWidget.set_soures(["QM", "KG"]) - self.save_mode_changed(self.save_mode_comboBox.currentIndex()) + self.taskids: dict[str, int] = { + "get_infos": 0, + "match_lyrics": 0, + } + self.workers: dict[int, LocalMatchWorker] = {} # 任务ID与工作线程的映射 + self.matching: bool = False # 是否正在匹配 + self.save_root_path: str | None = None # 保存根目录 + self.save_path_errors: set[str] = set() # 保存路径错误 - self.save_path_lineEdit.setText(cfg["default_save_path"]) + def connect_signals(self) -> None: + """连接信号与槽""" + self.select_files_button.clicked.connect(self.select_files) + self.select_dirs_button.clicked.connect(self.select_dirs) + self.save_path_button.clicked.connect(self.select_save_root_path) + self.start_cancel_pushButton.clicked.connect(self.start_cancel) - self.source_listWidget.set_soures(["QM", "KG"]) + self.save_mode_comboBox.currentIndexChanged.connect(self.update_save_paths) + self.filename_mode_comboBox.currentIndexChanged.connect(self.update_save_paths) + self.save2tag_mode_comboBox.currentIndexChanged.connect(self.update_save_paths) + self.lyricsformat_comboBox.currentIndexChanged.connect(self.update_save_paths) - def connect_signals(self) -> None: - self.song_path_pushButton.clicked.connect(lambda: self.select_path(self.song_path_lineEdit)) - self.save_path_pushButton.clicked.connect(lambda: self.select_path(self.save_path_lineEdit)) + @Slot() + def start_cancel(self) -> None: + """开始/取消按钮点击槽""" + if self.workers: + for worker in self.workers.values(): + worker.stop() + self.start_cancel_pushButton.setEnabled(False) + self.start_cancel_pushButton.setText(self.tr("取消中...")) + return - self.save_mode_comboBox.currentIndexChanged.connect(self.save_mode_changed) + if (LocalMatchSave2TagMode(self.save2tag_mode_comboBox.currentIndex()) in (LocalMatchSave2TagMode.ONLY_TAG, LocalMatchSave2TagMode.BOTH) and + LyricsFormat(self.lyricsformat_comboBox.currentIndex()) not in (LyricsFormat.VERBATIMLRC, + LyricsFormat.LINEBYLINELRC, + LyricsFormat.ENHANCEDLRC)): - self.start_cancel_pushButton.clicked.connect(self.start_cancel_button_clicked) + MsgBox.warning(self, self.tr("警告"), self.tr("歌曲标签中的歌词应为LRC格式")) + return + + langs = self.get_langs() + if not langs: + MsgBox.warning(self, self.tr("警告"), self.tr("请选择要匹配的语言")) + return + + if len(source := [Source[k] for k in self.source_listWidget.get_data()]) == 0: + MsgBox.warning(self, self.tr("警告"), self.tr("请选择至少一个源!")) + return - def retranslateUi(self, local_match: QWidget) -> None: - super().retranslateUi(local_match) - self.save_mode_changed(self.save_mode_comboBox.currentIndex()) - if self.running: - self.start_cancel_pushButton.setText(self.tr("取消匹配")) + if self.save_path_errors: + MsgBox.warning(self, self.tr("警告"), "\n".join(self.save_path_errors)) + return + + if not (infos := self.get_table_infos()): + MsgBox.warning(self, self.tr("警告"), self.tr("请选择要匹配的歌曲!")) + return - def select_path(self, path_line_edit: QLineEdit) -> None: + worker = LocalMatchWorker("match_lyrics", + self.taskids["match_lyrics"], + infos=infos, + save_mode=LocalMatchSaveMode(self.save_mode_comboBox.currentIndex()), + file_name_mode=LocalMatchFileNameMode(self.filename_mode_comboBox.currentIndex()), + save2tag_mode=LocalMatchSave2TagMode(self.save2tag_mode_comboBox.currentIndex()), + lyrics_format=LyricsFormat(self.lyricsformat_comboBox.currentIndex()), + langs=self.get_langs(), + save_root_path=self.save_root_path, + min_score=self.min_score_spinBox.value(), + source=source) + + worker.signals.finished.connect(self.match_lyrics_result_slot, Qt.ConnectionType.BlockingQueuedConnection) + worker.signals.progress.connect(self.update_progress_slot, Qt.ConnectionType.BlockingQueuedConnection) + threadpool.start(worker) + self.workers[self.taskids["match_lyrics"]] = worker + self.taskids["match_lyrics"] += 1 + self.start_cancel_pushButton.setText(self.tr("取消(匹配歌词)")) + + for widget in self.control_bar.children(): + if isinstance(widget, QWidget) and widget != self.start_cancel_pushButton: + widget.setEnabled(False) + self.matching = True + + self.update_save_paths() + for i in range(self.songs_table.rowCount()): + self.songs_table.setItem(i, 6, QTableWidgetItem(self.tr("未匹配"))) + + @Slot(list) + @Slot(str) + def get_infos(self, paths: list[str] | str | QMimeData) -> None: + """获取文件信息,并添加到表格中""" + if isinstance(paths, QMimeData): + mime = paths + + if isinstance(paths, str): + paths = [paths] + + if not paths: + return + + if isinstance(paths, list) and paths: + mime = QMimeData() + mime.setUrls([QUrl.fromLocalFile(path) for path in paths]) + + worker = LocalMatchWorker("get_infos", + self.taskids["get_infos"], + mime=mime) + worker.signals.finished.connect(self.get_infos_result_slot, Qt.ConnectionType.BlockingQueuedConnection) + worker.signals.progress.connect(self.update_progress_slot, Qt.ConnectionType.BlockingQueuedConnection) + threadpool.start(worker) + self.workers[self.taskids["get_infos"]] = worker + self.taskids["get_infos"] += 1 + self.start_cancel_pushButton.setText(self.tr("取消(获取文件信息)")) + + @Slot() + def select_files(self) -> None: + dialog = QFileDialog(self) + dialog.setWindowTitle(self.tr("选择要匹配的文件")) + dialog.setFileMode(QFileDialog.FileMode.ExistingFiles) + dialog.filesSelected.connect(self.get_infos) + dialog.open() + + @Slot() + def select_dirs(self) -> None: + dialog = QFileDialog(self) + dialog.setWindowTitle(self.tr("选择要遍历的文件夹")) + dialog.setFileMode(QFileDialog.FileMode.Directory) + dialog.fileSelected.connect(self.get_infos) + dialog.open() + + @Slot() + def select_save_root_path(self) -> None: + """选择保存根目录""" @Slot(str) - def file_selected(save_path: str) -> None: - path_line_edit.setText(os.path.normpath(save_path)) + def set_save_root_path(path: str) -> None: + self.save_root_path = os.path.abspath(path) + self.update_save_paths() dialog = QFileDialog(self) - dialog.setWindowTitle(self.tr("选择文件夹")) + dialog.setWindowTitle(self.tr("选择保存根目录")) dialog.setFileMode(QFileDialog.FileMode.Directory) - dialog.fileSelected.connect(file_selected) + dialog.fileSelected.connect(set_save_root_path) + dialog.setDirectory(default_save_lyrics_dir) dialog.open() - @Slot(int) - def save_mode_changed(self, index: int) -> None: - match index: - case 0: - self.save_path_lineEdit.setEnabled(True) - self.save_path_pushButton.setEnabled(True) - self.save_path_pushButton.setText(self.tr("选择镜像文件夹")) - case 1: - self.save_path_lineEdit.setEnabled(False) - self.save_path_pushButton.setEnabled(False) - self.save_path_pushButton.setText(self.tr("选择文件夹")) - case 2: - self.save_path_lineEdit.setEnabled(True) - self.save_path_pushButton.setEnabled(True) - self.save_path_pushButton.setText(self.tr("选择文件夹")) + def dragEnterEvent(self, event: QDragEnterEvent) -> None: + formats = event.mimeData().formats() + if (event.mimeData().hasUrls() or + 'application/x-qt-windows-mime;value="foobar2000_playable_location_format"' in formats or + 'application/x-qt-windows-mime;value="ACL.FileURIs"' in formats) and not self.matching: + event.acceptProposedAction() # 接受拖动操作 + else: + event.ignore() - @Slot() - def start_cancel_button_clicked(self) -> None: - if self.running: - # 取消 - if self.worker is not None: - self.worker.stop() - self.start_cancel_pushButton.setText(self.tr("正在取消...")) - return + def dropEvent(self, event: QDropEvent) -> None: + mime = QMimeData() - if not os.path.exists(self.song_path_lineEdit.text()): - MsgBox.warning(self, self.tr("警告"), self.tr("歌曲文件夹不存在!")) - return + # 复制一份数据以便在其他线程使用 + _mime = event.mimeData() + for f in _mime.formats(): + mime.setData(f, _mime.data(f)) + self.get_infos(mime) + + @Slot(dict) + def update_progress_slot(self, result: dict) -> None: + """处理更新进度条的结果 + + :param result: 更新进度条的结果 + """ + msg, progress, total = result["msg"], result["progress"], result["total"] + self.progressBar.setMaximum(total) + self.progressBar.setValue(progress) + self.status_label.setText(msg) + + if "current" in result: + match result["status"]: + case "成功": + self.songs_table.item(result["current"], 6).setForeground(Qt.GlobalColor.green) + self.songs_table.item(result["current"], 6).setText(self.tr("成功")) + if "save_path" in result: + info = self.songs_table.item(result["current"], 0).data(Qt.ItemDataRole.UserRole) + self.songs_table.item(result["current"], 6).setData(Qt.ItemDataRole.UserRole, + {"status": result["status"], "save_path": result["save_path"]}) + if (LocalMatchSave2TagMode(self.save2tag_mode_comboBox.currentIndex()) in (LocalMatchSave2TagMode.ONLY_TAG, + LocalMatchSave2TagMode.BOTH) and + info["type"] != "cue"): + + self.songs_table.setItem(result["current"], 5, QTableWidgetItem(f"{self.tr("保存到标签")} + {result['save_path']}")) + else: + self.songs_table.setItem(result["current"], 5, QTableWidgetItem(result["save_path"])) + case "跳过纯音乐": + self.songs_table.item(result["current"], 6).setForeground(Qt.GlobalColor.blue) + self.songs_table.item(result["current"], 6).setText(self.tr("跳过")) + self.songs_table.item(result["current"], 6).setData(Qt.ItemDataRole.UserRole, {"status": result["status"]}) + self.songs_table.item(result["current"], 6).setToolTip(self.tr("跳过纯音乐")) + + case _: + self.songs_table.item(result["current"], 6).setForeground(Qt.GlobalColor.red) + self.songs_table.item(result["current"], 6).setText(self.tr("失败")) + self.songs_table.item(result["current"], 6).setData(Qt.ItemDataRole.UserRole, {"status": result["status"]}) + match result["status"]: + case "没有找到符合要求的歌曲": + self.songs_table.item(result["current"], 6).setToolTip(self.tr("错误:没有找到符合要求的歌曲")) + case "搜索结果处理失败": + self.songs_table.item(result["current"], 6).setToolTip(self.tr("错误:搜索结果处理失败")) + case "没有足够的信息用于搜索": + self.songs_table.item(result["current"], 6).setToolTip(self.tr("错误:没有足够的信息用于搜索")) + case "保存歌词失败": + self.songs_table.item(result["current"], 6).setToolTip(self.tr("错误:保存歌词失败")) + case "超时": + self.songs_table.item(result["current"], 6).setToolTip(self.tr("错误:超时")) + + def get_table_infos(self) -> list[dict]: + """获取表格中的文件信息 + + :return: 文件信息列表 + """ + infos: list[dict] = [] + for row in range(self.songs_table.rowCount()): + item = self.songs_table.item(row, 0) + if item is not None: + infos.append(item.data(Qt.ItemDataRole.UserRole)) + return infos + + def get_langs(self) -> list[str]: + """获取选择的的语言""" lyric_langs = [] if self.original_checkBox.isChecked(): lyric_langs.append("orig") @@ -99,68 +260,172 @@ def start_cancel_button_clicked(self) -> None: lyric_langs.append("ts") if self.romanized_checkBox.isChecked(): lyric_langs.append("roma") - langs_order = [lang for lang in cfg["langs_order"] if lang in lyric_langs] - - if len(lyric_langs) == 0: - MsgBox.warning(self, self.tr("警告"), self.tr("请选择至少一种歌词语言!")) + return [lang for lang in cfg["langs_order"] if lang in lyric_langs] + @Slot() + def update_save_paths(self) -> None: + """更新保存路径""" save_mode = LocalMatchSaveMode(self.save_mode_comboBox.currentIndex()) + file_name_mode = LocalMatchFileNameMode(self.filename_mode_comboBox.currentIndex()) + save2tag_mode = LocalMatchSave2TagMode(self.save2tag_mode_comboBox.currentIndex()) + lyrics_format = LyricsFormat(self.lyricsformat_comboBox.currentIndex()) + langs = self.get_langs() + file_name_format = cfg["lyrics_file_name_fmt"] - flienmae_mode = LocalMatchFileNameMode(self.lyrics_filename_mode_comboBox.currentIndex()) + self.save_path_errors.clear() - if len(source := [Source[k] for k in self.source_listWidget.get_data()]) == 0: - MsgBox.warning(self, self.tr("警告"), self.tr("请选择至少一个源!")) - return + for row in range(self.songs_table.rowCount()): + info: dict = self.songs_table.item(row, 0).data(Qt.ItemDataRole.UserRole) + save_path_text = "" - self.running = True - self.plainTextEdit.setPlainText("") - self.start_cancel_pushButton.setText(self.tr("取消匹配")) - for item in self.findChildren(QWidget): - if isinstance(item, QLineEdit | QPushButton | QComboBox | QCheckBox | QListWidget) and item != self.start_cancel_pushButton: - item.setEnabled(False) - - self.worker = LocalMatchWorker( - { - "song_path": self.song_path_lineEdit.text(), - "save_path": self.save_path_lineEdit.text(), - "min_score": self.min_score_spinBox.value(), - "save_mode": save_mode, - "flienmae_mode": flienmae_mode, - "langs_order": langs_order, - "lyrics_format": LyricsFormat(self.lyricsformat_comboBox.currentIndex()), - "source": source, - }, - ) - self.worker.signals.error.connect(self.worker_error, Qt.ConnectionType.BlockingQueuedConnection) - self.worker.signals.finished.connect(self.worker_finished, Qt.ConnectionType.BlockingQueuedConnection) - self.worker.signals.massage.connect(self.worker_massage, Qt.ConnectionType.BlockingQueuedConnection) - self.worker.signals.progress.connect(self.change_progress, Qt.ConnectionType.BlockingQueuedConnection) - threadpool.startOnReservedThread(self.worker) + if save2tag_mode in (LocalMatchSave2TagMode.ONLY_TAG, LocalMatchSave2TagMode.BOTH) and info["type"] != "cue": + save_path_text += self.tr("保存到标签") + + if (info["type"] == "cue" or save2tag_mode != LocalMatchSave2TagMode.ONLY_TAG): + save_path = get_local_match_save_path(save_mode=save_mode, + file_name_mode=file_name_mode, + song_info=info, + lyrics_format=lyrics_format, + file_name_format=file_name_format, + langs=langs, + save_root_path=self.save_root_path, + allow_placeholder=True) + if isinstance(save_path, int): + match save_path: + case -1: + save_path = self.tr("需要指定保存路径") + case -2: + save_path = self.tr("错误(需要歌词信息)") + case -3: + save_path = self.tr("需要指定歌曲根目录") + case _: + save_path = self.tr("未知错误") + self.save_path_errors.add(save_path) + save_path_text += f"+ {save_path}" if save_path_text else save_path + + self.songs_table.setItem(row, 5, QTableWidgetItem(save_path_text)) @Slot(str) - def worker_massage(self, massage: str) -> None: - self.plainTextEdit.appendPlainText(massage) + def select_root_path(self, rows: list[int]) -> None: + @Slot(str) + def set_root_path(path: str) -> None: + skips = [] + for row in rows: + info: dict = self.songs_table.item(row, 0).data(Qt.ItemDataRole.UserRole) + # 获取绝对路径 + file_path = os.path.abspath(info["file_path"]) # 歌曲文件路径 + path = os.path.abspath(path) # 根目录路径 - @Slot(int, int) - def change_progress(self, current: int, maximum: int) -> None: - self.progressBar.setValue(current) - self.progressBar.setMaximum(maximum) + # 检查驱动器是否相同(仅在 Windows 上适用) + # 检查文件路径是否以目录路径为前缀 + if os.path.splitdrive(file_path)[0] == os.path.splitdrive(path)[0] and os.path.commonpath([file_path, path]) == path: + info["root_path"] = path + self.songs_table.item(row, 0).setData(Qt.ItemDataRole.UserRole, info) + else: + skips.append(info["file_path"]) - @Slot() - def worker_finished(self) -> None: - self.start_cancel_pushButton.setText(self.tr("开始匹配")) - self.running = False - for item in self.findChildren(QWidget): - if isinstance(item, QLineEdit | QPushButton | QComboBox | QCheckBox | QListWidget): - item.setEnabled(True) - self.save_mode_changed(self.save_mode_comboBox.currentIndex()) - self.progressBar.setValue(0) - self.progressBar.setMaximum(0) - - @Slot(str, int) - def worker_error(self, error: str, level: int) -> None: - if level == 0: - self.plainTextEdit.appendPlainText(error) + if skips: + MsgBox.warning(self, self.tr("警告"), self.tr("由于以下歌曲不在指定的根目录中,无法设置\n") + "\n".join(skips)) + + self.update_save_paths() + + dialog = QFileDialog(self) + dialog.setWindowTitle(self.tr("选择歌曲根目录")) + dialog.setFileMode(QFileDialog.FileMode.Directory) + dialog.setDirectory(os.path.dirname(self.songs_table.item(rows[0], 0).data(Qt.ItemDataRole.UserRole)["file_path"])) + dialog.fileSelected.connect(set_root_path) + dialog.open() + + def contextMenuEvent(self, event: QContextMenuEvent) -> None: + if not self.songs_table.geometry().contains(event.pos()): # 判断否在表格内点击 + return + selected_rows = self.songs_table.selectionModel().selectedRows() + if not selected_rows: + return + menu = QMenu(self) + if not self.matching: + menu.addAction(self.tr("删除"), lambda rows=selected_rows: [self.songs_table.removeRow(row.row()) for row in reversed(rows)]) + # 反向删除,防止删除行后影响后续行号 + menu.addAction(self.tr("指定根目录"), lambda rows=selected_rows: self.select_root_path([row.row() for row in rows])) + if len(selected_rows) == 1: + menu.addAction(self.tr("打开歌曲目录"), + lambda row=selected_rows[0]: QDesktopServices.openUrl( + QUrl.fromLocalFile(os.path.dirname(self.songs_table.item(row.row(), 0).data(Qt.ItemDataRole.UserRole)["file_path"])))) + menu.addAction(self.tr("在搜索中打开"), + lambda row=selected_rows[0]: self.search_song.emit(self.songs_table.item(row.row(), 0).data(Qt.ItemDataRole.UserRole))) + if ((completion_status := self.songs_table.item(selected_rows[0].row(), 6).data(Qt.ItemDataRole.UserRole)) and # 获取完成状态 + isinstance(completion_status, dict) and # 检查是否为字典 + (save_path := completion_status.get("save_path"))): # 获取保存路径 + menu.addAction(self.tr("打开保存目录"), + lambda save_path=save_path: QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.dirname(save_path)))) + menu.addAction(self.tr("打开歌词"), + lambda save_path=save_path: QDesktopServices.openUrl(QUrl.fromLocalFile(save_path))) + menu.exec_(event.globalPos()) + + def keyPressEvent(self, event: QKeyEvent) -> None: + if event.key() == Qt.Key.Key_Delete and not self.matching: + # 获取所有选中的行并删除 + selected_rows = {index.row() for index in self.songs_table.selectionModel().selectedRows()} + for row in sorted(selected_rows, reverse=True): + self.songs_table.removeRow(row) else: - MsgBox.critical(self, self.tr("错误"), error) - self.worker_finished() + super().keyPressEvent(event) + + @Slot(dict) + def get_infos_result_slot(self, result: dict) -> None: + """处理获取文件信息的结果 + + :param result: 获取文件信息的结果 + """ + del self.workers[result["taskid"]] + if not self.workers: + self.start_cancel_pushButton.setText(self.tr("开始")) + self.start_cancel_pushButton.setEnabled(True) + errors: list = result.get("errors", []) + if errors: + MsgBox.critical(self, self.tr("错误"), "\n".join(errors)) + + if result["status"] == "success": + infos: list[dict] = result["infos"] + existing_infos = self.get_table_infos() + for info in infos: + if info in existing_infos: + continue + i = self.songs_table.rowCount() + self.songs_table.insertRow(i) + item = QTableWidgetItem(info.get("title", "")) + item.setData(Qt.ItemDataRole.UserRole, info) + self.songs_table.setItem(i, 0, item) + self.songs_table.setItem(i, 1, QTableWidgetItem(get_artist_str(info.get("artist")))) + self.songs_table.setItem(i, 2, QTableWidgetItem(info.get("album", ""))) + self.songs_table.setItem(i, 3, QTableWidgetItem(info["file_path"])) + if info["duration"] is not None: + self.songs_table.setItem(i, 4, QTableWidgetItem('{:02d}:{:02d}'.format(*divmod(info['duration'], 60)))) + self.songs_table.setItem(i, 6, QTableWidgetItem(self.tr("未匹配"))) + self.update_save_paths() + + @Slot(dict) + def match_lyrics_result_slot(self, result: dict) -> None: + """处理匹配歌词的结果 + + :param result: 匹配歌词的结果 + """ + del self.workers[result["taskid"]] + self.update_progress_slot({"msg": "", "progress": 0, "total": 0}) + self.start_cancel_pushButton.setText(self.tr("开始")) + for widget in self.control_bar.children(): + if isinstance(widget, QWidget): + widget.setEnabled(True) + self.matching = False + + info_str = self.tr("总共{total}首歌曲,匹配成功{success}首,匹配失败{fail}首,跳过{skip}首。").format(total=result.get("total"), + success=result.get("success"), + fail=result.get("fail"), + skip=result.get("skip")) + match result["status"]: + case "success": + MsgBox.information(self, self.tr("匹配完成"), self.tr("匹配完成") + "\n" + info_str) + case "error": + MsgBox.critical(self, self.tr("匹配错误"), self.tr("匹配错误") + "\n" + info_str) + case "cancelled": + MsgBox.information(self, self.tr("匹配取消"), self.tr("匹配取消") + "\n" + info_str) diff --git a/LDDC/view/main_window.py b/LDDC/view/main_window.py index 53c4a24..ae3fc4a 100644 --- a/LDDC/view/main_window.py +++ b/LDDC/view/main_window.py @@ -57,10 +57,10 @@ def init_widgets(self) -> None: self.add_widget(self.tr("设置"), self.settings_widget, Direction.BOTTOM) def closeEvent(self, event: QCloseEvent) -> None: - if self.local_match_widget.running: + if self.local_match_widget.matching: def question_slot(but: QMessageBox.StandardButton) -> None: - if but == QMessageBox.StandardButton.Yes and self.local_match_widget.worker: - self.local_match_widget.worker.stop() + if but == QMessageBox.StandardButton.Yes and self.local_match_widget.workers: + self.local_match_widget.workers[0].stop() if exit_manager.window_close_event(self): self.destroy() self.deleteLater() @@ -82,6 +82,9 @@ def connect_signals(self) -> None: self.widget_changed.connect(self.settings_widget.update_cache_size) # 更新缓存大小 language_changed.connect(self.retranslateUi) + self.local_match_widget.search_song.connect(self.search_widget.auto_fetch) + self.local_match_widget.search_song.connect(lambda: self.set_current_widget(0)) + @Slot() def retranslateUi(self) -> None: self.search_widget.retranslateUi()