Compare commits

..

6 Commits

Author SHA1 Message Date
Keith Edmunds
f2a27366d3 Fix deleting rows from playlist 2022-12-23 21:27:06 +00:00
Keith Edmunds
46f2b662f3 Copy/paste, insert track/header works 2022-12-23 20:52:18 +00:00
Keith Edmunds
647e7d478a Move rows works. 2022-12-23 20:37:21 +00:00
Keith Edmunds
444c3e4fb4 Remove rows from playlist works and db updates 2022-12-23 20:15:07 +00:00
Keith Edmunds
35b101a538 Tidy up saving database 2022-12-23 17:23:43 +00:00
Keith Edmunds
d3958db8a3 Fix crash if create new playlist is cancelled 2022-12-23 09:27:14 +00:00
3 changed files with 256 additions and 203 deletions

View File

@ -2,6 +2,7 @@
# #
import os.path import os.path
import re import re
import stackprinter
# #
from dbconfig import Session from dbconfig import Session
# #
@ -417,7 +418,7 @@ class PlaylistRows(Base):
return ( return (
f"<PlaylistRow(id={self.id}, playlist_id={self.playlist_id}, " f"<PlaylistRow(id={self.id}, playlist_id={self.playlist_id}, "
f"track_id={self.track_id}, " f"track_id={self.track_id}, "
f"note={self.note} row_number={self.row_number}>" f"note={self.note}, row_number={self.row_number}>"
) )
def __init__(self, def __init__(self,
@ -452,31 +453,22 @@ class PlaylistRows(Base):
plr.note) plr.note)
@staticmethod @staticmethod
def delete_higher_rows(session: Session, playlist_id: int, row: int) \ def delete_plrids_not_in_list(session: Session, playlist_id: int,
-> None: plrids: List["PlaylistRows"]) -> None:
""" """
Delete rows in given playlist that have a higher row number Delete rows in given playlist that have a higher row number
than 'row' than 'maxrow'
""" """
# Log the rows to be deleted session.execute(
rows_to_go = session.execute( delete(PlaylistRows)
select(PlaylistRows) .where(
.where(PlaylistRows.playlist_id == playlist_id, PlaylistRows.playlist_id == playlist_id,
PlaylistRows.row_number > row) PlaylistRows.id.not_in(plrids)
).scalars().all() )
if not rows_to_go: )
return # Delete won't take effect until commit()
session.commit()
for row in rows_to_go:
log.debug(f"Should delete: {row}")
# If needed later:
# session.delete(row)
rows_to_go = session.execute(
select(PlaylistRows)
.where(PlaylistRows.playlist_id == playlist_id,
PlaylistRows.row_number > row)
).scalars().all()
@staticmethod @staticmethod
def delete_rows(session: Session, ids: List[int]) -> None: def delete_rows(session: Session, ids: List[int]) -> None:
@ -509,6 +501,42 @@ class PlaylistRows(Base):
# Ensure new row numbers are available to the caller # Ensure new row numbers are available to the caller
session.commit() session.commit()
@classmethod
def get_plr(session: Session, row_number: int,
playlist_id: int) -> "PlaylistRows":
"""Return playlistrows object matching passed parameters"""
return (
select(PlaylistRows)
.where(
PlaylistRows.row_number == row_number,
PlaylistRows.playlist_id == playlist_id)
.limit(1)
).first()
@staticmethod
def get_track_plr(session: Session, track_id: int,
playlist_id: int) -> Optional["PlaylistRows"]:
"""Return first matching PlaylistRows object or None"""
return session.scalars(
select(PlaylistRows)
.where(
PlaylistRows.track_id == track_id,
PlaylistRows.playlist_id == playlist_id
)
.limit(1)
).first()
@staticmethod
def get_last_used_row(session: Session, playlist_id: int) -> Optional[int]:
"""Return the last used row for playlist, or None if no rows"""
return session.execute(
select(func.max(PlaylistRows.row_number))
.where(PlaylistRows.playlist_id == playlist_id)
).scalar_one()
@classmethod @classmethod
def get_played_rows(cls, session: Session, def get_played_rows(cls, session: Session,
playlist_id: int) -> List[int]: playlist_id: int) -> List[int]:
@ -547,15 +575,6 @@ class PlaylistRows(Base):
return plrs return plrs
@staticmethod
def get_last_used_row(session: Session, playlist_id: int) -> Optional[int]:
"""Return the last used row for playlist, or None if no rows"""
return session.execute(
select(func.max(PlaylistRows.row_number))
.where(PlaylistRows.playlist_id == playlist_id)
).scalar_one()
@classmethod @classmethod
def get_unplayed_rows(cls, session: Session, def get_unplayed_rows(cls, session: Session,
playlist_id: int) -> List[int]: playlist_id: int) -> List[int]:
@ -594,23 +613,22 @@ class PlaylistRows(Base):
) )
@staticmethod @staticmethod
def indexed_by_row(session: Session, playlist_id: int) -> dict: def indexed_by_id(session: Session, plr_ids: List[int]) -> dict:
""" """
Return a dictionary of playlist_rows indexed by row number for Return a dictionary of playlist_rows indexed by their plr id from
the passed playlist_id. the passed plr_id list.
""" """
plrs = session.execute( plrs = session.execute(
select(PlaylistRows) select(PlaylistRows)
.where( .where(
PlaylistRows.playlist_id == playlist_id, PlaylistRows.id.in_(plr_ids)
) )
.order_by(PlaylistRows.row_number)
).scalars().all() ).scalars().all()
result = {} result = {}
for plr in plrs: for plr in plrs:
result[plr.row_number] = plr result[plr.id] = plr
return result return result

