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
+
+ ProportionallyStretchedTableWidget
+ QTableWidget
+
+
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()