diff options
Diffstat (limited to 'dialogs/album_info_dialog.py')
| -rw-r--r-- | dialogs/album_info_dialog.py | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/dialogs/album_info_dialog.py b/dialogs/album_info_dialog.py new file mode 100644 index 0000000..7ed5833 --- /dev/null +++ b/dialogs/album_info_dialog.py @@ -0,0 +1,204 @@ +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import ( + QAbstractItemView, + QCheckBox, + QDialog, + QHBoxLayout, + QHeaderView, + QLabel, + QLineEdit, + QListWidget, + QListWidgetItem, + QPushButton, + QSplitter, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, + QWidget, +) + +import covers +from ui_common import ProgressStrip, Task, section_label + + +class AlbumInfoDialog(QDialog): + def __init__(self, album: str, artist: str, count: int, parent=None): + super().__init__(parent) + self.setWindowTitle(f"Album Info · {count} track(s)") + self.resize(900, 640) + self.result_info: dict | None = None + self._task: Task | None = None + self._releases: list[dict] = [] + + self.album = QLineEdit(album, placeholderText="Album") + self.artist = QLineEdit(artist, placeholderText="Artist (optional)") + for edit in (self.album, self.artist): + edit.returnPressed.connect(self.search) + search = QPushButton("Search") + search.clicked.connect(self.search) + top = QHBoxLayout() + top.addWidget(self.album, 3) + top.addWidget(self.artist, 2) + top.addWidget(search) + + self.results = QListWidget() + self.results.setAlternatingRowColors(True) + self.results.itemDoubleClicked.connect(self._apply) + self.results.currentItemChanged.connect(self._on_release_changed) + + self.tracks = QTableWidget(0, 3) + self.tracks.setHorizontalHeaderLabels(["#", "Title", "Artist"]) + self.tracks.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + self.tracks.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + self.tracks.verticalHeader().setVisible(False) + self.tracks.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.tracks.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + + lists = QSplitter(Qt.Orientation.Horizontal) + releases_box = QWidget() + releases_layout = QVBoxLayout(releases_box) + releases_layout.setContentsMargins(0, 0, 0, 0) + releases_layout.addWidget(section_label("Releases")) + releases_layout.addWidget(self.results) + tracks_box = QWidget() + tracks_layout = QVBoxLayout(tracks_box) + tracks_layout.setContentsMargins(0, 0, 0, 0) + tracks_layout.addWidget(section_label("Tracklist")) + tracks_layout.addWidget(self.tracks) + lists.addWidget(releases_box) + lists.addWidget(tracks_box) + lists.setSizes([380, 520]) + + self.tag_titles = QCheckBox("Tag titles / artists by track number") + self.tag_titles.setChecked(True) + self.tag_titles.setToolTip("Match each row's track number against the MusicBrainz tracklist\n" + "and fill in TITLE and ARTIST") + self.status = QLabel() + self.apply_btn = QPushButton(f"Apply to {count} Track(s)") + self.apply_btn.setDefault(True) + self.apply_btn.setEnabled(False) + self.apply_btn.clicked.connect(self._apply) + cancel = QPushButton("Cancel") + cancel.clicked.connect(self.reject) + bottom = QHBoxLayout() + bottom.addWidget(self.tag_titles) + bottom.addWidget(self.status, 1) + bottom.addWidget(self.apply_btn) + bottom.addWidget(cancel) + + self.progress = ProgressStrip() + + layout = QVBoxLayout(self) + layout.addLayout(top) + layout.addWidget(self.progress) + layout.addWidget(lists, 1) + layout.addLayout(bottom) + + if album.strip(): + self.search() + + def search(self): + album, artist = self.album.text().strip(), self.artist.text().strip() + if not album: + self.status.setText("Enter an album name to search.") + return + self.results.clear() + self.tracks.setRowCount(0) + self._releases = [] + self.status.clear() + self.apply_btn.setEnabled(False) + self.progress.start("Searching MusicBrainz") + self.progress.set_detail(f"{album} - {artist}" if artist else album) + + def run(_task): + return covers.search_releases(album, artist) + + self._task = Task(run) + self._task.done.connect(self._on_results) + self._task.failed.connect(self._on_failed) + self._task.start() + + def _stale(self) -> bool: + return self.sender() is not self._task + + def _on_results(self, releases): + if self._stale(): + return + self.progress.finish() + self._releases = list(releases) + for rel in self._releases: + label = f"{rel['title']} — {rel['artist']}" + item = QListWidgetItem(f"{label}\n{rel['details']}" if rel["details"] else label) + item.setToolTip(f"{label}\n{rel['details']}\nMBID: {rel['id']}") + self.results.addItem(item) + if self._releases: + self.status.setText(f"{len(self._releases)} release(s) found.") + self.results.setCurrentRow(0) + else: + self.status.setText("No releases found.") + + def _on_failed(self, message): + if not self._stale(): + self.progress.finish() + self.status.setText(f"Search failed: {message}") + + def _current_release(self) -> dict | None: + row = self.results.currentRow() + return self._releases[row] if 0 <= row < len(self._releases) else None + + def _on_release_changed(self, *_): + release = self._current_release() + self.tracks.setRowCount(0) + self.apply_btn.setEnabled(bool(release)) + if not release: + return + self.progress.start("Loading tracklist") + self.progress.set_detail(release["title"]) + mbid = release["id"] + + def run(_task): + return mbid, covers.fetch_release_tracks(mbid) + + self._task = Task(run) + self._task.done.connect(self._on_tracks) + self._task.failed.connect(self._on_failed) + self._task.start() + + def _on_tracks(self, payload): + if self._stale(): + return + self.progress.finish() + mbid, tracks = payload + release = self._current_release() + if not release or release["id"] != mbid: + return + self.tracks.setRowCount(len(tracks)) + for row, track in enumerate(tracks): + number = f"{track['disc']}-{track['number']}" if track["disc"] > 1 else str(track["number"]) + for col, text in enumerate((number, track["title"], track["artist"])): + self.tracks.setItem(row, col, QTableWidgetItem(text)) + self.status.setText(f"{len(tracks)} track(s) in {release['title']}.") + + def _apply(self, *_): + release = self._current_release() + if not release: + return + by_number: dict[int, tuple[str, str]] = {} + for row in range(self.tracks.rowCount()): + number_item = self.tracks.item(row, 0) + title_item = self.tracks.item(row, 1) + artist_item = self.tracks.item(row, 2) + if not number_item: + continue + number = number_item.text().split("-")[-1] + if number.isdigit(): + by_number[int(number)] = (title_item.text() if title_item else "", + artist_item.text() if artist_item else "") + self.result_info = { + "album": release["title"], + "album_artist": release["artist"], + "mbid": release["id"], + "tag_tracks": self.tag_titles.isChecked(), + "by_number": by_number, + } + self.accept() |