View File

@ -2,7 +2,7 @@
from log import log from log import log
import argparse import argparse
import stackprinter import stackprinter # type: ignore
import subprocess import subprocess
import sys import sys
import threading import threading
@ -437,6 +437,8 @@ class Window(QMainWindow, Ui_MainWindow):
if not playlist_name: if not playlist_name:
playlist_name = self.solicit_playlist_name() playlist_name = self.solicit_playlist_name()
if not playlist_name:
return
playlist = Playlists(session, playlist_name) playlist = Playlists(session, playlist_name)
return playlist return playlist
@ -446,6 +448,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
playlist = self.create_playlist(session) playlist = self.create_playlist(session)
if playlist:
self.create_playlist_tab(session, playlist) self.create_playlist_tab(session, playlist)
def create_playlist_tab(self, session: Session, def create_playlist_tab(self, session: Session,
@ -736,6 +739,7 @@ class Window(QMainWindow, Ui_MainWindow):
helpers.set_track_metadata(session, track) helpers.set_track_metadata(session, track)
helpers.normalise_track(track.path) helpers.normalise_track(track.path)
self.visible_playlist_tab().insert_track(session, track) self.visible_playlist_tab().insert_track(session, track)
self.visible_playlist_tab().save_playlist(session)
def insert_header(self) -> None: def insert_header(self) -> None:
"""Show dialog box to enter header text and add to playlist""" """Show dialog box to enter header text and add to playlist"""
@ -755,6 +759,7 @@ class Window(QMainWindow, Ui_MainWindow):
if ok: if ok:
with Session() as session: with Session() as session:
playlist_tab.insert_header(session, dlg.textValue()) playlist_tab.insert_header(session, dlg.textValue())
playlist_tab.save_playlist(session)
def insert_track(self) -> None: def insert_track(self) -> None:
"""Show dialog box to select and add track from database""" """Show dialog box to select and add track from database"""
@ -792,6 +797,8 @@ class Window(QMainWindow, Ui_MainWindow):
# Identify destination playlist # Identify destination playlist
visible_tab = self.visible_playlist_tab() visible_tab = self.visible_playlist_tab()
source_playlist = visible_tab.playlist_id source_playlist = visible_tab.playlist_id
# Get destination playlist id
playlists = [] playlists = []
for playlist in Playlists.get_all(session): for playlist in Playlists.get_all(session):
if playlist.id == source_playlist: if playlist.id == source_playlist:
@ -799,17 +806,17 @@ class Window(QMainWindow, Ui_MainWindow):
else: else:
playlists.append(playlist) playlists.append(playlist)
# Get destination playlist id
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session) dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
dlg.exec() dlg.exec()
if not dlg.playlist: if not dlg.playlist:
return return
destination_playlist_id = dlg.playlist.id destination_playlist_id = dlg.playlist.id
# Remove moved rows from display # Remove moved rows from display and save
visible_tab.remove_rows([plr.row_number for plr in playlistrows]) visible_tab.remove_rows([plr.row_number for plr in playlistrows])
visible_tab.save_playlist(session)
# Update playlist for the rows in the database # Update destination playlist in the database
last_row = PlaylistRows.get_last_used_row(session, last_row = PlaylistRows.get_last_used_row(session,
destination_playlist_id) destination_playlist_id)
if last_row is not None: if last_row is not None:
@ -825,13 +832,13 @@ class Window(QMainWindow, Ui_MainWindow):
# 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)
destionation_playlist_tab = None destination_playlist_tab = None
for tab in range(self.tabPlaylist.count()): for tab in range(self.tabPlaylist.count()):
if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id: if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id:
destionation_playlist_tab = self.tabPlaylist.widget(tab) destination_playlist_tab = self.tabPlaylist.widget(tab)
break break
if destionation_playlist_tab: if destination_playlist_tab:
destionation_playlist_tab.populate(session, dlg.playlist.id) destination_playlist_tab.populate_display(session, dlg.playlist.id)
def move_selected(self) -> None: def move_selected(self) -> None:
""" """
@ -913,17 +920,17 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_tab = self.visible_playlist_tab() playlist_tab = self.visible_playlist_tab()
dst_playlist_id = playlist_tab.playlist_id dst_playlist_id = playlist_tab.playlist_id
dst_row = self.visible_playlist_tab().get_new_row_number()
with Session() as session: with Session() as session:
# Create space in destination playlist # Create space in destination playlist
if playlist_tab.selectionModel().hasSelection():
row = playlist_tab.currentRow()
PlaylistRows.move_rows_down(session, dst_playlist_id, PlaylistRows.move_rows_down(session, dst_playlist_id,
row, len(self.selected_plrs)) dst_row, len(self.selected_plrs))
session.commit() session.commit()
# Update plrs
row = dst_row
src_playlist_id = None src_playlist_id = None
dst_row = row
for plr in self.selected_plrs: for plr in self.selected_plrs:
# Update moved rows # Update moved rows
session.add(plr) session.add(plr)
@ -936,8 +943,8 @@ class Window(QMainWindow, Ui_MainWindow):
session.commit() session.commit()
# Update display # Update display
self.visible_playlist_tab().populate(session, dst_playlist_id, self.visible_playlist_tab().populate_display(
scroll_to_top=False) session, dst_playlist_id, scroll_to_top=False)
# If source playlist is not destination playlist, fixup row # If source playlist is not destination playlist, fixup row
# numbers and update display # numbers and update display
@ -952,8 +959,8 @@ class Window(QMainWindow, Ui_MainWindow):
source_playlist_tab = self.tabPlaylist.widget(tab) source_playlist_tab = self.tabPlaylist.widget(tab)
break break
if source_playlist_tab: if source_playlist_tab:
source_playlist_tab.populate(session, src_playlist_id, source_playlist_tab.populate_display(
scroll_to_top=False) session, src_playlist_id, scroll_to_top=False)
# Reset so rows can't be repasted # Reset so rows can't be repasted
self.selected_plrs = None self.selected_plrs = None
@ -1497,8 +1504,8 @@ class DbDialog(QDialog):
self.parent().visible_playlist_tab().insert_track( self.parent().visible_playlist_tab().insert_track(
self.session, track, note=self.ui.txtNote.text()) self.session, track, note=self.ui.txtNote.text())
# Commit session to get correct row numbers if more tracks added # Save to database (which will also commit changes)
self.session.commit() self.parent().visible_playlist_tab().save_playlist(self.session)
# Clear note field and select search text to make it easier for # Clear note field and select search text to make it easier for
# next search # next search
self.ui.txtNote.clear() self.ui.txtNote.clear()

View File

@ -1,5 +1,5 @@
import re import re
import stackprinter import stackprinter # type: ignore
import subprocess import subprocess
import threading import threading
@ -199,7 +199,7 @@ class PlaylistTab(QTableWidget):
# self.setSortingEnabled(True) # self.setSortingEnabled(True)
# Now load our tracks and notes # Now load our tracks and notes
self.populate(session, self.playlist_id) self.populate_display(session, self.playlist_id)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<PlaylistTab(id={self.playlist_id}>" return f"<PlaylistTab(id={self.playlist_id}>"
@ -327,8 +327,8 @@ class PlaylistTab(QTableWidget):
act_setnext.triggered.connect( act_setnext.triggered.connect(
lambda: self._set_next(session, row_number)) lambda: self._set_next(session, row_number))
# Open in Audacity
if not current: if not current:
# Open in Audacity
act_audacity = self.menu.addAction( act_audacity = self.menu.addAction(
"Open in Audacity") "Open in Audacity")
act_audacity.triggered.connect( act_audacity.triggered.connect(
@ -557,6 +557,17 @@ class PlaylistTab(QTableWidget):
self.clearSelection() self.clearSelection()
self.setDragEnabled(False) self.setDragEnabled(False)
def get_new_row_number(self) -> int:
"""
Return the selected row or the row count if no row selected
(ie, new row will be appended)
"""
if self.selectionModel().hasSelection():
return self.currentRow()
else:
return self.rowCount()
def get_selected_playlistrow_ids(self) -> Optional[List]: def get_selected_playlistrow_ids(self) -> Optional[List]:
""" """
Return a list of PlaylistRow ids of the selected rows Return a list of PlaylistRow ids of the selected rows
@ -584,63 +595,49 @@ class PlaylistTab(QTableWidget):
to do the heavy lifing. to do the heavy lifing.
""" """
# PlaylistRows object requires a row number, but that number row_number = self.get_new_row_number()
# can be reset by calling PlaylistRows.fixup_rownumbers() later,
# so just fudge a row number for now.
row_number = 0
plr = PlaylistRows(session, self.playlist_id, None, row_number, note) plr = PlaylistRows(session, self.playlist_id, None, row_number, note)
self.insert_row(session, plr) self.insert_row(session, plr, repaint)
PlaylistRows.fixup_rownumbers(session, self.playlist_id) self.save_playlist(session)
if repaint:
self.update_display(session, clear_selection=False)
def insert_row(self, session: Session, row_data: PlaylistRows, def insert_row(self, session: Session, plr: PlaylistRows,
repaint: bool = True) -> None: repaint: bool = True) -> None:
""" """
Insert a row into playlist tab. Insert passed playlist row (plr) into playlist tab.
If playlist has a row selected, add new row above. Otherwise,
add to end of playlist.
Note: we ignore the row number in the PlaylistRows record. That is
used only to order the query that generates the records.
""" """
if self.selectionModel().hasSelection(): row = plr.row_number
row = self.currentRow()
else:
row = self.rowCount()
self.insertRow(row) self.insertRow(row)
# Add row metadata to userdata column # Add row metadata to userdata column
userdata_item = QTableWidgetItem() userdata_item = QTableWidgetItem()
userdata_item.setData(self.ROW_FLAGS, 0) userdata_item.setData(self.ROW_FLAGS, 0)
userdata_item.setData(self.PLAYLISTROW_ID, row_data.id) userdata_item.setData(self.PLAYLISTROW_ID, plr.id)
userdata_item.setData(self.ROW_TRACK_ID, row_data.track_id) userdata_item.setData(self.ROW_TRACK_ID, plr.track_id)
self.setItem(row, USERDATA, userdata_item) self.setItem(row, USERDATA, userdata_item)
if row_data.track_id: if plr.track_id:
# Add track details to items # Add track details to items
try: try:
start_gap = row_data.track.start_gap start_gap = plr.track.start_gap
except: except AttributeError:
return return
start_gap_item = QTableWidgetItem(str(start_gap)) start_gap_item = QTableWidgetItem(str(start_gap))
if start_gap and start_gap >= 500: if start_gap and start_gap >= 500:
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START)) start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
self.setItem(row, START_GAP, start_gap_item) self.setItem(row, START_GAP, start_gap_item)
title_item = QTableWidgetItem(row_data.track.title) title_item = QTableWidgetItem(plr.track.title)
log.debug(f"KAE: insert_row:619, {title_item.text()=}") log.debug(f"KAE: insert_row:619, {title_item.text()=}")
self.setItem(row, TITLE, title_item) self.setItem(row, TITLE, title_item)
artist_item = QTableWidgetItem(row_data.track.artist) artist_item = QTableWidgetItem(plr.track.artist)
self.setItem(row, ARTIST, artist_item) self.setItem(row, ARTIST, artist_item)
duration_item = QTableWidgetItem( duration_item = QTableWidgetItem(
ms_to_mmss(row_data.track.duration)) ms_to_mmss(plr.track.duration))
self.setItem(row, DURATION, duration_item) self.setItem(row, DURATION, duration_item)
self._set_row_duration(row, row_data.track.duration) self._set_row_duration(row, plr.track.duration)
start_item = QTableWidgetItem() start_item = QTableWidgetItem()
self.setItem(row, START_TIME, start_item) self.setItem(row, START_TIME, start_item)
@ -648,8 +645,8 @@ class PlaylistTab(QTableWidget):
end_item = QTableWidgetItem() end_item = QTableWidgetItem()
self.setItem(row, END_TIME, end_item) self.setItem(row, END_TIME, end_item)
if row_data.track.bitrate: if plr.track.bitrate:
bitrate = str(row_data.track.bitrate) bitrate = str(plr.track.bitrate)
else: else:
bitrate = "" bitrate = ""
bitrate_item = QTableWidgetItem(bitrate) bitrate_item = QTableWidgetItem(bitrate)
@ -657,23 +654,23 @@ class PlaylistTab(QTableWidget):
# As we have track info, any notes should be contained in # As we have track info, any notes should be contained in
# the notes column # the notes column
notes_item = QTableWidgetItem(row_data.note) notes_item = QTableWidgetItem(plr.note)
self.setItem(row, ROW_NOTES, notes_item) self.setItem(row, ROW_NOTES, notes_item)
last_playtime = Playdates.last_played(session, row_data.track.id) last_playtime = Playdates.last_played(session, plr.track.id)
last_played_str = get_relative_date(last_playtime) last_played_str = get_relative_date(last_playtime)
last_played_item = QTableWidgetItem(last_played_str) last_played_item = QTableWidgetItem(last_played_str)
self.setItem(row, LASTPLAYED, last_played_item) self.setItem(row, LASTPLAYED, last_played_item)
# Mark track if file is unreadable # Mark track if file is unreadable
if not file_is_readable(row_data.track.path): if not file_is_readable(plr.track.path):
self._set_unreadable_row(row) self._set_unreadable_row(row)
else: else:
# This is a section header so it must have note text # This is a section header so it must have note text
if row_data.note is None: if plr.note is None:
log.debug( log.debug(
f"insert_row({row_data=}) with no track_id and no note" f"insert_row({plr=}) with no track_id and no note"
) )
return return
@ -687,17 +684,16 @@ class PlaylistTab(QTableWidget):
continue continue
self.setItem(row, i, QTableWidgetItem()) self.setItem(row, i, QTableWidgetItem())
self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1) self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
notes_item = QTableWidgetItem(row_data.note) notes_item = QTableWidgetItem(plr.note)
self.setItem(row, HEADER_NOTES_COLUMN, notes_item) self.setItem(row, HEADER_NOTES_COLUMN, notes_item)
# Save (no) track_id # Save (no) track_id
userdata_item.setData(self.ROW_TRACK_ID, 0) userdata_item.setData(self.ROW_TRACK_ID, 0)
if repaint: if repaint:
self.save_playlist(session)
self.update_display(session, clear_selection=False) self.update_display(session, clear_selection=False)
def insert_track(self, session: Session, track: Optional[Tracks], def insert_track(self, session: Session, track: Tracks,
note: str = None, repaint: bool = True) -> None: note: str = None, repaint: bool = True) -> None:
""" """
Insert track into playlist tab. Insert track into playlist tab.
@ -709,20 +705,30 @@ class PlaylistTab(QTableWidget):
to do the heavy lifing. to do the heavy lifing.
""" """
# PlaylistRows object requires a row number, but that number if not track and track.id:
# can be reset by calling PlaylistRows.fixup_rownumbers() later, log.debug(
# so just fudge a row number for now. f"insert_track({session=}, {track=}, {note=}, {repaint=}"
row_number = 0 " called with either no track or no track.id"
if track: )
track_id = track.id return
else:
track_id = None row_number = self.get_new_row_number()
plr = PlaylistRows(session, self.playlist_id,
track_id, row_number, note) # Check to see whether track is already in playlist
self.insert_row(session, plr) existing_plr = PlaylistRows.get_track_plr(session, track.id,
PlaylistRows.fixup_rownumbers(session, self.playlist_id) self.playlist_id)
if repaint: if existing_plr and ask_yes_no("Duplicate row",
self.update_display(session, clear_selection=False) "Track already in playlist. "
"Move to new location?"):
# Yes it is and we shoudl reuse it
return self._move_row(session, existing_plr, row_number)
# Build playlist_row object
plr = PlaylistRows(session, self.playlist_id, track.id,
row_number, note)
self.insert_row(session, plr, repaint)
# Let display update, then save playlist
QTimer.singleShot(0, lambda: self.save_playlist(session))
def play_started(self, session: Session) -> None: def play_started(self, session: Session) -> None:
""" """
@ -771,10 +777,10 @@ class PlaylistTab(QTableWidget):
self._clear_current_track_row() self._clear_current_track_row()
self.current_track_start_time = None self.current_track_start_time = None
def populate(self, session: Session, playlist_id: int, def populate_display(self, session: Session, playlist_id: int,
scroll_to_top: bool = True) -> None: scroll_to_top: bool = True) -> None:
""" """
Populate from the associated playlist ID Populate display from the associated playlist ID
""" """
# Sanity check row numbering before we load # Sanity check row numbering before we load
@ -785,8 +791,8 @@ class PlaylistTab(QTableWidget):
# Add the rows # Add the rows
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
for row in playlist.rows: for plr in playlist.rows:
self.insert_row(session, row, repaint=False) self.insert_row(session, plr, repaint=False)
# Scroll to top # Scroll to top
if scroll_to_top: if scroll_to_top:
@ -822,22 +828,39 @@ class PlaylistTab(QTableWidget):
def save_playlist(self, session: Session) -> None: def save_playlist(self, session: Session) -> None:
""" """
All playlist rows have a PlaylistRows id. Check that that id points Get the PlaylistRow objects for each row in the display. Correct
to this playlist (in case track has been moved from other) and that the row_number and playlist_id if necessary. Remove any row
the row number is correct (in case tracks have been reordered). numbers in the database that are higher than the last row in
the display.
""" """
plr_dict = PlaylistRows.indexed_by_row(session, self.playlist_id) # Build a dictionary of
for row in range(self.rowCount()): # {display_row_number: display_row_plr_id}
# Set the row number and playlist id (even if correct) display_plr_ids = {row_number: self._get_playlistrow_id(row_number)
plr_dict[row].row_number = row for row_number in range(self.rowCount())}
plr_dict[row].playlist_id = self.playlist_id
# Any rows in the database with a row_number higher that the # Now build a dictionary of
# current value of 'row' should not be there. Commit session # {display_row_number: display_row_plr}
# first to ensure any changes made above are committed. plr_dict_by_id = PlaylistRows.indexed_by_id(session,
display_plr_ids.values())
# Finally a dictionary of
# {display_row_number: plr}
row_plr = {row_number: plr_dict_by_id[display_plr_ids[row_number]]
for row_number in range(self.rowCount())}
# Ensure all row plrs have correct row number and playlist_id
for row in range(self.rowCount()):
row_plr[row].row_number = row
row_plr[row].playlist_id = self.playlist_id
# Any rows in the database for this playlist that have a plr id
# that's not in the displayed playlist need to be deleted.
# Ensure changes flushed
session.commit() session.commit()
PlaylistRows.delete_higher_rows(session, self.playlist_id, row) PlaylistRows.delete_plrids_not_in_list(session, self.playlist_id,
display_plr_ids.values())
def scroll_current_to_top(self) -> None: def scroll_current_to_top(self) -> None:
"""Scroll currently-playing row to top""" """Scroll currently-playing row to top"""
@ -860,65 +883,6 @@ class PlaylistTab(QTableWidget):
return return
self._search(next=True) self._search(next=True)
def _search(self, next: bool = True) -> None:
"""
Select next/previous row containg self.search_string. Start from
top selected row if there is one, else from top.
Wrap at last/first row.
"""
if not self.search_text:
return
selected_row = self._get_selected_row()
if next:
if selected_row is not None and selected_row < self.rowCount() - 1:
starting_row = selected_row + 1
else:
starting_row = 0
else:
if selected_row is not None and selected_row > 0:
starting_row = selected_row - 1
else:
starting_row = self.rowCount() - 1
wrapped = False
match_row = None
row = starting_row
needle = self.search_text.lower()
while True:
# Check for match in title, artist or notes
title = self._get_row_title(row)
if title and needle in title.lower():
match_row = row
break
artist = self._get_row_artist(row)
if artist and needle in artist.lower():
match_row = row
break
note = self._get_row_note(row)
if note and needle in note.lower():
match_row = row
break
if next:
row += 1
if wrapped and row >= starting_row:
break
if row >= self.rowCount():
row = 0
wrapped = True
else:
row -= 1
if wrapped and row <= starting_row:
break
if row < 0:
row = self.rowCount() - 1
wrapped = True
if match_row is not None:
self.selectRow(row)
def search_next(self) -> None: def search_next(self) -> None:
""" """
Select next row containg self.search_string. Select next row containg self.search_string.
@ -1344,9 +1308,8 @@ class PlaylistTab(QTableWidget):
Delete mutliple rows Delete mutliple rows
Actions required: Actions required:
- Delete the rows from the PlaylistRows table
- Correct the row numbers in the PlaylistRows table
- Remove the rows from the display - Remove the rows from the display
- Save the playlist
""" """
# Delete rows from database # Delete rows from database
@ -1359,14 +1322,11 @@ class PlaylistTab(QTableWidget):
f"Really delete {row_count} row{plural}?"): f"Really delete {row_count} row{plural}?"):
return return
with Session() as session:
PlaylistRows.delete_rows(session, plr_ids)
# Fix up row numbers left in this playlist
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
# Remove selected rows from display
self.remove_selected_rows() self.remove_selected_rows()
with Session() as session:
QTimer.singleShot(0, lambda: self.save_playlist(session))
def _drop_on(self, event): def _drop_on(self, event):
""" """
https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget
@ -1545,11 +1505,6 @@ class PlaylistTab(QTableWidget):
return self._meta_search(RowMeta.UNREADABLE, one=False) return self._meta_search(RowMeta.UNREADABLE, one=False)
# def _header_click(self, index: int) -> None:
# """Handle playlist header click"""
# print(f"_header_click({index=})")
def _info_row(self, track_id: int) -> None: def _info_row(self, track_id: int) -> None:
"""Display popup with info re row""" """Display popup with info re row"""
@ -1658,6 +1613,20 @@ class PlaylistTab(QTableWidget):
new_metadata = self._meta_get(row) | (1 << attribute) new_metadata = self._meta_get(row) | (1 << attribute)
self.item(row, USERDATA).setData(self.ROW_FLAGS, new_metadata) self.item(row, USERDATA).setData(self.ROW_FLAGS, new_metadata)
def _move_row(self, session: Session, plr: PlaylistRows,
new_row_number: int) -> None:
"""Move playlist row to new_row_number using parent copy/paste"""
# Remove source row
self.removeRow(plr.row_number)
# Fixup plr row number
if plr.row_number < new_row_number:
plr.row_number = new_row_number - 1
else:
plr.row_number = new_row_number
self.insert_row(session, plr)
self.save_playlist(session)
def _mplayer_play(self, track_id: int) -> None: def _mplayer_play(self, track_id: int) -> None:
"""Play track with mplayer""" """Play track with mplayer"""
@ -1769,6 +1738,65 @@ class PlaylistTab(QTableWidget):
scroll_item = self.item(top_row, 0) scroll_item = self.item(top_row, 0)
self.scrollToItem(scroll_item, QAbstractItemView.PositionAtTop) self.scrollToItem(scroll_item, QAbstractItemView.PositionAtTop)
def _search(self, next: bool = True) -> None:
"""
Select next/previous row containg self.search_string. Start from
top selected row if there is one, else from top.
Wrap at last/first row.
"""
if not self.search_text:
return
selected_row = self._get_selected_row()
if next:
if selected_row is not None and selected_row < self.rowCount() - 1:
starting_row = selected_row + 1
else:
starting_row = 0
else:
if selected_row is not None and selected_row > 0:
starting_row = selected_row - 1
else:
starting_row = self.rowCount() - 1
wrapped = False
match_row = None
row = starting_row
needle = self.search_text.lower()
while True:
# Check for match in title, artist or notes
title = self._get_row_title(row)
if title and needle in title.lower():
match_row = row
break
artist = self._get_row_artist(row)
if artist and needle in artist.lower():
match_row = row
break
note = self._get_row_note(row)
if note and needle in note.lower():
match_row = row
break
if next:
row += 1
if wrapped and row >= starting_row:
break
if row >= self.rowCount():
row = 0
wrapped = True
else:
row -= 1
if wrapped and row <= starting_row:
break
if row < 0:
row = self.rowCount() - 1
wrapped = True
if match_row is not None:
self.selectRow(row)
def _select_event(self) -> None: def _select_event(self) -> None:
""" """
Called when item selection changes. Called when item selection changes.