fix: correct playlist creation on iPod (real db_track_id, metadata fallback, Settings hover fix)
This commit is contained in:
parent
8861e55f32
commit
b7367e63c4
53
src/app.py
53
src/app.py
@ -48,6 +48,8 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.playlist_manager = PlaylistManager()
|
self.playlist_manager = PlaylistManager()
|
||||||
self._sidebar_visible = True
|
self._sidebar_visible = True
|
||||||
|
self._pending_playlist_id: str = ""
|
||||||
|
self._pending_playlist_tracks: list = []
|
||||||
|
|
||||||
self._saved_volume: int = 80
|
self._saved_volume: int = 80
|
||||||
self._saved_track_path: str = ""
|
self._saved_track_path: str = ""
|
||||||
@ -141,7 +143,9 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self.sidebar.tracks_dropped_on_playlist.connect(self._on_tracks_dropped_on_playlist)
|
self.sidebar.tracks_dropped_on_playlist.connect(self._on_tracks_dropped_on_playlist)
|
||||||
self.sidebar.tracks_dropped_for_nav_transfer.connect(self._on_tracks_dropped_for_nav_transfer)
|
self.sidebar.tracks_dropped_for_nav_transfer.connect(self._on_tracks_dropped_for_nav_transfer)
|
||||||
|
self.sidebar.playlist_dropped_for_nav_transfer.connect(self._on_playlist_dropped_for_nav_transfer)
|
||||||
self.ipod_tab.tracks_dropped_for_transfer.connect(self._on_tracks_dropped_for_transfer)
|
self.ipod_tab.tracks_dropped_for_transfer.connect(self._on_tracks_dropped_for_transfer)
|
||||||
|
self.library_tab.transfer_finished.connect(self._on_transfer_finished_for_playlist)
|
||||||
|
|
||||||
def _on_sidebar_item_selected(self, kind: str, value: str):
|
def _on_sidebar_item_selected(self, kind: str, value: str):
|
||||||
if kind == "tab":
|
if kind == "tab":
|
||||||
@ -197,6 +201,55 @@ class MainWindow(QMainWindow):
|
|||||||
self.stack.setCurrentIndex(0)
|
self.stack.setCurrentIndex(0)
|
||||||
self.library_tab.transfer_tracks_by_paths(paths)
|
self.library_tab.transfer_tracks_by_paths(paths)
|
||||||
|
|
||||||
|
def _on_playlist_dropped_for_nav_transfer(self, playlist_id: str):
|
||||||
|
playlist = self.playlist_manager.get_by_id(playlist_id)
|
||||||
|
if not playlist or not playlist.track_paths:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stack.setCurrentIndex(0)
|
||||||
|
self._pending_playlist_id = playlist_id
|
||||||
|
self._pending_playlist_tracks = list(playlist.track_paths)
|
||||||
|
self.library_tab.transfer_tracks_by_paths(playlist.track_paths)
|
||||||
|
|
||||||
|
def _on_transfer_finished_for_playlist(self):
|
||||||
|
playlist_id = self._pending_playlist_id
|
||||||
|
track_paths = self._pending_playlist_tracks
|
||||||
|
self._pending_playlist_id = ""
|
||||||
|
self._pending_playlist_tracks = []
|
||||||
|
|
||||||
|
if not playlist_id or not track_paths:
|
||||||
|
return
|
||||||
|
|
||||||
|
mount_point = getattr(self.ipod_tab, "current_mount_point", None)
|
||||||
|
if not mount_point:
|
||||||
|
self.statusBar().showMessage("Cannot create playlist: iPod not mounted", 5000)
|
||||||
|
return
|
||||||
|
|
||||||
|
playlist = self.playlist_manager.get_by_id(playlist_id)
|
||||||
|
if not playlist:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.statusBar().showMessage(f"Creating playlist '{playlist.name}' on iPod...")
|
||||||
|
|
||||||
|
from worker import WorkerThread
|
||||||
|
self._pl_worker = WorkerThread(
|
||||||
|
task_type="create_playlist",
|
||||||
|
mount_point=mount_point,
|
||||||
|
playlist_name=playlist.name,
|
||||||
|
track_paths=track_paths,
|
||||||
|
)
|
||||||
|
self._pl_worker.finished_signal.connect(self._on_playlist_creation_finished)
|
||||||
|
self._pl_worker.start()
|
||||||
|
|
||||||
|
def _on_playlist_creation_finished(self, success, message, result):
|
||||||
|
self.statusBar().showMessage(message, 8000)
|
||||||
|
try:
|
||||||
|
self._pl_worker.quit()
|
||||||
|
self._pl_worker.wait()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._pl_worker = None
|
||||||
|
|
||||||
def _load_playlists_into_sidebar(self):
|
def _load_playlists_into_sidebar(self):
|
||||||
for pl in self.playlist_manager.get_all():
|
for pl in self.playlist_manager.get_all():
|
||||||
self.sidebar.add_playlist_item(pl.id, pl.name)
|
self.sidebar.add_playlist_item(pl.id, pl.name)
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import sqlite3
|
|||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from typing import Any, Optional
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
from mutagen import File as MutagenFile
|
from mutagen import File as MutagenFile
|
||||||
from mutagen.easyid3 import EasyID3
|
from mutagen.easyid3 import EasyID3
|
||||||
@ -627,6 +627,114 @@ class Nano7Database:
|
|||||||
deleted += 1
|
deleted += 1
|
||||||
return deleted
|
return deleted
|
||||||
|
|
||||||
|
# ── playlist creation ────────────────────────────────────────────
|
||||||
|
|
||||||
|
def create_playlist(self, name: str, track_paths: list[str]) -> bool:
|
||||||
|
"""Create a user playlist on the iPod.
|
||||||
|
|
||||||
|
Two-pass matching: content hash first, then title+artist+album
|
||||||
|
fallback. Only tracks already present on the iPod will be
|
||||||
|
included in the playlist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Playlist name.
|
||||||
|
track_paths: Local file paths of the tracks to include.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the playlist was created, False otherwise.
|
||||||
|
"""
|
||||||
|
scanned = self._scan_ipod_files()
|
||||||
|
if not scanned:
|
||||||
|
logger.warning("No tracks on iPod — cannot create playlist")
|
||||||
|
return False
|
||||||
|
|
||||||
|
playlist_created = False
|
||||||
|
self._merge_play_stats(scanned)
|
||||||
|
|
||||||
|
def _build_pl_cb(scanned, tracks):
|
||||||
|
"""Build playlist using real db_track_id from TrackInfo."""
|
||||||
|
nonlocal playlist_created
|
||||||
|
|
||||||
|
try:
|
||||||
|
IopPlaylistInfo = _import_iop(
|
||||||
|
"iTunesDB_Writer.mhyp_writer"
|
||||||
|
).PlaylistInfo
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Failed to import PlaylistInfo: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Pass 1: content_hash → db_track_id
|
||||||
|
hash_to_db_id: dict[str, int] = {}
|
||||||
|
for i, s in enumerate(scanned):
|
||||||
|
ch = s.get("content_hash", "")
|
||||||
|
if ch:
|
||||||
|
hash_to_db_id[ch] = tracks[i].db_track_id
|
||||||
|
|
||||||
|
found_ids: list[int] = []
|
||||||
|
unmatched: list[str] = []
|
||||||
|
|
||||||
|
for path in track_paths:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
|
ch = content_hash(path)
|
||||||
|
db_id = hash_to_db_id.get(ch)
|
||||||
|
if db_id is not None:
|
||||||
|
found_ids.append(db_id)
|
||||||
|
else:
|
||||||
|
unmatched.append(path)
|
||||||
|
|
||||||
|
hash_matched_count = len(found_ids)
|
||||||
|
|
||||||
|
# Pass 2: metadata fallback
|
||||||
|
if unmatched:
|
||||||
|
scanned_meta = []
|
||||||
|
for i, s in enumerate(scanned):
|
||||||
|
scanned_meta.append((
|
||||||
|
s.get("title", "").lower(),
|
||||||
|
s.get("artist", "").lower(),
|
||||||
|
(s.get("album") or "").lower(),
|
||||||
|
tracks[i].db_track_id,
|
||||||
|
))
|
||||||
|
|
||||||
|
found_set = set(found_ids)
|
||||||
|
for path in unmatched:
|
||||||
|
tags = _read_audio_tags(path)
|
||||||
|
title = (tags.get("title") or "").lower()
|
||||||
|
artist = (tags.get("artist") or "").lower()
|
||||||
|
album = (tags.get("album") or "").lower()
|
||||||
|
if not title or not artist:
|
||||||
|
continue
|
||||||
|
for s_title, s_artist, s_album, db_id in scanned_meta:
|
||||||
|
if (db_id not in found_set
|
||||||
|
and s_title == title
|
||||||
|
and s_artist == artist
|
||||||
|
and s_album == album):
|
||||||
|
found_ids.append(db_id)
|
||||||
|
found_set.add(db_id)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found_ids:
|
||||||
|
logger.warning(
|
||||||
|
"No tracks from playlist '%s' found on iPod",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
matched_by_hash = hash_matched_count
|
||||||
|
matched_by_meta = len(found_ids) - hash_matched_count
|
||||||
|
logger.info(
|
||||||
|
"Playlist '%s': %d track(s) matched (%d by hash, %d by metadata)",
|
||||||
|
name, len(found_ids), matched_by_hash, matched_by_meta,
|
||||||
|
)
|
||||||
|
playlist_created = True
|
||||||
|
return [IopPlaylistInfo(name=name, track_ids=found_ids)]
|
||||||
|
|
||||||
|
db_ok = self._write_databases_from_tracks(
|
||||||
|
scanned, build_extra_playlists=_build_pl_cb,
|
||||||
|
)
|
||||||
|
self._cleanup_play_counts_file()
|
||||||
|
return db_ok and playlist_created
|
||||||
|
|
||||||
# ── sync: regenerate everything via iOpenPod ──────────────────────
|
# ── sync: regenerate everything via iOpenPod ──────────────────────
|
||||||
|
|
||||||
def sync_itunescdb(self) -> None:
|
def sync_itunescdb(self) -> None:
|
||||||
@ -694,7 +802,9 @@ class Nano7Database:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._merge_play_stats(scanned)
|
self._merge_play_stats(scanned)
|
||||||
self._write_databases_from_tracks(scanned)
|
if not self._write_databases_from_tracks(scanned):
|
||||||
|
logger.error("iTunesDB sync failed — play counts not cleaned up")
|
||||||
|
return
|
||||||
self._cleanup_play_counts_file()
|
self._cleanup_play_counts_file()
|
||||||
|
|
||||||
def find_track_by_content_hash(self, content_hash: str) -> dict | None:
|
def find_track_by_content_hash(self, content_hash: str) -> dict | None:
|
||||||
@ -901,7 +1011,9 @@ class Nano7Database:
|
|||||||
self,
|
self,
|
||||||
scanned: list[dict],
|
scanned: list[dict],
|
||||||
artwork_overrides: dict[str, object] | None = None,
|
artwork_overrides: dict[str, object] | None = None,
|
||||||
) -> None:
|
extra_playlists: list | None = None,
|
||||||
|
build_extra_playlists: Callable | None = None,
|
||||||
|
) -> bool:
|
||||||
"""Build artwork, convert to TrackInfo, write iTunesDB + SQLite.
|
"""Build artwork, convert to TrackInfo, write iTunesDB + SQLite.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -911,6 +1023,15 @@ class Nano7Database:
|
|||||||
artwork should come from a local file instead of the iPod file.
|
artwork should come from a local file instead of the iPod file.
|
||||||
Use the ``_REMOVE_ARTWORK`` sentinel to explicitly remove
|
Use the ``_REMOVE_ARTWORK`` sentinel to explicitly remove
|
||||||
artwork for a track. Omit a key to keep existing iPod artwork.
|
artwork for a track. Omit a key to keep existing iPod artwork.
|
||||||
|
extra_playlists: Pre-built list of PlaylistInfo (ignored when
|
||||||
|
build_extra_playlists is provided).
|
||||||
|
build_extra_playlists: Optional callable(scanned, tracks) → list
|
||||||
|
of PlaylistInfo. Called after TrackInfo conversion and
|
||||||
|
db_track_id generation, so the callback can use real
|
||||||
|
db_track_id values from TrackInfo objects.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if databases were written successfully.
|
||||||
"""
|
"""
|
||||||
artwork_overrides = artwork_overrides or {}
|
artwork_overrides = artwork_overrides or {}
|
||||||
# 1. Prepare artwork for all tracks
|
# 1. Prepare artwork for all tracks
|
||||||
@ -970,6 +1091,20 @@ class Nano7Database:
|
|||||||
tracks = _scanned_tracks_to_iop(self.mountpoint, scanned, artwork_map)
|
tracks = _scanned_tracks_to_iop(self.mountpoint, scanned, artwork_map)
|
||||||
logger.info("Built %d iOpenPod TrackInfo objects", len(tracks))
|
logger.info("Built %d iOpenPod TrackInfo objects", len(tracks))
|
||||||
|
|
||||||
|
# 2b. Pre-generate db_track_id for all tracks so callbacks can use them
|
||||||
|
generate_db_track_id = _import_iop("iTunesDB_Writer.mhit_writer").generate_db_track_id
|
||||||
|
for track in tracks:
|
||||||
|
if track.db_track_id == 0:
|
||||||
|
track.db_track_id = generate_db_track_id()
|
||||||
|
|
||||||
|
# 2c. Build extra playlists via callback if provided
|
||||||
|
if build_extra_playlists is not None:
|
||||||
|
result = build_extra_playlists(scanned, tracks)
|
||||||
|
if result is not None:
|
||||||
|
extra_playlists = result
|
||||||
|
else:
|
||||||
|
extra_playlists = None
|
||||||
|
|
||||||
# 3. Determine capabilities for this device
|
# 3. Determine capabilities for this device
|
||||||
ipod_device = _import_iop("ipod_device")
|
ipod_device = _import_iop("ipod_device")
|
||||||
capabilities = None
|
capabilities = None
|
||||||
@ -998,10 +1133,11 @@ class Nano7Database:
|
|||||||
backup=True,
|
backup=True,
|
||||||
capabilities=capabilities,
|
capabilities=capabilities,
|
||||||
master_playlist_name="iPod",
|
master_playlist_name="iPod",
|
||||||
|
playlists=extra_playlists or None,
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
logger.error("iTunesDB write failed")
|
logger.error("iTunesDB write failed")
|
||||||
return
|
return False
|
||||||
logger.info("iTunesDB written (%d tracks)", len(tracks))
|
logger.info("iTunesDB written (%d tracks)", len(tracks))
|
||||||
|
|
||||||
# 5. Extract db_pid from the binary DB for SQLite cross-reference
|
# 5. Extract db_pid from the binary DB for SQLite cross-reference
|
||||||
@ -1029,13 +1165,18 @@ class Nano7Database:
|
|||||||
capabilities=capabilities,
|
capabilities=capabilities,
|
||||||
firewire_id=firewire_id,
|
firewire_id=firewire_id,
|
||||||
backup=True,
|
backup=True,
|
||||||
|
playlists=extra_playlists or None,
|
||||||
)
|
)
|
||||||
if sqlite_ok:
|
if sqlite_ok:
|
||||||
logger.info("SQLite databases written")
|
logger.info("SQLite databases written")
|
||||||
else:
|
else:
|
||||||
logger.error("SQLite database write failed")
|
logger.error("SQLite database write failed")
|
||||||
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("SQLite write error: %s", e)
|
logger.exception("SQLite write error: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _write_empty_databases(self) -> None:
|
def _write_empty_databases(self) -> None:
|
||||||
"""Write minimal empty databases when no tracks exist."""
|
"""Write minimal empty databases when no tracks exist."""
|
||||||
|
|||||||
@ -27,6 +27,14 @@ class PlaylistDropListWidget(QListWidget):
|
|||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
self._hovered_item = None
|
self._hovered_item = None
|
||||||
|
|
||||||
|
def mimeData(self, items):
|
||||||
|
mime = super().mimeData(items)
|
||||||
|
if items:
|
||||||
|
playlist_id = items[0].data(Qt.ItemDataRole.UserRole)
|
||||||
|
if playlist_id:
|
||||||
|
mime.setData("application/x-neopod-playlist", playlist_id.encode())
|
||||||
|
return mime
|
||||||
|
|
||||||
def dragEnterEvent(self, event: QDragEnterEvent):
|
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||||
if event.source() is not self and event.mimeData().hasUrls():
|
if event.source() is not self and event.mimeData().hasUrls():
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
@ -83,6 +91,7 @@ class PlaylistDropListWidget(QListWidget):
|
|||||||
class NavDropListWidget(QListWidget):
|
class NavDropListWidget(QListWidget):
|
||||||
"""QListWidget that accepts external track drops on the iPod nav item."""
|
"""QListWidget that accepts external track drops on the iPod nav item."""
|
||||||
tracks_dropped_for_transfer = pyqtSignal(list)
|
tracks_dropped_for_transfer = pyqtSignal(list)
|
||||||
|
playlist_dropped_for_transfer = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -95,7 +104,19 @@ class NavDropListWidget(QListWidget):
|
|||||||
return item
|
return item
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _repolish(self):
|
||||||
|
self.style().unpolish(self)
|
||||||
|
self.style().polish(self)
|
||||||
|
|
||||||
def dragEnterEvent(self, event: QDragEnterEvent):
|
def dragEnterEvent(self, event: QDragEnterEvent):
|
||||||
|
if event.mimeData().hasFormat("application/x-neopod-playlist"):
|
||||||
|
self.setProperty("dndActive", True)
|
||||||
|
self._repolish()
|
||||||
|
pos = event.position().toPoint()
|
||||||
|
if self._target_item_at(pos):
|
||||||
|
event.acceptProposedAction()
|
||||||
|
return
|
||||||
|
# Let default handler accept so dragMoveEvent still fires
|
||||||
if event.source() is not self and event.mimeData().hasUrls():
|
if event.source() is not self and event.mimeData().hasUrls():
|
||||||
pos = event.position().toPoint()
|
pos = event.position().toPoint()
|
||||||
if self._target_item_at(pos):
|
if self._target_item_at(pos):
|
||||||
@ -104,6 +125,7 @@ class NavDropListWidget(QListWidget):
|
|||||||
super().dragEnterEvent(event)
|
super().dragEnterEvent(event)
|
||||||
|
|
||||||
def dragMoveEvent(self, event: QDragMoveEvent):
|
def dragMoveEvent(self, event: QDragMoveEvent):
|
||||||
|
accept = False
|
||||||
if event.source() is not self and event.mimeData().hasUrls():
|
if event.source() is not self and event.mimeData().hasUrls():
|
||||||
pos = event.position().toPoint()
|
pos = event.position().toPoint()
|
||||||
item = self._target_item_at(pos)
|
item = self._target_item_at(pos)
|
||||||
@ -115,15 +137,34 @@ class NavDropListWidget(QListWidget):
|
|||||||
item.setBackground(bg)
|
item.setBackground(bg)
|
||||||
if item:
|
if item:
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
return
|
accept = True
|
||||||
super().dragMoveEvent(event)
|
if not accept and event.mimeData().hasFormat("application/x-neopod-playlist"):
|
||||||
|
pos = event.position().toPoint()
|
||||||
|
item = self._target_item_at(pos)
|
||||||
|
if item != self._hovered_item:
|
||||||
|
self._clear_hover()
|
||||||
|
if item:
|
||||||
|
self._hovered_item = item
|
||||||
|
bg = self.palette().color(self.palette().ColorRole.Highlight)
|
||||||
|
item.setBackground(bg)
|
||||||
|
if item:
|
||||||
|
event.acceptProposedAction()
|
||||||
|
accept = True
|
||||||
|
if not accept:
|
||||||
|
event.ignore()
|
||||||
|
|
||||||
def dragLeaveEvent(self, event: QDragLeaveEvent):
|
def dragLeaveEvent(self, event: QDragLeaveEvent):
|
||||||
self._clear_hover()
|
self._clear_hover()
|
||||||
|
if self.property("dndActive"):
|
||||||
|
self.setProperty("dndActive", False)
|
||||||
|
self._repolish()
|
||||||
super().dragLeaveEvent(event)
|
super().dragLeaveEvent(event)
|
||||||
|
|
||||||
def dropEvent(self, event: QDropEvent):
|
def dropEvent(self, event: QDropEvent):
|
||||||
self._clear_hover()
|
self._clear_hover()
|
||||||
|
if self.property("dndActive"):
|
||||||
|
self.setProperty("dndActive", False)
|
||||||
|
self._repolish()
|
||||||
if event.source() is not self and event.mimeData().hasUrls():
|
if event.source() is not self and event.mimeData().hasUrls():
|
||||||
pos = event.position().toPoint()
|
pos = event.position().toPoint()
|
||||||
if self._target_item_at(pos):
|
if self._target_item_at(pos):
|
||||||
@ -133,6 +174,14 @@ class NavDropListWidget(QListWidget):
|
|||||||
self.tracks_dropped_for_transfer.emit(paths)
|
self.tracks_dropped_for_transfer.emit(paths)
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
return
|
return
|
||||||
|
if event.mimeData().hasFormat("application/x-neopod-playlist"):
|
||||||
|
pos = event.position().toPoint()
|
||||||
|
if self._target_item_at(pos):
|
||||||
|
playlist_id = bytes(event.mimeData().data("application/x-neopod-playlist")).decode()
|
||||||
|
if playlist_id:
|
||||||
|
self.playlist_dropped_for_transfer.emit(playlist_id)
|
||||||
|
event.acceptProposedAction()
|
||||||
|
return
|
||||||
super().dropEvent(event)
|
super().dropEvent(event)
|
||||||
|
|
||||||
def _clear_hover(self):
|
def _clear_hover(self):
|
||||||
@ -146,6 +195,7 @@ class SidebarWidget(QWidget):
|
|||||||
playlist_selected = pyqtSignal(str)
|
playlist_selected = pyqtSignal(str)
|
||||||
tracks_dropped_on_playlist = pyqtSignal(str, list)
|
tracks_dropped_on_playlist = pyqtSignal(str, list)
|
||||||
tracks_dropped_for_nav_transfer = pyqtSignal(list)
|
tracks_dropped_for_nav_transfer = pyqtSignal(list)
|
||||||
|
playlist_dropped_for_nav_transfer = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -205,6 +255,9 @@ class SidebarWidget(QWidget):
|
|||||||
QListWidget::item:hover:!selected {{
|
QListWidget::item:hover:!selected {{
|
||||||
background: {hl_light_name};
|
background: {hl_light_name};
|
||||||
}}
|
}}
|
||||||
|
#navList[dndActive="true"]::item:hover:!selected {{
|
||||||
|
background: transparent;
|
||||||
|
}}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
def _build_ui(self):
|
def _build_ui(self):
|
||||||
@ -292,6 +345,7 @@ class SidebarWidget(QWidget):
|
|||||||
layout.addWidget(line2)
|
layout.addWidget(line2)
|
||||||
|
|
||||||
self._nav_list = NavDropListWidget()
|
self._nav_list = NavDropListWidget()
|
||||||
|
self._nav_list.setObjectName("navList")
|
||||||
items = [
|
items = [
|
||||||
("🎸 iPod", "ipod"),
|
("🎸 iPod", "ipod"),
|
||||||
("⚙️ Settings", "settings"),
|
("⚙️ Settings", "settings"),
|
||||||
@ -303,6 +357,7 @@ class SidebarWidget(QWidget):
|
|||||||
self._nav_list.addItem(item)
|
self._nav_list.addItem(item)
|
||||||
self._nav_list.itemClicked.connect(self._on_nav_item_clicked)
|
self._nav_list.itemClicked.connect(self._on_nav_item_clicked)
|
||||||
self._nav_list.tracks_dropped_for_transfer.connect(self._on_tracks_dropped_for_nav_transfer)
|
self._nav_list.tracks_dropped_for_transfer.connect(self._on_tracks_dropped_for_nav_transfer)
|
||||||
|
self._nav_list.playlist_dropped_for_transfer.connect(self._on_playlist_dropped_for_transfer)
|
||||||
layout.addWidget(self._nav_list)
|
layout.addWidget(self._nav_list)
|
||||||
|
|
||||||
self._playlist_items: dict = {}
|
self._playlist_items: dict = {}
|
||||||
@ -396,6 +451,9 @@ class SidebarWidget(QWidget):
|
|||||||
def _on_tracks_dropped_for_nav_transfer(self, paths: list):
|
def _on_tracks_dropped_for_nav_transfer(self, paths: list):
|
||||||
self.tracks_dropped_for_nav_transfer.emit(paths)
|
self.tracks_dropped_for_nav_transfer.emit(paths)
|
||||||
|
|
||||||
|
def _on_playlist_dropped_for_transfer(self, playlist_id: str):
|
||||||
|
self.playlist_dropped_for_nav_transfer.emit(playlist_id)
|
||||||
|
|
||||||
def _on_playlists_reordered(self):
|
def _on_playlists_reordered(self):
|
||||||
order = self.get_playlist_order_ids()
|
order = self.get_playlist_order_ids()
|
||||||
self.item_selected.emit("playlist_reorder", "|".join(order))
|
self.item_selected.emit("playlist_reorder", "|".join(order))
|
||||||
|
|||||||
@ -44,6 +44,7 @@ class WorkerThread(QThread):
|
|||||||
"delete_tracks": self._run_delete_tracks,
|
"delete_tracks": self._run_delete_tracks,
|
||||||
"remove_duplicates": self._run_remove_duplicates,
|
"remove_duplicates": self._run_remove_duplicates,
|
||||||
"sync_metadata": self._run_sync_metadata,
|
"sync_metadata": self._run_sync_metadata,
|
||||||
|
"create_playlist": self._run_create_playlist,
|
||||||
}
|
}
|
||||||
handler = handlers.get(self.task_type)
|
handler = handlers.get(self.task_type)
|
||||||
if handler:
|
if handler:
|
||||||
@ -362,6 +363,26 @@ class WorkerThread(QThread):
|
|||||||
self.progress_signal.emit(100, msg)
|
self.progress_signal.emit(100, msg)
|
||||||
self.finished_signal.emit(synced > 0, msg, {"synced": synced, "errors": errors})
|
self.finished_signal.emit(synced > 0, msg, {"synced": synced, "errors": errors})
|
||||||
|
|
||||||
|
def _run_create_playlist(self):
|
||||||
|
mount_point = self.kwargs.get("mount_point")
|
||||||
|
name = self.kwargs.get("playlist_name", "")
|
||||||
|
track_paths = self.kwargs.get("track_paths", [])
|
||||||
|
|
||||||
|
if not mount_point or not name:
|
||||||
|
self.finished_signal.emit(False, "Missing parameters for playlist creation", None)
|
||||||
|
return
|
||||||
|
|
||||||
|
from ipod_nano7_db import Nano7Database
|
||||||
|
db = Nano7Database(mount_point)
|
||||||
|
|
||||||
|
self.progress_signal.emit(0, f"Creating playlist '{name}' on iPod...")
|
||||||
|
success = db.create_playlist(name, track_paths)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.finished_signal.emit(True, f"Playlist '{name}' created on iPod", None)
|
||||||
|
else:
|
||||||
|
self.finished_signal.emit(False, f"Failed to create playlist '{name}'", None)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.wait()
|
self.wait()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user