Compare commits
6 Commits
be4f19757c
...
f2a27366d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2a27366d3 | ||
|
|
46f2b662f3 | ||
|
|
647e7d478a | ||
|
|
444c3e4fb4 | ||
|
|
35b101a538 | ||
|
|
d3958db8a3 |
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
316
app/playlists.py
316
app/playlists.py
@ -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.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user