Replace Ready to Transfer list with sortable table (QTableWidget)
- Replace QListWidget with QTableWidget in Library tab - 6 columns: #, Artist, Title, Duration, Genre, Size - Track numbers displayed without leading zeros (1, 2, 3...) - Dash shown when track number is unknown - Sort by track number within album by default - Clickable column headers for sorting by any field - Alternating row colors for readability - Row selection (SelectRows behavior) - Artist and Title columns stretch to fill space - Add genre field to TrackInfo dataclass - Include genre in metadata extract_tags() return value - Extract genre and track_number during library scan
This commit is contained in:
parent
11e4d49e2b
commit
7477cfba66
114
src/main.py
114
src/main.py
@ -14,7 +14,8 @@ from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QPushButton, QLabel, QLineEdit, QProgressBar, QFileDialog,
|
||||
QComboBox, QCheckBox, QTabWidget, QListWidget, QListWidgetItem,
|
||||
QMessageBox, QSpinBox, QGroupBox, QHeaderView
|
||||
QMessageBox, QSpinBox, QGroupBox, QHeaderView,
|
||||
QTableWidget, QTableWidgetItem
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
|
||||
@ -326,9 +327,26 @@ class MainWindow(QMainWindow):
|
||||
self.ready_label = QLabel("Ready to Transfer:")
|
||||
layout.addWidget(self.ready_label)
|
||||
|
||||
self.library_ready_list = QListWidget()
|
||||
self.library_ready_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
|
||||
layout.addWidget(self.library_ready_list, stretch=2)
|
||||
self.library_ready_table = QTableWidget()
|
||||
self.library_ready_table.setColumnCount(6)
|
||||
self.library_ready_table.setHorizontalHeaderLabels(
|
||||
["#", "Artist", "Title", "Duration", "Genre", "Size"]
|
||||
)
|
||||
self.library_ready_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self.library_ready_table.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection)
|
||||
self.library_ready_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self.library_ready_table.setAlternatingRowColors(True)
|
||||
self.library_ready_table.setSortingEnabled(True)
|
||||
|
||||
header = self.library_ready_table.horizontalHeader()
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
|
||||
|
||||
layout.addWidget(self.library_ready_table, stretch=2)
|
||||
|
||||
self.source_label = QLabel("Source Files — Need Conversion:")
|
||||
layout.addWidget(self.source_label)
|
||||
@ -487,11 +505,14 @@ class MainWindow(QMainWindow):
|
||||
if not output_dir or not os.path.isdir(output_dir):
|
||||
self.library_ready.clear()
|
||||
self._refresh_library_ui()
|
||||
self._refresh_source_ui([])
|
||||
return
|
||||
|
||||
ready = []
|
||||
sources = []
|
||||
|
||||
handler = MetadataHandler()
|
||||
|
||||
for root, _dirs, files in os.walk(output_dir):
|
||||
for fname in sorted(files):
|
||||
fpath = os.path.join(root, fname)
|
||||
@ -502,15 +523,18 @@ class MainWindow(QMainWindow):
|
||||
if ext in AUDIO_EXTS:
|
||||
size = os.path.getsize(fpath)
|
||||
try:
|
||||
handler = MetadataHandler()
|
||||
info = handler.extract_tags(fpath)
|
||||
duration = info.duration if info else 0
|
||||
artist = info.artist if info else "Unknown"
|
||||
title = info.title if info else fname
|
||||
genre = info.genre if info else ""
|
||||
track_num = info.track_number if info else 0
|
||||
except Exception:
|
||||
duration = 0
|
||||
artist = "Unknown"
|
||||
title = fname
|
||||
genre = ""
|
||||
track_num = 0
|
||||
|
||||
ready.append({
|
||||
"path": fpath,
|
||||
@ -518,22 +542,26 @@ class MainWindow(QMainWindow):
|
||||
"artist": artist,
|
||||
"duration_s": duration,
|
||||
"size": size,
|
||||
"genre": genre,
|
||||
"track_num": track_num,
|
||||
})
|
||||
elif ext in SOURCE_EXTS:
|
||||
size = os.path.getsize(fpath)
|
||||
try:
|
||||
handler = MetadataHandler()
|
||||
info = handler.extract_tags(fpath)
|
||||
title = info.title if info else fname
|
||||
artist = info.artist if info else "Unknown"
|
||||
track_num = info.track_number if info else 0
|
||||
except Exception:
|
||||
title = fname
|
||||
artist = "Unknown"
|
||||
track_num = 0
|
||||
sources.append({
|
||||
"path": fpath,
|
||||
"title": title,
|
||||
"artist": artist,
|
||||
"size": size,
|
||||
"track_num": track_num,
|
||||
})
|
||||
|
||||
self.library_ready = ready
|
||||
@ -541,20 +569,36 @@ class MainWindow(QMainWindow):
|
||||
self._refresh_source_ui(sources)
|
||||
|
||||
def _refresh_library_ui(self):
|
||||
self.library_ready_list.clear()
|
||||
for track in sorted(self.library_ready, key=lambda t: (t["artist"], t["title"])):
|
||||
display = (
|
||||
f"{track['artist']} — {track['title']}"
|
||||
f" {self._format_duration(track['duration_s'])}"
|
||||
f" {self._format_size(track['size'])}"
|
||||
)
|
||||
item = QListWidgetItem(display)
|
||||
item.setData(Qt.ItemDataRole.UserRole, track)
|
||||
self.library_ready_list.addItem(item)
|
||||
self.library_ready_table.setRowCount(0)
|
||||
self.library_ready_table.setSortingEnabled(False)
|
||||
|
||||
for track in sorted(
|
||||
self.library_ready,
|
||||
key=lambda t: (t.get("track_num") or 0, t["artist"].lower(), t["title"].lower())
|
||||
):
|
||||
row = self.library_ready_table.rowCount()
|
||||
self.library_ready_table.insertRow(row)
|
||||
|
||||
track_num_str = str(track["track_num"]) if track.get("track_num") else "—"
|
||||
|
||||
cells = [
|
||||
QTableWidgetItem(track_num_str),
|
||||
QTableWidgetItem(track["artist"]),
|
||||
QTableWidgetItem(track["title"]),
|
||||
QTableWidgetItem(self._format_duration(track["duration_s"])),
|
||||
QTableWidgetItem(track.get("genre", "") or ""),
|
||||
QTableWidgetItem(self._format_size(track["size"])),
|
||||
]
|
||||
|
||||
cells[0].setData(Qt.ItemDataRole.UserRole, track)
|
||||
for col, item in enumerate(cells):
|
||||
self.library_ready_table.setItem(row, col, item)
|
||||
|
||||
self.library_ready_table.setSortingEnabled(True)
|
||||
|
||||
count = len(self.library_ready)
|
||||
self.ready_label.setVisible(count > 0)
|
||||
self.library_ready_list.setVisible(count > 0)
|
||||
self.library_ready_table.setVisible(count > 0)
|
||||
if count:
|
||||
self.library_status.setText(f"{count} track(s) ready to transfer")
|
||||
self.library_transfer_btn.setEnabled(True)
|
||||
@ -615,21 +659,29 @@ class MainWindow(QMainWindow):
|
||||
self.library_convert_btn.setEnabled(count > 0)
|
||||
|
||||
def _get_selected_ready_tracks(self) -> List[Tuple[TrackInfo, str]]:
|
||||
selected = self.library_ready_list.selectedItems()
|
||||
if not selected:
|
||||
selected = [self.library_ready_list.item(i) for i in range(self.library_ready_list.count())]
|
||||
selected_rows = set()
|
||||
for item in self.library_ready_table.selectedItems():
|
||||
selected_rows.add(item.row())
|
||||
|
||||
if not selected_rows:
|
||||
selected_rows = set(range(self.library_ready_table.rowCount()))
|
||||
|
||||
tracks = []
|
||||
for item in selected:
|
||||
data = item.data(Qt.ItemDataRole.UserRole)
|
||||
for row in sorted(selected_rows):
|
||||
data_item = self.library_ready_table.item(row, 0)
|
||||
if data_item is None:
|
||||
continue
|
||||
data = data_item.data(Qt.ItemDataRole.UserRole)
|
||||
if data:
|
||||
track_info = TrackInfo(
|
||||
video_id=f"local_{hash(data['path'])}",
|
||||
title=data['title'],
|
||||
artist=data['artist'],
|
||||
album="",
|
||||
album=data.get("album", ""),
|
||||
thumbnail_url="",
|
||||
duration=data.get('duration_s', 0),
|
||||
track_number=data.get('track_num'),
|
||||
genre=data.get('genre'),
|
||||
download_path=data['path'],
|
||||
)
|
||||
tracks.append((track_info, data['path']))
|
||||
@ -677,8 +729,20 @@ class MainWindow(QMainWindow):
|
||||
def _on_library_remove_selected(self):
|
||||
"""Delete selected files from disk and clean up empty directories."""
|
||||
files_to_delete = []
|
||||
for lst in (self.library_ready_list, self.library_source_list):
|
||||
for item in lst.selectedItems():
|
||||
|
||||
selected_rows = set()
|
||||
for item in self.library_ready_table.selectedItems():
|
||||
selected_rows.add(item.row())
|
||||
|
||||
for row in selected_rows:
|
||||
data_item = self.library_ready_table.item(row, 0)
|
||||
if data_item is None:
|
||||
continue
|
||||
data = data_item.data(Qt.ItemDataRole.UserRole)
|
||||
if data and os.path.exists(data.get("path", "")):
|
||||
files_to_delete.append(data)
|
||||
|
||||
for item in self.library_source_list.selectedItems():
|
||||
data = item.data(Qt.ItemDataRole.UserRole)
|
||||
if data and os.path.exists(data.get("path", "")):
|
||||
files_to_delete.append(data)
|
||||
|
||||
@ -356,6 +356,7 @@ class MetadataHandler:
|
||||
thumbnail_url="",
|
||||
duration=duration,
|
||||
track_number=track_number if track_number else None,
|
||||
genre=genre,
|
||||
download_path=file_path,
|
||||
)
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ class TrackInfo:
|
||||
playlist_title: Optional[str] = None
|
||||
playlist_index: Optional[int] = None
|
||||
download_path: Optional[str] = None
|
||||
genre: Optional[str] = None
|
||||
|
||||
|
||||
class YouTubeDownloader:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user