Redesign Library tab and add Download tab toggle
Library tab redesign: - Merge 'Ready to Transfer' table and 'Source Files' list into one unified table - Replace bottom buttons with top toolbar (Add Files, Convert, Transfer, Remove, Refresh, Search) - Add search/filter bar to filter tracks by title - Add context menu (right-click): Play, Convert/Transfer, Show in File Manager, Remove - Source tracks shown in gray (dimmed), ready tracks in normal color - Remove status column with emoji — clean iTunes-like layout Settings: - Add 'Show YouTube Download Tab' checkbox in Advanced section - Download tab hidden by default, can be enabled in Settings - Uses QTabWidget.setTabVisible() to toggle without destroying widgets
This commit is contained in:
parent
885a401ad0
commit
8c576232b1
381
src/main.py
381
src/main.py
@ -15,7 +15,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QPushButton, QLabel, QLineEdit, QProgressBar, QFileDialog,
|
QPushButton, QLabel, QLineEdit, QProgressBar, QFileDialog,
|
||||||
QComboBox, QCheckBox, QTabWidget, QListWidget, QListWidgetItem,
|
QComboBox, QCheckBox, QTabWidget, QListWidget, QListWidgetItem,
|
||||||
QMessageBox, QSpinBox, QGroupBox, QHeaderView,
|
QMessageBox, QSpinBox, QGroupBox, QHeaderView,
|
||||||
QTableWidget, QTableWidgetItem, QSlider
|
QTableWidget, QTableWidgetItem, QSlider, QMenu
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl
|
||||||
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
||||||
@ -219,8 +219,10 @@ class MainWindow(QMainWindow):
|
|||||||
self.downloaded_tracks: List[TrackInfo] = []
|
self.downloaded_tracks: List[TrackInfo] = []
|
||||||
self.library_ready: List[Dict] = []
|
self.library_ready: List[Dict] = []
|
||||||
self.library_source_files: List[str] = []
|
self.library_source_files: List[str] = []
|
||||||
|
self.library_scanned_sources: List[Dict] = []
|
||||||
self.ipod_devices: List[Dict] = []
|
self.ipod_devices: List[Dict] = []
|
||||||
self.current_mount_point: Optional[str] = None
|
self.current_mount_point: Optional[str] = None
|
||||||
|
self.show_download_tab = False
|
||||||
|
|
||||||
self.player: Optional[QMediaPlayer] = None
|
self.player: Optional[QMediaPlayer] = None
|
||||||
self.audio_output: Optional[QAudioOutput] = None
|
self.audio_output: Optional[QAudioOutput] = None
|
||||||
@ -248,25 +250,28 @@ class MainWindow(QMainWindow):
|
|||||||
main_widget = QWidget()
|
main_widget = QWidget()
|
||||||
main_layout = QVBoxLayout(main_widget)
|
main_layout = QVBoxLayout(main_widget)
|
||||||
|
|
||||||
tab_widget = QTabWidget()
|
self.tab_widget = QTabWidget()
|
||||||
main_layout.addWidget(tab_widget)
|
main_layout.addWidget(self.tab_widget)
|
||||||
|
|
||||||
download_tab = QWidget()
|
download_tab = QWidget()
|
||||||
tab_widget.addTab(download_tab, "Download")
|
self.tab_widget.addTab(download_tab, "Download")
|
||||||
self._setup_download_tab(download_tab)
|
self._setup_download_tab(download_tab)
|
||||||
|
self.download_tab_index = 0
|
||||||
|
|
||||||
library_tab = QWidget()
|
library_tab = QWidget()
|
||||||
tab_widget.addTab(library_tab, "Library")
|
self.tab_widget.addTab(library_tab, "Library")
|
||||||
self._setup_library_tab(library_tab)
|
self._setup_library_tab(library_tab)
|
||||||
|
|
||||||
ipod_tab = QWidget()
|
ipod_tab = QWidget()
|
||||||
tab_widget.addTab(ipod_tab, "iPod")
|
self.tab_widget.addTab(ipod_tab, "iPod")
|
||||||
self._setup_ipod_tab(ipod_tab)
|
self._setup_ipod_tab(ipod_tab)
|
||||||
|
|
||||||
settings_tab = QWidget()
|
settings_tab = QWidget()
|
||||||
tab_widget.addTab(settings_tab, "Settings")
|
self.tab_widget.addTab(settings_tab, "Settings")
|
||||||
self._setup_settings_tab(settings_tab)
|
self._setup_settings_tab(settings_tab)
|
||||||
|
|
||||||
|
self.tab_widget.setTabVisible(self.download_tab_index, self.show_download_tab)
|
||||||
|
|
||||||
self.statusBar().showMessage("Ready")
|
self.statusBar().showMessage("Ready")
|
||||||
self.setCentralWidget(main_widget)
|
self.setCentralWidget(main_widget)
|
||||||
|
|
||||||
@ -384,30 +389,23 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
layout.addWidget(playback_group)
|
layout.addWidget(playback_group)
|
||||||
|
|
||||||
# Refresh button
|
self._setup_library_toolbar(layout)
|
||||||
refresh_row = QHBoxLayout()
|
|
||||||
refresh_btn = QPushButton("\u21BB Refresh Library")
|
|
||||||
refresh_btn.clicked.connect(self._on_refresh_library_clicked)
|
|
||||||
refresh_row.addStretch()
|
|
||||||
refresh_row.addWidget(refresh_btn)
|
|
||||||
layout.addLayout(refresh_row)
|
|
||||||
|
|
||||||
self.ready_label = QLabel("Ready to Transfer:")
|
self.library_table = QTableWidget()
|
||||||
layout.addWidget(self.ready_label)
|
self.library_table.setColumnCount(7)
|
||||||
|
self.library_table.setHorizontalHeaderLabels(
|
||||||
self.library_ready_table = QTableWidget()
|
|
||||||
self.library_ready_table.setColumnCount(7)
|
|
||||||
self.library_ready_table.setHorizontalHeaderLabels(
|
|
||||||
["#", "Artist", "Title", "Album", "Duration", "Genre", "Size"]
|
["#", "Artist", "Title", "Album", "Duration", "Genre", "Size"]
|
||||||
)
|
)
|
||||||
self.library_ready_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
self.library_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||||
self.library_ready_table.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection)
|
self.library_table.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection)
|
||||||
self.library_ready_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
self.library_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||||
self.library_ready_table.setAlternatingRowColors(True)
|
self.library_table.setAlternatingRowColors(True)
|
||||||
self.library_ready_table.setSortingEnabled(True)
|
self.library_table.setSortingEnabled(True)
|
||||||
self.library_ready_table.cellDoubleClicked.connect(self._on_table_double_clicked)
|
self.library_table.cellDoubleClicked.connect(self._on_table_double_clicked)
|
||||||
|
self.library_table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||||
|
self.library_table.customContextMenuRequested.connect(self._on_library_table_context_menu)
|
||||||
|
|
||||||
header = self.library_ready_table.horizontalHeader()
|
header = self.library_table.horizontalHeader()
|
||||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||||
@ -416,32 +414,7 @@ class MainWindow(QMainWindow):
|
|||||||
header.setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
|
header.setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
header.setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents)
|
header.setSectionResizeMode(6, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
|
|
||||||
layout.addWidget(self.library_ready_table, stretch=2)
|
layout.addWidget(self.library_table, stretch=1)
|
||||||
|
|
||||||
self.source_label = QLabel("Source Files — Need Conversion:")
|
|
||||||
layout.addWidget(self.source_label)
|
|
||||||
|
|
||||||
self.library_source_list = QListWidget()
|
|
||||||
self.library_source_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
|
|
||||||
layout.addWidget(self.library_source_list, stretch=1)
|
|
||||||
|
|
||||||
btn_row = QHBoxLayout()
|
|
||||||
self.library_add_btn = QPushButton("Add Files...")
|
|
||||||
self.library_add_btn.clicked.connect(self._on_library_add_files)
|
|
||||||
self.library_remove_btn = QPushButton("Remove Selected")
|
|
||||||
self.library_remove_btn.clicked.connect(self._on_library_remove_selected)
|
|
||||||
btn_row.addWidget(self.library_add_btn)
|
|
||||||
btn_row.addWidget(self.library_remove_btn)
|
|
||||||
btn_row.addStretch()
|
|
||||||
layout.addLayout(btn_row)
|
|
||||||
|
|
||||||
self.library_convert_btn = QPushButton("Convert Selected")
|
|
||||||
self.library_convert_btn.clicked.connect(self._on_library_convert_clicked)
|
|
||||||
layout.addWidget(self.library_convert_btn)
|
|
||||||
|
|
||||||
self.library_transfer_btn = QPushButton("Transfer Selected to iPod \u25B8")
|
|
||||||
self.library_transfer_btn.clicked.connect(self._on_library_transfer_clicked)
|
|
||||||
layout.addWidget(self.library_transfer_btn)
|
|
||||||
|
|
||||||
self.library_progress = QProgressBar()
|
self.library_progress = QProgressBar()
|
||||||
self.library_progress.setRange(0, 100)
|
self.library_progress.setRange(0, 100)
|
||||||
@ -450,6 +423,44 @@ class MainWindow(QMainWindow):
|
|||||||
self.library_status = QLabel("Ready")
|
self.library_status = QLabel("Ready")
|
||||||
layout.addWidget(self.library_status)
|
layout.addWidget(self.library_status)
|
||||||
|
|
||||||
|
def _setup_library_toolbar(self, layout):
|
||||||
|
toolbar = QHBoxLayout()
|
||||||
|
toolbar.setSpacing(4)
|
||||||
|
|
||||||
|
self.library_add_btn = QPushButton("\u2795 Add Files...")
|
||||||
|
self.library_add_btn.clicked.connect(self._on_library_add_files)
|
||||||
|
toolbar.addWidget(self.library_add_btn)
|
||||||
|
|
||||||
|
self.library_convert_btn = QPushButton("\uD83D\uDD04 Convert")
|
||||||
|
self.library_convert_btn.setEnabled(False)
|
||||||
|
self.library_convert_btn.clicked.connect(self._on_library_convert_clicked)
|
||||||
|
toolbar.addWidget(self.library_convert_btn)
|
||||||
|
|
||||||
|
self.library_transfer_btn = QPushButton("\uD83D\uDCE4 Transfer")
|
||||||
|
self.library_transfer_btn.setEnabled(False)
|
||||||
|
self.library_transfer_btn.clicked.connect(self._on_library_transfer_clicked)
|
||||||
|
toolbar.addWidget(self.library_transfer_btn)
|
||||||
|
|
||||||
|
self.library_remove_btn = QPushButton("\uD83D\uDDD1\uFE0F Remove")
|
||||||
|
self.library_remove_btn.setEnabled(False)
|
||||||
|
self.library_remove_btn.clicked.connect(self._on_library_remove_selected)
|
||||||
|
toolbar.addWidget(self.library_remove_btn)
|
||||||
|
|
||||||
|
refresh_btn = QPushButton("\uD83D\uDD04")
|
||||||
|
refresh_btn.setToolTip("Refresh Library")
|
||||||
|
refresh_btn.clicked.connect(self._on_refresh_library_clicked)
|
||||||
|
toolbar.addWidget(refresh_btn)
|
||||||
|
|
||||||
|
toolbar.addStretch()
|
||||||
|
|
||||||
|
self.library_search_input = QLineEdit()
|
||||||
|
self.library_search_input.setPlaceholderText("\uD83D\uDD0D Search Library...")
|
||||||
|
self.library_search_input.setFixedWidth(220)
|
||||||
|
self.library_search_input.textChanged.connect(self._on_search_text_changed)
|
||||||
|
toolbar.addWidget(self.library_search_input)
|
||||||
|
|
||||||
|
layout.addLayout(toolbar)
|
||||||
|
|
||||||
def _setup_ipod_tab(self, tab):
|
def _setup_ipod_tab(self, tab):
|
||||||
layout = QVBoxLayout(tab)
|
layout = QVBoxLayout(tab)
|
||||||
|
|
||||||
@ -565,6 +576,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.auto_detect_check.setChecked(True)
|
self.auto_detect_check.setChecked(True)
|
||||||
advanced_layout.addWidget(self.auto_detect_check)
|
advanced_layout.addWidget(self.auto_detect_check)
|
||||||
|
|
||||||
|
self.show_download_check = QCheckBox("Show YouTube Download Tab")
|
||||||
|
self.show_download_check.setChecked(False)
|
||||||
|
self.show_download_check.toggled.connect(self._on_show_download_toggled)
|
||||||
|
advanced_layout.addWidget(self.show_download_check)
|
||||||
|
|
||||||
layout.addWidget(advanced_group)
|
layout.addWidget(advanced_group)
|
||||||
|
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
@ -577,6 +593,10 @@ class MainWindow(QMainWindow):
|
|||||||
about_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
about_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
layout.addWidget(about_label)
|
layout.addWidget(about_label)
|
||||||
|
|
||||||
|
def _on_show_download_toggled(self, checked: bool):
|
||||||
|
self.show_download_tab = checked
|
||||||
|
self.tab_widget.setTabVisible(self.download_tab_index, checked)
|
||||||
|
|
||||||
def _setup_player(self):
|
def _setup_player(self):
|
||||||
"""Initialize QMediaPlayer and QAudioOutput for playback."""
|
"""Initialize QMediaPlayer and QAudioOutput for playback."""
|
||||||
self.player = QMediaPlayer()
|
self.player = QMediaPlayer()
|
||||||
@ -590,12 +610,12 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _play_track_from_index(self, index: int):
|
def _play_track_from_index(self, index: int):
|
||||||
"""Start playing a track from the library table by row index."""
|
"""Start playing a track from the library table by row index."""
|
||||||
if index < 0 or index >= self.library_ready_table.rowCount():
|
if index < 0 or index >= self.library_table.rowCount():
|
||||||
return
|
return
|
||||||
data_item = self.library_ready_table.item(index, 0)
|
data_item = self.library_table.item(index, 0)
|
||||||
if data_item is None:
|
if data_item is None:
|
||||||
return
|
return
|
||||||
track_data = data_item.data(Qt.ItemDataRole.UserRole)
|
is_ready, track_data = data_item.data(Qt.ItemDataRole.UserRole)
|
||||||
if not track_data or not os.path.exists(track_data.get("path", "")):
|
if not track_data or not os.path.exists(track_data.get("path", "")):
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -611,8 +631,8 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Highlight the current row
|
# Highlight the current row
|
||||||
self.library_ready_table.clearSelection()
|
self.library_table.clearSelection()
|
||||||
self.library_ready_table.selectRow(index)
|
self.library_table.selectRow(index)
|
||||||
|
|
||||||
def _on_play_pause_clicked(self):
|
def _on_play_pause_clicked(self):
|
||||||
if self.player is None:
|
if self.player is None:
|
||||||
@ -624,7 +644,7 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
if self.player.playbackState() == QMediaPlayer.PlaybackState.StoppedState:
|
if self.player.playbackState() == QMediaPlayer.PlaybackState.StoppedState:
|
||||||
# Nothing playing yet — play first selected row or row 0
|
# Nothing playing yet — play first selected row or row 0
|
||||||
selected_rows = {item.row() for item in self.library_ready_table.selectedItems()}
|
selected_rows = {item.row() for item in self.library_table.selectedItems()}
|
||||||
start = min(selected_rows) if selected_rows else 0
|
start = min(selected_rows) if selected_rows else 0
|
||||||
self._play_track_from_index(start)
|
self._play_track_from_index(start)
|
||||||
else:
|
else:
|
||||||
@ -644,7 +664,7 @@ class MainWindow(QMainWindow):
|
|||||||
def _on_next_clicked(self):
|
def _on_next_clicked(self):
|
||||||
if self.player is None:
|
if self.player is None:
|
||||||
return
|
return
|
||||||
idx = min(self.library_ready_table.rowCount() - 1, self.current_playback_index + 1)
|
idx = min(self.library_table.rowCount() - 1, self.current_playback_index + 1)
|
||||||
self._play_track_from_index(idx)
|
self._play_track_from_index(idx)
|
||||||
|
|
||||||
def _on_slider_pressed(self):
|
def _on_slider_pressed(self):
|
||||||
@ -705,8 +725,8 @@ class MainWindow(QMainWindow):
|
|||||||
output_dir = self.output_dir_input.text().strip()
|
output_dir = self.output_dir_input.text().strip()
|
||||||
if not output_dir or not os.path.isdir(output_dir):
|
if not output_dir or not os.path.isdir(output_dir):
|
||||||
self.library_ready.clear()
|
self.library_ready.clear()
|
||||||
|
self.library_scanned_sources = []
|
||||||
self._refresh_library_ui()
|
self._refresh_library_ui()
|
||||||
self._refresh_source_ui([])
|
|
||||||
return
|
return
|
||||||
|
|
||||||
ready = []
|
ready = []
|
||||||
@ -778,68 +798,33 @@ class MainWindow(QMainWindow):
|
|||||||
ready = deduped_ready
|
ready = deduped_ready
|
||||||
|
|
||||||
self.library_ready = ready
|
self.library_ready = ready
|
||||||
|
self.library_scanned_sources = sources
|
||||||
self._refresh_library_ui()
|
self._refresh_library_ui()
|
||||||
self._refresh_source_ui(sources)
|
|
||||||
|
|
||||||
def _refresh_library_ui(self):
|
def _refresh_library_ui(self):
|
||||||
self.library_ready_table.setRowCount(0)
|
self.library_table.setSortingEnabled(False)
|
||||||
self.library_ready_table.setSortingEnabled(False)
|
self.library_table.setRowCount(0)
|
||||||
|
|
||||||
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(track.get("album", "") or ""),
|
|
||||||
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_table.setVisible(count > 0)
|
|
||||||
if count:
|
|
||||||
self.library_status.setText(f"{count} track(s) ready to transfer")
|
|
||||||
self.library_transfer_btn.setEnabled(True)
|
|
||||||
else:
|
|
||||||
self.library_status.setText("No tracks ready — convert source files or download")
|
|
||||||
self.library_transfer_btn.setEnabled(False)
|
|
||||||
|
|
||||||
def _refresh_source_ui(self, scanned_sources):
|
|
||||||
"""Build the Source Files list by combining scanned files + manual additions.
|
|
||||||
|
|
||||||
Deduplicates by path. All items get UserRole data for _get_selected_source_files().
|
|
||||||
"""
|
|
||||||
self.library_source_list.clear()
|
|
||||||
|
|
||||||
seen_paths = set()
|
seen_paths = set()
|
||||||
all_sources = []
|
all_tracks = []
|
||||||
|
|
||||||
for src in scanned_sources:
|
# Add ready tracks
|
||||||
path = src.get("path", "")
|
for t in self.library_ready:
|
||||||
|
path = t.get("path", "")
|
||||||
if path and path not in seen_paths and os.path.exists(path):
|
if path and path not in seen_paths and os.path.exists(path):
|
||||||
seen_paths.add(path)
|
seen_paths.add(path)
|
||||||
all_sources.append(src)
|
all_tracks.append({**t, "_status": "ready"})
|
||||||
|
|
||||||
|
# Add scanned source files
|
||||||
|
for s in self.library_scanned_sources:
|
||||||
|
path = s.get("path", "")
|
||||||
|
if path and path not in seen_paths and os.path.exists(path):
|
||||||
|
seen_paths.add(path)
|
||||||
|
all_tracks.append({**s, "duration_s": 0, "genre": "", "_status": "source"})
|
||||||
|
|
||||||
|
# Add manually added source files
|
||||||
for fpath in self.library_source_files:
|
for fpath in self.library_source_files:
|
||||||
if fpath in seen_paths:
|
if fpath in seen_paths or not os.path.exists(fpath):
|
||||||
continue
|
|
||||||
if not os.path.exists(fpath):
|
|
||||||
continue
|
continue
|
||||||
fname = os.path.basename(fpath)
|
fname = os.path.basename(fpath)
|
||||||
size = os.path.getsize(fpath)
|
size = os.path.getsize(fpath)
|
||||||
@ -851,42 +836,84 @@ class MainWindow(QMainWindow):
|
|||||||
except Exception:
|
except Exception:
|
||||||
title = fname
|
title = fname
|
||||||
artist = "Unknown"
|
artist = "Unknown"
|
||||||
|
|
||||||
src = {
|
|
||||||
"path": fpath,
|
|
||||||
"title": title,
|
|
||||||
"artist": artist,
|
|
||||||
"size": size,
|
|
||||||
}
|
|
||||||
all_sources.append(src)
|
|
||||||
seen_paths.add(fpath)
|
seen_paths.add(fpath)
|
||||||
|
all_tracks.append({
|
||||||
|
"path": fpath, "title": title, "artist": artist,
|
||||||
|
"size": size, "duration_s": 0, "genre": "", "album": "",
|
||||||
|
"_status": "source",
|
||||||
|
})
|
||||||
|
|
||||||
for src in sorted(all_sources, key=lambda s: (s["artist"], s["title"])):
|
all_tracks.sort(key=lambda t: (t["artist"].lower(), t["title"].lower()))
|
||||||
display = f"{src['artist']} — {src['title']} {self._format_size(src['size'])}"
|
|
||||||
item = QListWidgetItem(display)
|
|
||||||
item.setData(Qt.ItemDataRole.UserRole, src)
|
|
||||||
self.library_source_list.addItem(item)
|
|
||||||
|
|
||||||
count = len(all_sources)
|
for idx, track in enumerate(all_tracks):
|
||||||
self.source_label.setVisible(count > 0)
|
row = self.library_table.rowCount()
|
||||||
self.library_source_list.setVisible(count > 0)
|
self.library_table.insertRow(row)
|
||||||
self.library_convert_btn.setEnabled(count > 0)
|
|
||||||
|
is_ready = track["_status"] == "ready"
|
||||||
|
|
||||||
|
track_num_str = str(track.get("track_num") or "") if is_ready else ""
|
||||||
|
duration_str = self._format_duration(track.get("duration_s", 0)) if is_ready else "\u2014"
|
||||||
|
|
||||||
|
cells = [
|
||||||
|
QTableWidgetItem(track_num_str),
|
||||||
|
QTableWidgetItem(track["artist"]),
|
||||||
|
QTableWidgetItem(track["title"]),
|
||||||
|
QTableWidgetItem(track.get("album", "") or ""),
|
||||||
|
QTableWidgetItem(duration_str),
|
||||||
|
QTableWidgetItem(track.get("genre", "") or ""),
|
||||||
|
QTableWidgetItem(self._format_size(track["size"])),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Dim source tracks (gray text) like iTunes
|
||||||
|
if not is_ready:
|
||||||
|
gray = self.palette().color(self.palette().ColorRole.PlaceholderText)
|
||||||
|
for item in cells:
|
||||||
|
item.setForeground(gray)
|
||||||
|
|
||||||
|
# Store full track data in column 0
|
||||||
|
data_key = track.copy()
|
||||||
|
data_key.pop("_status", None)
|
||||||
|
cells[0].setData(Qt.ItemDataRole.UserRole, (is_ready, data_key))
|
||||||
|
|
||||||
|
for col, item in enumerate(cells):
|
||||||
|
self.library_table.setItem(row, col, item)
|
||||||
|
|
||||||
|
self.library_table.setSortingEnabled(True)
|
||||||
|
|
||||||
|
ready_count = sum(1 for t in all_tracks if t["_status"] == "ready")
|
||||||
|
source_count = len(all_tracks) - ready_count
|
||||||
|
|
||||||
|
has_ready = ready_count > 0
|
||||||
|
has_source = source_count > 0
|
||||||
|
|
||||||
|
self.library_transfer_btn.setEnabled(has_ready)
|
||||||
|
self.library_convert_btn.setEnabled(has_source)
|
||||||
|
self.library_remove_btn.setEnabled(len(all_tracks) > 0)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if ready_count:
|
||||||
|
parts.append(f"{ready_count} ready")
|
||||||
|
if source_count:
|
||||||
|
parts.append(f"{source_count} need conversion")
|
||||||
|
status = f"{len(all_tracks)} track(s) \u2014 {', '.join(parts)}" if parts else "No tracks in library"
|
||||||
|
self.library_status.setText(status)
|
||||||
|
|
||||||
def _get_selected_ready_tracks(self) -> List[Tuple[TrackInfo, str]]:
|
def _get_selected_ready_tracks(self) -> List[Tuple[TrackInfo, str]]:
|
||||||
selected_rows = set()
|
selected_rows = set()
|
||||||
for item in self.library_ready_table.selectedItems():
|
for item in self.library_table.selectedItems():
|
||||||
selected_rows.add(item.row())
|
selected_rows.add(item.row())
|
||||||
|
|
||||||
if not selected_rows:
|
if not selected_rows:
|
||||||
selected_rows = set(range(self.library_ready_table.rowCount()))
|
selected_rows = set(range(self.library_table.rowCount()))
|
||||||
|
|
||||||
tracks = []
|
tracks = []
|
||||||
for row in sorted(selected_rows):
|
for row in sorted(selected_rows):
|
||||||
data_item = self.library_ready_table.item(row, 0)
|
data_item = self.library_table.item(row, 0)
|
||||||
if data_item is None:
|
if data_item is None:
|
||||||
continue
|
continue
|
||||||
data = data_item.data(Qt.ItemDataRole.UserRole)
|
is_ready, data = data_item.data(Qt.ItemDataRole.UserRole)
|
||||||
if data:
|
if not is_ready:
|
||||||
|
continue
|
||||||
track_info = TrackInfo(
|
track_info = TrackInfo(
|
||||||
video_id=f"local_{hash(data['path'])}",
|
video_id=f"local_{hash(data['path'])}",
|
||||||
title=data['title'],
|
title=data['title'],
|
||||||
@ -902,14 +929,22 @@ class MainWindow(QMainWindow):
|
|||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
def _get_selected_source_files(self) -> List[str]:
|
def _get_selected_source_files(self) -> List[str]:
|
||||||
selected = self.library_source_list.selectedItems()
|
selected_rows = set()
|
||||||
if not selected:
|
for item in self.library_table.selectedItems():
|
||||||
selected = [self.library_source_list.item(i) for i in range(self.library_source_list.count())]
|
selected_rows.add(item.row())
|
||||||
|
|
||||||
|
if not selected_rows:
|
||||||
|
selected_rows = set(range(self.library_table.rowCount()))
|
||||||
|
|
||||||
paths = []
|
paths = []
|
||||||
for item in selected:
|
for row in sorted(selected_rows):
|
||||||
data = item.data(Qt.ItemDataRole.UserRole)
|
data_item = self.library_table.item(row, 0)
|
||||||
if data and os.path.exists(data.get("path", "")):
|
if data_item is None:
|
||||||
|
continue
|
||||||
|
is_ready, data = data_item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
if is_ready:
|
||||||
|
continue
|
||||||
|
if os.path.exists(data.get("path", "")):
|
||||||
paths.append(data["path"])
|
paths.append(data["path"])
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
@ -938,26 +973,21 @@ class MainWindow(QMainWindow):
|
|||||||
new_files.append(f)
|
new_files.append(f)
|
||||||
|
|
||||||
if new_files:
|
if new_files:
|
||||||
self._refresh_source_ui([])
|
self._scan_library()
|
||||||
|
|
||||||
def _on_library_remove_selected(self):
|
def _on_library_remove_selected(self):
|
||||||
"""Delete selected files from disk and clean up empty directories."""
|
"""Delete selected files from disk and clean up empty directories."""
|
||||||
files_to_delete = []
|
files_to_delete = []
|
||||||
|
|
||||||
selected_rows = set()
|
selected_rows = set()
|
||||||
for item in self.library_ready_table.selectedItems():
|
for item in self.library_table.selectedItems():
|
||||||
selected_rows.add(item.row())
|
selected_rows.add(item.row())
|
||||||
|
|
||||||
for row in selected_rows:
|
for row in selected_rows:
|
||||||
data_item = self.library_ready_table.item(row, 0)
|
data_item = self.library_table.item(row, 0)
|
||||||
if data_item is None:
|
if data_item is None:
|
||||||
continue
|
continue
|
||||||
data = data_item.data(Qt.ItemDataRole.UserRole)
|
is_ready, 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", "")):
|
if data and os.path.exists(data.get("path", "")):
|
||||||
files_to_delete.append(data)
|
files_to_delete.append(data)
|
||||||
|
|
||||||
@ -1002,6 +1032,57 @@ class MainWindow(QMainWindow):
|
|||||||
self._scan_library()
|
self._scan_library()
|
||||||
QMessageBox.information(self, "Done", f"Deleted {deleted} file(s)")
|
QMessageBox.information(self, "Done", f"Deleted {deleted} file(s)")
|
||||||
|
|
||||||
|
def _on_search_text_changed(self, text: str):
|
||||||
|
for row in range(self.library_table.rowCount()):
|
||||||
|
item = self.library_table.item(row, 3)
|
||||||
|
match = not text or text.lower() in item.text().lower() if item else True
|
||||||
|
self.library_table.setRowHidden(row, not match)
|
||||||
|
|
||||||
|
def _on_library_table_context_menu(self, pos):
|
||||||
|
item = self.library_table.itemAt(pos)
|
||||||
|
if item is None:
|
||||||
|
return
|
||||||
|
row = item.row()
|
||||||
|
data_item = self.library_table.item(row, 0)
|
||||||
|
if data_item is None:
|
||||||
|
return
|
||||||
|
is_ready, track_data = data_item.data(Qt.ItemDataRole.UserRole)
|
||||||
|
|
||||||
|
menu = QMenu(self)
|
||||||
|
|
||||||
|
play_action = menu.addAction("\u25B6 Play")
|
||||||
|
play_action.triggered.connect(lambda: self._play_track_from_index(row))
|
||||||
|
|
||||||
|
if is_ready:
|
||||||
|
transfer_action = menu.addAction("\uD83D\uDCE4 Transfer to iPod")
|
||||||
|
transfer_action.triggered.connect(self._on_library_transfer_clicked)
|
||||||
|
else:
|
||||||
|
convert_action = menu.addAction("\uD83D\uDD04 Convert to iPod Format")
|
||||||
|
convert_action.triggered.connect(self._on_library_convert_clicked)
|
||||||
|
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
|
show_action = menu.addAction("\uD83D\uDCC2 Show in File Manager")
|
||||||
|
show_action.triggered.connect(lambda: self._on_show_in_file_manager(track_data))
|
||||||
|
|
||||||
|
remove_action = menu.addAction("\uD83D\uDDD1\uFE0F Remove from Library")
|
||||||
|
remove_action.triggered.connect(self._on_library_remove_selected)
|
||||||
|
|
||||||
|
menu.exec(self.library_table.viewport().mapToGlobal(pos))
|
||||||
|
|
||||||
|
def _on_show_in_file_manager(self, track_data):
|
||||||
|
path = track_data.get("path", "")
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
import subprocess
|
||||||
|
platform = sys.platform
|
||||||
|
if platform == "win32":
|
||||||
|
subprocess.Popen(["explorer", "/select,", os.path.normpath(path)])
|
||||||
|
elif platform == "darwin":
|
||||||
|
subprocess.Popen(["open", "-R", path])
|
||||||
|
else:
|
||||||
|
subprocess.Popen(["xdg-open", os.path.dirname(path)])
|
||||||
|
|
||||||
def _on_download_clicked(self):
|
def _on_download_clicked(self):
|
||||||
url = self.url_input.text().strip()
|
url = self.url_input.text().strip()
|
||||||
if not url:
|
if not url:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user