feat: download tracks with full ID3 metadata
- Add downloads.py: "Artist - Title.mp3" naming, no-overwrite dedupe, configurable download_dir, progress and per-item callbacks. - Save maximum metadata via tags.py (ID3v2.3, mutagen): artists, album and album artist, track/disc, year/date, raw genre, label, duration, explicit flag, cover 600x600, plain and synced lyrics (MTS then LRCLIB), track URLs and Yandex/MTS ids. - TUI/Qt: download current track and queue from the menu, "d" key in lists, folder picker in settings; CLI download gains -d/--dir and --no-tags. - Add mutagen to requirements and document download/metadata in README. - Fix MPRIS update_metadata crashing without dbus-next.
This commit is contained in:
123
qt_app.py
123
qt_app.py
@@ -44,6 +44,7 @@ from PySide6.QtWidgets import (
|
||||
QAbstractButton,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QFileDialog,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@@ -313,6 +314,53 @@ class Task(QRunnable):
|
||||
pass
|
||||
|
||||
|
||||
class DownloadTask(QRunnable):
|
||||
"""Скачивает список треков в фоне и сообщает о прогрессе."""
|
||||
|
||||
class Signals(QObject):
|
||||
progress = Signal(str)
|
||||
done = Signal(int, int, str)
|
||||
failed = Signal(str)
|
||||
|
||||
def __init__(self, api, tracks: list[dict], dest: str) -> None:
|
||||
super().__init__()
|
||||
self.api = api
|
||||
self.tracks = tracks
|
||||
self.dest = dest
|
||||
self.signals = DownloadTask.Signals()
|
||||
self.setAutoDelete(False)
|
||||
|
||||
def run(self) -> None:
|
||||
from downloads import download_many
|
||||
|
||||
def on_item(index: int, count: int, track: dict) -> None:
|
||||
title = str(track.get("title") or "")
|
||||
self._emit(f"Скачиваю {index}/{count}: {title}")
|
||||
|
||||
try:
|
||||
saved = download_many(
|
||||
self.api, self.tracks, self.dest, on_item=on_item)
|
||||
except Exception as exc: # noqa: BLE001 - сообщение показываем в UI
|
||||
self._emit_failed(str(exc) or exc.__class__.__name__)
|
||||
else:
|
||||
try:
|
||||
self.signals.done.emit(len(saved), len(self.tracks), self.dest)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def _emit(self, text: str) -> None:
|
||||
try:
|
||||
self.signals.progress.emit(text)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def _emit_failed(self, message: str) -> None:
|
||||
try:
|
||||
self.signals.failed.emit(message)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
|
||||
class CoverView(QWidget):
|
||||
"""Обложка со скруглениями и аккуратным плейсхолдером."""
|
||||
|
||||
@@ -569,6 +617,7 @@ class QtMusicApp(QMainWindow):
|
||||
self.pool = QThreadPool(self)
|
||||
self.pool.setMaxThreadCount(4)
|
||||
self.covers = QNetworkAccessManager(self)
|
||||
self._download_tasks: set[DownloadTask] = set()
|
||||
|
||||
self.player = QMediaPlayer(self)
|
||||
self.audio = QAudioOutput(self)
|
||||
@@ -1006,6 +1055,22 @@ class QtMusicApp(QMainWindow):
|
||||
self.wave_check.toggled.connect(self.on_wave_from_history_toggled)
|
||||
layout.addWidget(self.wave_check)
|
||||
|
||||
dir_row = QHBoxLayout()
|
||||
dir_row.setSpacing(10)
|
||||
dir_label = QLabel("Папка загрузок")
|
||||
dir_label.setObjectName("settingLabel")
|
||||
self.download_dir_edit = QLineEdit(
|
||||
str(self.settings.get("download_dir") or ""))
|
||||
self.download_dir_edit.setObjectName("settingCombo")
|
||||
self.download_dir_edit.setReadOnly(True)
|
||||
dir_button = QPushButton("Выбрать…")
|
||||
dir_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
dir_button.clicked.connect(self.choose_download_dir)
|
||||
dir_row.addWidget(dir_label)
|
||||
dir_row.addWidget(self.download_dir_edit, 1)
|
||||
dir_row.addWidget(dir_button)
|
||||
layout.addLayout(dir_row)
|
||||
|
||||
hint = QLabel("Настройки сохраняются в .mts-music/settings.json")
|
||||
hint.setObjectName("pageStatus")
|
||||
hint.setWordWrap(True)
|
||||
@@ -1043,6 +1108,62 @@ class QtMusicApp(QMainWindow):
|
||||
def on_wave_from_history_toggled(self, checked: bool) -> None:
|
||||
self.update_setting("wave_from_history", checked)
|
||||
|
||||
def choose_download_dir(self) -> None:
|
||||
current = str(self.settings.get("download_dir") or "")
|
||||
chosen = QFileDialog.getExistingDirectory(
|
||||
self, "Папка загрузок", current)
|
||||
if not chosen:
|
||||
return
|
||||
self.update_setting("download_dir", chosen)
|
||||
self.download_dir_edit.setText(chosen)
|
||||
self.toast(f"Папка загрузок: {chosen}")
|
||||
|
||||
# -- скачивание -------------------------------------------------------
|
||||
|
||||
def download_current(self) -> None:
|
||||
self.close_drawer()
|
||||
if not self.current:
|
||||
self.toast("Сначала выбери трек")
|
||||
return
|
||||
self.download_tracks([self.current], "трек")
|
||||
|
||||
def download_queue(self) -> None:
|
||||
self.close_drawer()
|
||||
tracks = self.queue or ([self.current] if self.current else [])
|
||||
if not tracks:
|
||||
self.toast("Список пуст")
|
||||
return
|
||||
self.download_tracks(tracks, "список")
|
||||
|
||||
def download_tracks(self, tracks: list[dict], label: str) -> None:
|
||||
if not tracks:
|
||||
return
|
||||
dest = str(self.settings.get("download_dir") or "")
|
||||
task = DownloadTask(self.api, list(tracks), dest)
|
||||
task.signals.progress.connect(self.set_top_status)
|
||||
task.signals.done.connect(
|
||||
lambda saved, total, folder, t=task:
|
||||
self._download_done(t, saved, total, folder))
|
||||
task.signals.failed.connect(
|
||||
lambda message, t=task: self._download_failed(t, message))
|
||||
self._download_tasks.add(task)
|
||||
self.set_top_status(f"Скачиваю {label}…")
|
||||
self.pool.start(task)
|
||||
|
||||
def _download_done(
|
||||
self, task: DownloadTask, saved: int, total: int, dest: str
|
||||
) -> None:
|
||||
self._download_tasks.discard(task)
|
||||
self.set_top_status(self.queue_name)
|
||||
if saved:
|
||||
self.toast(f"Скачано {saved}/{total} → {dest}")
|
||||
else:
|
||||
self.toast("Не удалось скачать")
|
||||
|
||||
def _download_failed(self, task: DownloadTask, message: str) -> None:
|
||||
self._download_tasks.discard(task)
|
||||
self.on_error(message)
|
||||
|
||||
def show_settings(self) -> None:
|
||||
self._show(self.settings_page)
|
||||
|
||||
@@ -1071,6 +1192,8 @@ class QtMusicApp(QMainWindow):
|
||||
("История", self.show_history),
|
||||
("Поиск", self.show_search),
|
||||
("Текст песни", self.show_lyrics),
|
||||
("Скачать трек", self.download_current),
|
||||
("Скачать список", self.download_queue),
|
||||
("Настройки", self.show_settings),
|
||||
):
|
||||
button = QPushButton(text)
|
||||
|
||||
Reference in New Issue
Block a user