Compare commits

..

No commits in common. "f2a27366d3ee0f5d380c78fea4f3bd6dbd81726d" and "be4f19757cf2cb6374aa9aeda688683df2214624" have entirely different histories.

3 changed files with 202 additions and 255 deletions

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import re
import stackprinter # type: ignore
import stackprinter
import subprocess
import threading
@ -199,7 +199,7 @@ class PlaylistTab(QTableWidget):
# self.setSortingEnabled(True)
# Now load our tracks and notes
self.populate_display(session, self.playlist_id)
self.populate(session, self.playlist_id)
def __repr__(self) -> str:
return f"<PlaylistTab(id={self.playlist_id}>"
@ -327,8 +327,8 @@ class PlaylistTab(QTableWidget):
act_setnext.triggered.connect(
lambda: self._set_next(session, row_number))
if not current:
# Open in Audacity
if not current:
act_audacity = self.menu.addAction(
"Open in Audacity")
act_audacity.triggered.connect(
@ -557,17 +557,6 @@ class PlaylistTab(QTableWidget):
self.clearSelection()
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]:
"""
Return a list of PlaylistRow ids of the selected rows
@ -595,49 +584,63 @@ class PlaylistTab(QTableWidget):
to do the heavy lifing.
"""
row_number = self.get_new_row_number()
# PlaylistRows object requires a row number, but that 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)
self.insert_row(session, plr, repaint)
self.save_playlist(session)
self.insert_row(session, plr)
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
if repaint:
self.update_display(session, clear_selection=False)
def insert_row(self, session: Session, plr: PlaylistRows,
def insert_row(self, session: Session, row_data: PlaylistRows,
repaint: bool = True) -> None:
"""
Insert passed playlist row (plr) into playlist tab.
Insert a row 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.
"""
row = plr.row_number
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
self.insertRow(row)
# Add row metadata to userdata column
userdata_item = QTableWidgetItem()
userdata_item.setData(self.ROW_FLAGS, 0)
userdata_item.setData(self.PLAYLISTROW_ID, plr.id)
userdata_item.setData(self.ROW_TRACK_ID, plr.track_id)
userdata_item.setData(self.PLAYLISTROW_ID, row_data.id)
userdata_item.setData(self.ROW_TRACK_ID, row_data.track_id)
self.setItem(row, USERDATA, userdata_item)
if plr.track_id:
if row_data.track_id:
# Add track details to items
try:
start_gap = plr.track.start_gap
except AttributeError:
start_gap = row_data.track.start_gap
except:
return
start_gap_item = QTableWidgetItem(str(start_gap))
if start_gap and start_gap >= 500:
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
self.setItem(row, START_GAP, start_gap_item)
title_item = QTableWidgetItem(plr.track.title)
title_item = QTableWidgetItem(row_data.track.title)
log.debug(f"KAE: insert_row:619, {title_item.text()=}")
self.setItem(row, TITLE, title_item)
artist_item = QTableWidgetItem(plr.track.artist)
artist_item = QTableWidgetItem(row_data.track.artist)
self.setItem(row, ARTIST, artist_item)
duration_item = QTableWidgetItem(
ms_to_mmss(plr.track.duration))
ms_to_mmss(row_data.track.duration))
self.setItem(row, DURATION, duration_item)
self._set_row_duration(row, plr.track.duration)
self._set_row_duration(row, row_data.track.duration)
start_item = QTableWidgetItem()
self.setItem(row, START_TIME, start_item)
@ -645,8 +648,8 @@ class PlaylistTab(QTableWidget):
end_item = QTableWidgetItem()
self.setItem(row, END_TIME, end_item)
if plr.track.bitrate:
bitrate = str(plr.track.bitrate)
if row_data.track.bitrate:
bitrate = str(row_data.track.bitrate)
else:
bitrate = ""
bitrate_item = QTableWidgetItem(bitrate)
@ -654,23 +657,23 @@ class PlaylistTab(QTableWidget):
# As we have track info, any notes should be contained in
# the notes column
notes_item = QTableWidgetItem(plr.note)
notes_item = QTableWidgetItem(row_data.note)
self.setItem(row, ROW_NOTES, notes_item)
last_playtime = Playdates.last_played(session, plr.track.id)
last_playtime = Playdates.last_played(session, row_data.track.id)
last_played_str = get_relative_date(last_playtime)
last_played_item = QTableWidgetItem(last_played_str)
self.setItem(row, LASTPLAYED, last_played_item)
# Mark track if file is unreadable
if not file_is_readable(plr.track.path):
if not file_is_readable(row_data.track.path):
self._set_unreadable_row(row)
else:
# This is a section header so it must have note text
if plr.note is None:
if row_data.note is None:
log.debug(
f"insert_row({plr=}) with no track_id and no note"
f"insert_row({row_data=}) with no track_id and no note"
)
return
@ -684,16 +687,17 @@ class PlaylistTab(QTableWidget):
continue
self.setItem(row, i, QTableWidgetItem())
self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
notes_item = QTableWidgetItem(plr.note)
notes_item = QTableWidgetItem(row_data.note)
self.setItem(row, HEADER_NOTES_COLUMN, notes_item)
# Save (no) track_id
userdata_item.setData(self.ROW_TRACK_ID, 0)
if repaint:
self.save_playlist(session)
self.update_display(session, clear_selection=False)
def insert_track(self, session: Session, track: Tracks,
def insert_track(self, session: Session, track: Optional[Tracks],
note: str = None, repaint: bool = True) -> None:
"""
Insert track into playlist tab.
@ -705,30 +709,20 @@ class PlaylistTab(QTableWidget):
to do the heavy lifing.
"""
if not track and track.id:
log.debug(
f"insert_track({session=}, {track=}, {note=}, {repaint=}"
" called with either no track or no track.id"
)
return
row_number = self.get_new_row_number()
# Check to see whether track is already in playlist
existing_plr = PlaylistRows.get_track_plr(session, track.id,
self.playlist_id)
if existing_plr and ask_yes_no("Duplicate row",
"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))
# PlaylistRows object requires a row number, but that number
# can be reset by calling PlaylistRows.fixup_rownumbers() later,
# so just fudge a row number for now.
row_number = 0
if track:
track_id = track.id
else:
track_id = None
plr = PlaylistRows(session, self.playlist_id,
track_id, row_number, note)
self.insert_row(session, plr)
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
if repaint:
self.update_display(session, clear_selection=False)
def play_started(self, session: Session) -> None:
"""
@ -777,10 +771,10 @@ class PlaylistTab(QTableWidget):
self._clear_current_track_row()
self.current_track_start_time = None
def populate_display(self, session: Session, playlist_id: int,
def populate(self, session: Session, playlist_id: int,
scroll_to_top: bool = True) -> None:
"""
Populate display from the associated playlist ID
Populate from the associated playlist ID
"""
# Sanity check row numbering before we load
@ -791,8 +785,8 @@ class PlaylistTab(QTableWidget):
# Add the rows
playlist = session.get(Playlists, playlist_id)
for plr in playlist.rows:
self.insert_row(session, plr, repaint=False)
for row in playlist.rows:
self.insert_row(session, row, repaint=False)
# Scroll to top
if scroll_to_top:
@ -828,39 +822,22 @@ class PlaylistTab(QTableWidget):
def save_playlist(self, session: Session) -> None:
"""
Get the PlaylistRow objects for each row in the display. Correct
the row_number and playlist_id if necessary. Remove any row
numbers in the database that are higher than the last row in
the display.
All playlist rows have a PlaylistRows id. Check that that id points
to this playlist (in case track has been moved from other) and that
the row number is correct (in case tracks have been reordered).
"""
# Build a dictionary of
# {display_row_number: display_row_plr_id}
display_plr_ids = {row_number: self._get_playlistrow_id(row_number)
for row_number in range(self.rowCount())}
# Now build a dictionary of
# {display_row_number: display_row_plr}
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
plr_dict = PlaylistRows.indexed_by_row(session, self.playlist_id)
for row in range(self.rowCount()):
row_plr[row].row_number = row
row_plr[row].playlist_id = self.playlist_id
# Set the row number and playlist id (even if correct)
plr_dict[row].row_number = row
plr_dict[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
# Any rows in the database with a row_number higher that the
# current value of 'row' should not be there. Commit session
# first to ensure any changes made above are committed.
session.commit()
PlaylistRows.delete_plrids_not_in_list(session, self.playlist_id,
display_plr_ids.values())
PlaylistRows.delete_higher_rows(session, self.playlist_id, row)
def scroll_current_to_top(self) -> None:
"""Scroll currently-playing row to top"""
@ -883,6 +860,65 @@ class PlaylistTab(QTableWidget):
return
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:
"""
Select next row containg self.search_string.
@ -1308,8 +1344,9 @@ class PlaylistTab(QTableWidget):
Delete mutliple rows
Actions required:
- Delete the rows from the PlaylistRows table
- Correct the row numbers in the PlaylistRows table
- Remove the rows from the display
- Save the playlist
"""
# Delete rows from database
@ -1322,10 +1359,13 @@ class PlaylistTab(QTableWidget):
f"Really delete {row_count} row{plural}?"):
return
self.remove_selected_rows()
with Session() as session:
QTimer.singleShot(0, lambda: self.save_playlist(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()
def _drop_on(self, event):
"""
@ -1505,6 +1545,11 @@ class PlaylistTab(QTableWidget):
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:
"""Display popup with info re row"""
@ -1613,20 +1658,6 @@ class PlaylistTab(QTableWidget):
new_metadata = self._meta_get(row) | (1 << attribute)
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:
"""Play track with mplayer"""
@ -1738,65 +1769,6 @@ class PlaylistTab(QTableWidget):
scroll_item = self.item(top_row, 0)
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:
"""
Called when item selection changes.