Fix moving tracks between playlists

This commit is contained in:
Keith Edmunds 2022-04-04 21:30:31 +01:00
parent 0a3700e208
commit c5f33c437f
2 changed files with 78 additions and 49 deletions

View File

@ -17,9 +17,10 @@ from sqlalchemy import (
DateTime, DateTime,
Float, Float,
ForeignKey, ForeignKey,
func,
Integer, Integer,
String, String,
func UniqueConstraint,
) )
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import ( from sqlalchemy.orm import (
@ -39,7 +40,6 @@ from helpers import (
) )
from log import DEBUG, ERROR from log import DEBUG, ERROR
Base: DeclarativeMeta = declarative_base() Base: DeclarativeMeta = declarative_base()
@ -265,7 +265,7 @@ class Playlists(Base):
""" """
if not row: if not row:
row = PlaylistTracks.next_free_row(session, self) row = PlaylistTracks.next_free_row(session, self.id)
PlaylistTracks(session, self.id, track_id, row) PlaylistTracks(session, self.id, track_id, row)
@ -317,7 +317,8 @@ class Playlists(Base):
self.last_used = datetime.now() self.last_used = datetime.now()
session.flush() session.flush()
def move_track(self, session: Session, rows: List[int], def move_track(
self, session: Session, rows: List[int],
to_playlist: "Playlists") -> None: to_playlist: "Playlists") -> None:
"""Move tracks to another playlist""" """Move tracks to another playlist"""
@ -367,6 +368,10 @@ class PlaylistTracks(Base):
cascade="all, delete-orphan" cascade="all, delete-orphan"
) )
) )
# Ensure row numbers are unique within each playlist
__table_args__ = (UniqueConstraint
('row', 'playlist_id', name="uniquerow"),
)
def __init__( def __init__(
self, session: Session, playlist_id: int, track_id: int, self, session: Session, playlist_id: int, track_id: int,
@ -380,14 +385,14 @@ class PlaylistTracks(Base):
session.flush() session.flush()
@staticmethod @staticmethod
def next_free_row(session: Session, playlist: Playlists) -> int: def next_free_row(session: Session, playlist_id: int) -> int:
"""Return next free row number""" """Return next free row number"""
row: int row: int
last_row = session.query( last_row = session.query(
func.max(PlaylistTracks.row) func.max(PlaylistTracks.row)
).filter_by(playlist_id=playlist.id).first() ).filter_by(playlist_id=playlist_id).first()
# if there are no rows, the above returns (None, ) which is True # if there are no rows, the above returns (None, ) which is True
if last_row and last_row[0] is not None: if last_row and last_row[0] is not None:
row = last_row[0] + 1 row = last_row[0] + 1
@ -396,6 +401,33 @@ class PlaylistTracks(Base):
return row return row
@staticmethod
def move_rows(
session: Session, rows: List[int], from_playlist_id: int,
to_playlist_id: int) -> None:
"""Move rows between playlists"""
# A constraint deliberately blocks duplicate (playlist_id, row)
# entries in database; however, unallocated rows in the database
# are fine (ie, we can have rows 1, 4, 6 and no 2, 3, 5).
# Unallocated rows will be automatically removed when the
# playlist is saved.
lowest_source_row: int = min(rows)
first_destination_free_row = PlaylistTracks.next_free_row(
session, to_playlist_id)
# Calculate offset that will put the lowest row number being
# moved at the first free row in destination playlist
offset = first_destination_free_row - lowest_source_row
session.query(PlaylistTracks).filter(
PlaylistTracks.playlist_id == from_playlist_id,
PlaylistTracks.row.in_(rows)
).update({'playlist_id': to_playlist_id,
'row': PlaylistTracks.row + offset},
False
)
class Settings(Base): class Settings(Base):
__tablename__ = 'settings' __tablename__ = 'settings'
@ -450,7 +482,8 @@ class Tracks(Base):
back_populates="tracks", back_populates="tracks",
lazy="joined") lazy="joined")
def __init__(self, def __init__(
self,
session: Session, session: Session,
path: str, path: str,
title: Optional[str] = None, title: Optional[str] = None,

View File

@ -424,7 +424,8 @@ class Window(QMainWindow, Ui_MainWindow):
# Update database for both source and destination playlists # Update database for both source and destination playlists
rows = visible_tab.get_selected_rows() rows = visible_tab.get_selected_rows()
source_playlist.move_track(session, rows, destination_playlist) PlaylistTracks.move_rows(session, rows, source_playlist.id,
destination_playlist.id)
# Update destination playlist_tab if visible (if not visible, it # Update destination playlist_tab if visible (if not visible, it
# will be re-populated when it is opened) # will be re-populated when it is opened)
@ -594,10 +595,10 @@ class Window(QMainWindow, Ui_MainWindow):
dlg = SelectPlaylistDialog(self, playlists=playlists, dlg = SelectPlaylistDialog(self, playlists=playlists,
session=session) session=session)
dlg.exec() dlg.exec()
if dlg.plid: playlist = dlg.playlist
p = Playlists.get_by_id(session=session, playlist_id=dlg.plid) if playlist:
p.mark_open(session) playlist.mark_open(session)
self.create_playlist_tab(session, p) self.create_playlist_tab(session, playlist)
def select_next_row(self) -> None: def select_next_row(self) -> None:
"""Select next or first row in playlist""" """Select next or first row in playlist"""
@ -971,11 +972,6 @@ class SelectPlaylistDialog(QDialog):
height = record.f_int or 600 height = record.f_int or 600
self.resize(width, height) self.resize(width, height)
# for (plid, plname) in [(a.id, a.name) for a in playlists]:
# p = QListWidgetItem()
# p.setText(plname)
# p.setData(Qt.UserRole, plid)
# self.ui.lstPlaylists.addItem(p)
for playlist in playlists: for playlist in playlists:
p = QListWidgetItem() p = QListWidgetItem()
p.setText(playlist.name) p.setText(playlist.name)