Fix move tracks
This commit is contained in:
parent
f1aba41921
commit
f22f2780a3
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (musicmuster) (2)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (musicmuster)" project-jdk-type="Python SDK" />
|
||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Poetry (musicmuster) (2)" jdkType="Python SDK" />
|
||||
<orderEntry type="jdk" jdkName="Poetry (musicmuster)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
|
||||
@ -338,9 +338,6 @@ class Playlists(Base):
|
||||
|
||||
self.loaded = True
|
||||
self.last_used = datetime.now()
|
||||
if self not in session:
|
||||
session.add(self)
|
||||
session.commit()
|
||||
|
||||
def remove_all_tracks(self, session: Session) -> None:
|
||||
"""
|
||||
|
||||
@ -88,7 +88,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.visible_playlist_tab().insert_track(session, track)
|
||||
|
||||
def set_main_window_size(self) -> None:
|
||||
# TODO: V2 check
|
||||
"""Set size of window from database"""
|
||||
|
||||
with Session() as session:
|
||||
@ -355,7 +354,6 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.end_of_track_actions()
|
||||
|
||||
def insert_note(self):
|
||||
#TODO: V2 check
|
||||
"Add non-track row to playlist"
|
||||
|
||||
dlg = QInputDialog(self)
|
||||
@ -374,6 +372,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
with Session() as session:
|
||||
for playlist in Playlists.get_open(session):
|
||||
self.create_playlist_tab(session, playlist)
|
||||
playlist.mark_open(session)
|
||||
|
||||
def create_playlist_tab(self, session: Session,
|
||||
playlist: Playlists) -> None:
|
||||
@ -383,7 +382,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
"""
|
||||
|
||||
playlist_tab: PlaylistTab = PlaylistTab(
|
||||
parent=self, session=session, playlist=playlist)
|
||||
musicmuster=self, session=session, playlist_id=playlist.id)
|
||||
idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name)
|
||||
self.tabPlaylist.setCurrentIndex(idx)
|
||||
|
||||
@ -391,47 +390,37 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
"""Move selected rows to another playlist"""
|
||||
|
||||
with Session() as session:
|
||||
visible_tab = self.visible_playlist_tab()
|
||||
visible_tab_id = visible_tab.playlist_id
|
||||
|
||||
playlists = [p for p in Playlists.get_all(session)
|
||||
if p.id != self.visible_playlist_tab().id]
|
||||
if p.id != visible_tab_id]
|
||||
dlg = SelectPlaylistDialog(self, playlists=playlists)
|
||||
dlg.exec()
|
||||
if not dlg.plid:
|
||||
return
|
||||
|
||||
# TODO: just update dest playlist and call populate if
|
||||
# visible
|
||||
# Update database for both source and destination playlists
|
||||
rows = visible_tab.get_selected_rows()
|
||||
for row in rows:
|
||||
PlaylistTracks.move_track(
|
||||
session, visible_tab.playlist_id, row, dlg.plid
|
||||
)
|
||||
|
||||
# If destination playlist is visible, we need to add the moved
|
||||
# tracks to it. If not, they will be automatically loaded when
|
||||
# the playlistis opened.
|
||||
# Update destination playlist_tab if visible (if not visible, it
|
||||
# will be re-populated when it is opened)
|
||||
destination_visible_playlist_tab = None
|
||||
for tab in range(self.tabPlaylist.count()):
|
||||
# Non-playlist tabs won't have a 'playlist' attribute
|
||||
if not hasattr(self.tabPlaylist.widget(tab), 'playlist'):
|
||||
# Non-playlist tabs won't have a 'playlist_id' attribute
|
||||
if not hasattr(self.tabPlaylist.widget(tab), 'playlist_id'):
|
||||
continue
|
||||
if self.tabPlaylist.widget(tab).id == dlg.plid:
|
||||
if self.tabPlaylist.widget(tab).playlist_id == dlg.plid:
|
||||
destination_visible_playlist_tab = (
|
||||
self.tabPlaylist.widget(tab))
|
||||
break
|
||||
|
||||
rows = []
|
||||
for (row, track) in (
|
||||
self.visible_playlist_tab().get_selected_rows_and_tracks(
|
||||
session)
|
||||
):
|
||||
rows.append(row)
|
||||
if destination_visible_playlist_tab:
|
||||
# Insert with repaint=False to not update database
|
||||
destination_visible_playlist_tab.insert_track(
|
||||
session, track, repaint=False)
|
||||
|
||||
# Update database for both source and destination playlists
|
||||
PlaylistTracks.move_rows(
|
||||
session, self.visible_playlist_tab().id, rows, dlg.plid)
|
||||
|
||||
# Update destination playlist if visible
|
||||
if destination_visible_playlist_tab:
|
||||
destination_visible_playlist_tab.update_display()
|
||||
if destination_visible_playlist_tab:
|
||||
destination_visible_playlist_tab.populate(session, dlg.plid)
|
||||
|
||||
# Update source playlist
|
||||
self.visible_playlist_tab().remove_rows(rows)
|
||||
@ -577,8 +566,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
dlg = SelectPlaylistDialog(self, playlists=playlists)
|
||||
dlg.exec()
|
||||
if dlg.plid:
|
||||
playlist = Playlists.get_by_id(session, dlg.plid)
|
||||
self.create_playlist_tab(session, playlist)
|
||||
p = Playlists.get_by_id(session=session, playlist_id=dlg.plid)
|
||||
self.create_playlist_tab(session, p)
|
||||
|
||||
def select_next_row(self) -> None:
|
||||
"""Select next or first row in playlist"""
|
||||
|
||||
438
app/playlists.py
438
app/playlists.py
@ -65,13 +65,12 @@ class PlaylistTab(QTableWidget):
|
||||
ROW_METADATA = Qt.UserRole
|
||||
CONTENT_OBJECT = Qt.UserRole + 1
|
||||
|
||||
def __init__(self, parent: QMainWindow, session: Session,
|
||||
playlist: Playlists, *args, **kwargs):
|
||||
def __init__(self, musicmuster: QMainWindow, session: Session,
|
||||
playlist_id: int, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.parent: QMainWindow = parent # The MusicMuster process
|
||||
self.playlist: Playlists = playlist
|
||||
self.playlist.mark_open(session)
|
||||
self.musicmuster: QMainWindow = musicmuster
|
||||
self.playlist_id: int = playlist_id
|
||||
self.menu: Optional[QMenu] = None
|
||||
self.current_track_start_time: Optional[datetime] = None
|
||||
|
||||
@ -140,13 +139,10 @@ class PlaylistTab(QTableWidget):
|
||||
self.cellEditingEnded.connect(self._cell_edit_ended)
|
||||
|
||||
# Now load our tracks and notes
|
||||
self._populate(session, playlist)
|
||||
self.populate(session, self.playlist_id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<PlaylistTab(id={self.playlist.id}, "
|
||||
f"name={self.playlist.name}>"
|
||||
)
|
||||
return (f"<PlaylistTab(id={self.playlist_id}")
|
||||
|
||||
# ########## Events ##########
|
||||
|
||||
@ -285,7 +281,7 @@ class PlaylistTab(QTableWidget):
|
||||
if ok:
|
||||
with Session() as session:
|
||||
note: Notes = Notes(
|
||||
session, self.playlist.id, row, dlg.textValue())
|
||||
session, self.playlist_id, row, dlg.textValue())
|
||||
self._insert_note(session, note, row, True) # checked
|
||||
|
||||
def get_selected_row(self) -> Optional[int]:
|
||||
@ -296,19 +292,11 @@ class PlaylistTab(QTableWidget):
|
||||
else:
|
||||
return self.selectionModel().selectedRows()[0].row()
|
||||
|
||||
def get_selected_rows_and_tracks(self, session: Session) \
|
||||
-> Optional[List[Tuple[int, Tracks]]]:
|
||||
"""Return a list of selected (row-number, track) tuples"""
|
||||
def get_selected_rows(self) -> List[int]:
|
||||
"""Return a list of selected row numbers"""
|
||||
|
||||
if not self.selectionModel().hasSelection():
|
||||
return None
|
||||
|
||||
result = []
|
||||
|
||||
for row in [r.row() for r in self.selectionModel().selectedRows()]:
|
||||
result.append((row, self._get_row_object(row, session)))
|
||||
|
||||
return result
|
||||
rows = self.selectionModel().selectedRows()
|
||||
return [row.row() for row in rows]
|
||||
|
||||
def get_selected_title(self) -> Optional[str]:
|
||||
"""Return title of selected row or None"""
|
||||
@ -448,6 +436,49 @@ class PlaylistTab(QTableWidget):
|
||||
self._clear_current_track_row()
|
||||
self.current_track_start_time = None
|
||||
|
||||
def populate(self, session: Session, playlist_id: int) -> None:
|
||||
"""
|
||||
Populate from the associated playlist ID
|
||||
|
||||
We don't mandate that an item will be on its specified row, only
|
||||
that it will be above larger-numbered row items, and below
|
||||
lower-numbered ones.
|
||||
"""
|
||||
|
||||
data: List[Union[Tuple[List[int], Tracks], Tuple[List[int], Notes]]] \
|
||||
= []
|
||||
item: Union[Notes, Tracks]
|
||||
note: Notes
|
||||
row: int
|
||||
track: Tracks
|
||||
|
||||
playlist = Playlists.get_by_id(session, playlist_id)
|
||||
|
||||
for row, track in playlist.tracks.items():
|
||||
data.append(([row], track))
|
||||
for note in playlist.notes:
|
||||
data.append(([note.row], note))
|
||||
|
||||
# Clear playlist
|
||||
self.setRowCount(0)
|
||||
|
||||
# Now add data in row order
|
||||
for i in sorted(data, key=lambda x: x[0]):
|
||||
item = i[1]
|
||||
if isinstance(item, Tracks):
|
||||
self.insert_track(session, item, repaint=False)
|
||||
elif isinstance(item, Notes):
|
||||
self._insert_note(session, item, repaint=False)
|
||||
|
||||
# Scroll to top
|
||||
scroll_to: QTableWidgetItem = self.item(0, self.COL_TITLE)
|
||||
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop)
|
||||
|
||||
# We possibly don't need to save the playlist here, but row
|
||||
# numbers may have changed during population, and it's cheap to do
|
||||
self.save_playlist(session)
|
||||
self.update_display(session)
|
||||
|
||||
def save_playlist(self, session) -> None:
|
||||
"""
|
||||
Save playlist to database.
|
||||
@ -461,8 +492,7 @@ class PlaylistTab(QTableWidget):
|
||||
changes.
|
||||
"""
|
||||
|
||||
# Ensure we have a valid database class
|
||||
session.add(self.playlist)
|
||||
playlist = Playlists.get_by_id(session, self.playlist_id)
|
||||
|
||||
# Notes first
|
||||
# Create dictionaries indexed by note_id
|
||||
@ -477,7 +507,7 @@ class PlaylistTab(QTableWidget):
|
||||
playlist_notes[note.id] = note
|
||||
|
||||
# Database
|
||||
for note in self.playlist.notes:
|
||||
for note in playlist.notes:
|
||||
database_notes[note.id] = note
|
||||
|
||||
# We don't need to check for notes to add to the database as
|
||||
@ -505,14 +535,14 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
# Tracks
|
||||
# Remove all tracks from this playlist
|
||||
self.playlist.remove_all_tracks(session)
|
||||
playlist.remove_all_tracks(session)
|
||||
# Iterate on-screen playlist and add tracks back in
|
||||
for row in range(self.rowCount()):
|
||||
if row in notes_rows:
|
||||
continue
|
||||
track_id: int = self.item(
|
||||
row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
|
||||
self.playlist.add_track(session, track_id, row)
|
||||
playlist.add_track(session, track_id, row)
|
||||
|
||||
def select_next_row(self) -> None:
|
||||
"""
|
||||
@ -616,17 +646,13 @@ class PlaylistTab(QTableWidget):
|
||||
- Show unplayed tracks in bold
|
||||
"""
|
||||
|
||||
if self.playlist not in session:
|
||||
session.add(self.playlist)
|
||||
DEBUG(f"playlist.update_display [{self.playlist=}]")
|
||||
|
||||
# Clear selection if required
|
||||
if clear_selection:
|
||||
self.clearSelection()
|
||||
|
||||
current_row: Optional[int] = self._get_current_track_row()
|
||||
next_row: Optional[int] = self._get_next_track_row()
|
||||
notes: Optional[List[int]] = self._get_notes_rows()
|
||||
notes: List[int] = self._get_notes_rows()
|
||||
played: Optional[List[int]] = self._get_played_track_rows()
|
||||
unreadable: Optional[List[int]] = self._get_unreadable_track_rows()
|
||||
|
||||
@ -871,7 +897,7 @@ class PlaylistTab(QTableWidget):
|
||||
with Session() as session:
|
||||
self.update_display(session)
|
||||
|
||||
self.parent.enable_play_next_controls()
|
||||
self.musicmuster.enable_play_next_controls()
|
||||
|
||||
def _cell_edit_started(self, row: int, column: int) -> None:
|
||||
"""
|
||||
@ -883,7 +909,28 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
self.editing_cell = True
|
||||
# Disable play controls so that keyboard input doesn't disturb playing
|
||||
self.parent.disable_play_next_controls()
|
||||
self.musicmuster.disable_play_next_controls()
|
||||
|
||||
def _clear_current_track_row(self) -> None:
|
||||
"""
|
||||
Clear current row if there is one.
|
||||
"""
|
||||
|
||||
current_row: Optional[int] = self._get_current_track_row()
|
||||
if current_row is not None:
|
||||
self._meta_clear_attribute(current_row, RowMeta.CURRENT)
|
||||
# Reset row colour
|
||||
if current_row % 2:
|
||||
self._set_row_colour(
|
||||
current_row, QColor(Config.COLOUR_ODD_PLAYLIST))
|
||||
else:
|
||||
self._set_row_colour(
|
||||
current_row, QColor(Config.COLOUR_EVEN_PLAYLIST))
|
||||
|
||||
def _clear_played_row_status(self, row: int) -> None:
|
||||
"""Clear played status on row"""
|
||||
|
||||
self._meta_clear_attribute(row, RowMeta.PLAYED)
|
||||
|
||||
def _delete_rows(self) -> None:
|
||||
"""Delete mutliple rows"""
|
||||
@ -932,6 +979,67 @@ class PlaylistTab(QTableWidget):
|
||||
return (index.row() + 1 if self._is_below(event.pos(), index)
|
||||
else index.row())
|
||||
|
||||
def _edit_cell(self, mi): # review
|
||||
"""Called when table is double-clicked"""
|
||||
|
||||
row = mi.row()
|
||||
column = mi.column()
|
||||
item = self.item(row, column)
|
||||
|
||||
if column in [self.COL_TITLE, self.COL_ARTIST]:
|
||||
self.editItem(item)
|
||||
|
||||
@staticmethod
|
||||
def _file_is_readable(path: str) -> bool:
|
||||
"""
|
||||
Returns True if track path is readable, else False
|
||||
|
||||
vlc cannot read files with a colon in the path
|
||||
"""
|
||||
|
||||
if os.access(path, os.R_OK):
|
||||
if ':' not in path:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _find_next_track_row(self, starting_row: int = None) -> Optional[int]:
|
||||
"""
|
||||
Find next track to play. If a starting row is given, start there;
|
||||
else if there's a track selected, start looking from next track;
|
||||
otherwise, start from top. Skip rows already played.
|
||||
|
||||
If not found, return None.
|
||||
|
||||
If found, return row number.
|
||||
"""
|
||||
|
||||
if starting_row is None:
|
||||
current_row = self._get_current_track_row()
|
||||
if current_row is not None:
|
||||
starting_row = current_row + 1
|
||||
else:
|
||||
starting_row = 0
|
||||
notes_rows = self._get_notes_rows()
|
||||
played_rows = self._get_played_track_rows()
|
||||
for row in range(starting_row, self.rowCount()):
|
||||
if row in notes_rows or row in played_rows:
|
||||
continue
|
||||
else:
|
||||
return row
|
||||
|
||||
return None
|
||||
|
||||
def _get_current_track_row(self) -> Optional[int]:
|
||||
"""Return row marked as current, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.CURRENT)
|
||||
|
||||
def _get_next_track_row(self) -> Optional[int]:
|
||||
"""Return row marked as next, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.NEXT)
|
||||
|
||||
@staticmethod
|
||||
def _get_note_text_time(text: str) -> Optional[datetime]:
|
||||
"""Return time specified at the end of text"""
|
||||
@ -945,6 +1053,11 @@ class PlaylistTab(QTableWidget):
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _get_notes_rows(self) -> List[int]:
|
||||
"""Return rows marked as notes, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.NOTE, one=False)
|
||||
|
||||
def _get_row_end_time(self, row) -> Optional[datetime]:
|
||||
"""
|
||||
Return row end time as string
|
||||
@ -961,14 +1074,6 @@ class PlaylistTab(QTableWidget):
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _get_row_track_object(self, row: int, session: Session) \
|
||||
-> Optional[Tracks]:
|
||||
"""Return track associated with this row"""
|
||||
|
||||
track_id = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
|
||||
track = Tracks.get_by_id(session, track_id)
|
||||
return track
|
||||
|
||||
def _get_row_notes_object(self, row: int, session: Session) \
|
||||
-> Optional[Notes]:
|
||||
"""Return note associated with this row"""
|
||||
@ -977,6 +1082,11 @@ class PlaylistTab(QTableWidget):
|
||||
note = Notes.get_by_id(session, note_id)
|
||||
return note
|
||||
|
||||
def _get_played_track_rows(self) -> Optional[List[int]]:
|
||||
"""Return rows marked as played, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.PLAYED, one=False)
|
||||
|
||||
def _get_row_start_time(self, row: int) -> Optional[datetime]:
|
||||
try:
|
||||
if self.item(row, self.COL_START_TIME):
|
||||
@ -989,6 +1099,24 @@ class PlaylistTab(QTableWidget):
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _get_row_track_object(self, row: int, session: Session) \
|
||||
-> Optional[Tracks]:
|
||||
"""Return track associated with this row"""
|
||||
|
||||
track_id = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
|
||||
track = Tracks.get_by_id(session, track_id)
|
||||
return track
|
||||
|
||||
def _get_track_rows(self) -> Optional[List[int]]:
|
||||
"""Return rows marked as tracks, or None"""
|
||||
|
||||
return self._meta_notset(RowMeta.NOTE)
|
||||
|
||||
def _get_unreadable_track_rows(self) -> Optional[List[int]]:
|
||||
"""Return rows marked as unreadable, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.UNREADABLE, one=False)
|
||||
|
||||
def _info_row(self, row: int) -> None:
|
||||
"""Display popup with info re row"""
|
||||
|
||||
@ -1071,43 +1199,6 @@ class PlaylistTab(QTableWidget):
|
||||
and pos.y() >= rect.center().y() # noqa W503
|
||||
)
|
||||
|
||||
def _edit_cell(self, mi): # review
|
||||
"""Called when table is double-clicked"""
|
||||
|
||||
row = mi.row()
|
||||
column = mi.column()
|
||||
item = self.item(row, column)
|
||||
|
||||
if column in [self.COL_TITLE, self.COL_ARTIST]:
|
||||
self.editItem(item)
|
||||
|
||||
def _find_next_track_row(self, starting_row: int = None) -> Optional[int]:
|
||||
"""
|
||||
Find next track to play. If a starting row is given, start there;
|
||||
else if there's a track selected, start looking from next track;
|
||||
otherwise, start from top. Skip rows already played.
|
||||
|
||||
If not found, return None.
|
||||
|
||||
If found, return row number.
|
||||
"""
|
||||
|
||||
if starting_row is None:
|
||||
current_row = self._get_current_track_row()
|
||||
if current_row is not None:
|
||||
starting_row = current_row + 1
|
||||
else:
|
||||
starting_row = 0
|
||||
notes_rows = self._get_notes_rows()
|
||||
played_rows = self._get_played_track_rows()
|
||||
for row in range(starting_row, self.rowCount()):
|
||||
if row in notes_rows or row in played_rows:
|
||||
continue
|
||||
else:
|
||||
return row
|
||||
|
||||
return None
|
||||
|
||||
def _meta_clear_attribute(self, row: int, attribute: int) -> None:
|
||||
"""Clear given metadata for row"""
|
||||
|
||||
@ -1118,22 +1209,6 @@ class PlaylistTab(QTableWidget):
|
||||
self.item(row, self.COL_USERDATA).setData(
|
||||
self.ROW_METADATA, new_metadata)
|
||||
|
||||
def _clear_current_track_row(self) -> None:
|
||||
"""
|
||||
Clear current row if there is one.
|
||||
"""
|
||||
|
||||
current_row: Optional[int] = self._get_current_track_row()
|
||||
if current_row is not None:
|
||||
self._meta_clear_attribute(current_row, RowMeta.CURRENT)
|
||||
# Reset row colour
|
||||
if current_row % 2:
|
||||
self._set_row_colour(
|
||||
current_row, QColor(Config.COLOUR_ODD_PLAYLIST))
|
||||
else:
|
||||
self._set_row_colour(
|
||||
current_row, QColor(Config.COLOUR_EVEN_PLAYLIST))
|
||||
|
||||
def _meta_clear_next(self) -> None:
|
||||
"""
|
||||
Clear next row if there is one.
|
||||
@ -1143,46 +1218,11 @@ class PlaylistTab(QTableWidget):
|
||||
if next_row is not None:
|
||||
self._meta_clear_attribute(next_row, RowMeta.NEXT)
|
||||
|
||||
def _clear_played_row_status(self, row: int) -> None:
|
||||
"""Clear played status on row"""
|
||||
|
||||
self._meta_clear_attribute(row, RowMeta.PLAYED)
|
||||
|
||||
def _meta_get(self, row: int) -> Optional[int]:
|
||||
"""Return row metadata"""
|
||||
|
||||
return self.item(row, self.COL_USERDATA).data(self.ROW_METADATA)
|
||||
|
||||
def _get_current_track_row(self) -> Optional[int]:
|
||||
"""Return row marked as current, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.CURRENT)
|
||||
|
||||
def _get_next_track_row(self) -> Optional[int]:
|
||||
"""Return row marked as next, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.NEXT)
|
||||
|
||||
def _get_notes_rows(self) -> Optional[List[int]]:
|
||||
"""Return rows marked as notes, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.NOTE, one=False)
|
||||
|
||||
def _get_track_rows(self) -> Optional[List[int]]:
|
||||
"""Return rows marked as tracks, or None"""
|
||||
|
||||
return self._meta_notset(RowMeta.NOTE)
|
||||
|
||||
def _get_played_track_rows(self) -> Optional[List[int]]:
|
||||
"""Return rows marked as played, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.PLAYED, one=False)
|
||||
|
||||
def _get_unreadable_track_rows(self) -> Optional[List[int]]:
|
||||
"""Return rows marked as unreadable, or None"""
|
||||
|
||||
return self._meta_search(RowMeta.UNREADABLE, one=False)
|
||||
|
||||
def _meta_notset(self, metadata: int) -> Union[List[int]]:
|
||||
"""
|
||||
Search rows for metadata not set.
|
||||
@ -1243,6 +1283,21 @@ class PlaylistTab(QTableWidget):
|
||||
self.item(row, self.COL_USERDATA).setData(
|
||||
self.ROW_METADATA, new_metadata)
|
||||
|
||||
def _rescan(self, row: int) -> None:
|
||||
"""
|
||||
If passed row is track row, rescan it.
|
||||
Otherwise, return None.
|
||||
"""
|
||||
|
||||
DEBUG(f"_rescan({row=})")
|
||||
|
||||
with Session() as session:
|
||||
for row in self._get_track_rows():
|
||||
track: Tracks = self._get_row_track_object(row, session)
|
||||
if track:
|
||||
track.rescan(session)
|
||||
self._update_row(session, row, track)
|
||||
|
||||
def _set_current_track_row(self, row: int) -> None:
|
||||
"""Mark this row as current track"""
|
||||
|
||||
@ -1270,65 +1325,6 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
self._meta_set_attribute(row, RowMeta.UNREADABLE)
|
||||
|
||||
def _populate(self, session: Session, playlist: Playlists) -> None:
|
||||
"""
|
||||
Populate from the associated playlist object
|
||||
|
||||
We don't mandate that an item will be on its specified row, only
|
||||
that it will be above larger-numbered row items, and below
|
||||
lower-numbered ones.
|
||||
"""
|
||||
|
||||
data: List[Union[Tuple[List[int], Tracks], Tuple[List[int], Notes]]] \
|
||||
= []
|
||||
item: Union[Notes, Tracks]
|
||||
note: Notes
|
||||
row: int
|
||||
track: Tracks
|
||||
|
||||
if playlist not in session:
|
||||
session.add(playlist)
|
||||
|
||||
for row, track in self.playlist.tracks.items():
|
||||
data.append(([row], track))
|
||||
for note in self.playlist.notes:
|
||||
data.append(([note.row], note))
|
||||
|
||||
# Clear playlist
|
||||
self.setRowCount(0)
|
||||
|
||||
# Now add data in row order
|
||||
for i in sorted(data, key=lambda x: x[0]):
|
||||
item = i[1]
|
||||
if isinstance(item, Tracks):
|
||||
self.insert_track(session, item, repaint=False)
|
||||
elif isinstance(item, Notes):
|
||||
self._insert_note(session, item, repaint=False)
|
||||
|
||||
# Scroll to top
|
||||
scroll_to: QTableWidgetItem = self.item(0, self.COL_TITLE)
|
||||
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop)
|
||||
|
||||
# We possibly don't need to save the playlist here, but row
|
||||
# numbers may have changed during population, and it's cheap to do
|
||||
self.save_playlist(session)
|
||||
self.update_display(session)
|
||||
|
||||
def _rescan(self, row: int) -> None:
|
||||
"""
|
||||
If passed row is track row, rescan it.
|
||||
Otherwise, return None.
|
||||
"""
|
||||
|
||||
DEBUG(f"_rescan({row=})")
|
||||
|
||||
with Session() as session:
|
||||
for row in self._get_track_rows():
|
||||
track: Tracks = self._get_row_track_object(row, session)
|
||||
if track:
|
||||
track.rescan(session)
|
||||
self._update_row(session, row, track)
|
||||
|
||||
def _select_event(self) -> None:
|
||||
"""
|
||||
Called when item selection changes.
|
||||
@ -1346,10 +1342,31 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
# Only paint message if there are selected track rows
|
||||
if ms > 0:
|
||||
self.parent.lblSumPlaytime.setText(
|
||||
self.musicmuster.lblSumPlaytime.setText(
|
||||
f"Selected duration: {helpers.ms_to_mmss(ms)}")
|
||||
else:
|
||||
self.parent.lblSumPlaytime.setText("")
|
||||
self.musicmuster.lblSumPlaytime.setText("")
|
||||
|
||||
def _select_tracks(self, played: bool) -> None:
|
||||
"""
|
||||
Select all played (played=True) or unplayed (played=False)
|
||||
tracks in playlist
|
||||
"""
|
||||
|
||||
# Need to allow multiple rows to be selected
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
|
||||
notes_rows: List[int] = self._get_notes_rows()
|
||||
self.clearSelection()
|
||||
|
||||
played_rows: List[int] = self._get_played_track_rows()
|
||||
for row in range(self.rowCount()):
|
||||
if row in notes_rows:
|
||||
continue
|
||||
if row in played_rows == played:
|
||||
self.selectRow(row)
|
||||
|
||||
# Reset extended selection
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
|
||||
def _set_column_widths(self) -> None:
|
||||
"""Column widths from settings"""
|
||||
@ -1394,7 +1411,7 @@ class PlaylistTab(QTableWidget):
|
||||
self._set_next_track_row(row)
|
||||
|
||||
# Notify musicmuster
|
||||
self.parent.this_is_the_next_track(self, track)
|
||||
self.musicmuster.this_is_the_next_track(self, track)
|
||||
|
||||
# Update display
|
||||
self.update_display(session)
|
||||
@ -1453,41 +1470,6 @@ class PlaylistTab(QTableWidget):
|
||||
item: QTableWidgetItem = QTableWidgetItem(time_str)
|
||||
self.setItem(row, self.COL_START_TIME, item)
|
||||
|
||||
def _select_tracks(self, played: bool) -> None:
|
||||
"""
|
||||
Select all played (played=True) or unplayed (played=False)
|
||||
tracks in playlist
|
||||
"""
|
||||
|
||||
# Need to allow multiple rows to be selected
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
|
||||
notes_rows: List[int] = self._get_notes_rows()
|
||||
self.clearSelection()
|
||||
|
||||
played_rows: List[int] = self._get_played_track_rows()
|
||||
for row in range(self.rowCount()):
|
||||
if row in notes_rows:
|
||||
continue
|
||||
if row in played_rows == played:
|
||||
self.selectRow(row)
|
||||
|
||||
# Reset extended selection
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
|
||||
@staticmethod
|
||||
def _file_is_readable(path: str) -> bool:
|
||||
"""
|
||||
Returns True if track path is readable, else False
|
||||
|
||||
vlc cannot read files with a colon in the path
|
||||
"""
|
||||
|
||||
if os.access(path, os.R_OK):
|
||||
if ':' not in path:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _update_row(self, session, row: int, track: Tracks) -> None:
|
||||
"""
|
||||
Update the passed row with info from the passed track.
|
||||
|
||||
27
poetry.lock
generated
27
poetry.lock
generated
@ -217,7 +217,7 @@ python-versions = ">=3.5, <4"
|
||||
name = "mypy"
|
||||
version = "0.931"
|
||||
description = "Optional static typing for Python"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
@ -234,7 +234,7 @@ python2 = ["typed-ast (>=1.4.0,<2)"]
|
||||
name = "mypy-extensions"
|
||||
version = "0.4.3"
|
||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
@ -404,6 +404,17 @@ category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "pyqt5-stubs"
|
||||
version = "5.15.2.0"
|
||||
description = "PEP561 stub files for the PyQt5 framework"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">= 3.5"
|
||||
|
||||
[package.extras]
|
||||
build = ["docker (==4.2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyqtwebengine"
|
||||
version = "5.15.5"
|
||||
@ -513,7 +524,7 @@ sqlcipher = ["sqlcipher3-binary"]
|
||||
name = "sqlalchemy-stubs"
|
||||
version = "0.4"
|
||||
description = "SQLAlchemy stubs and mypy plugin"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
@ -560,7 +571,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
@ -579,7 +590,7 @@ test = ["pytest"]
|
||||
name = "typing-extensions"
|
||||
version = "4.1.1"
|
||||
description = "Backported and Experimental Type Hints for Python 3.6+"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
@ -594,7 +605,7 @@ python-versions = "*"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "6ea248abf2050b4d2fb08baaf78fee1b358e43109bb1bbf202a4d3e78a35ec53"
|
||||
content-hash = "c9f7a8c9a15812c1f3affeaa301fe6e6310388cca41a229bdb97fdd58c765e6f"
|
||||
|
||||
[metadata.files]
|
||||
alembic = [
|
||||
@ -910,6 +921,10 @@ pyqt5-sip = [
|
||||
{file = "PyQt5_sip-12.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:989d51c41456cc496cb96f0b341464932b957040d26561f0bb4cf5a0914d6b36"},
|
||||
{file = "PyQt5_sip-12.9.1.tar.gz", hash = "sha256:2f24f299b44c511c23796aafbbb581bfdebf78d0905657b7cee2141b4982030e"},
|
||||
]
|
||||
pyqt5-stubs = [
|
||||
{file = "PyQt5-stubs-5.15.2.0.tar.gz", hash = "sha256:dc0dea66f02fe297fb0cddd5767fbf58275b54ecb67f69ebeb994e1553bfb9b2"},
|
||||
{file = "PyQt5_stubs-5.15.2.0-py3-none-any.whl", hash = "sha256:4b750d04ffca1bb188615d1a4e7d655a1d81d30df6ee3488f0adfe09b78e3d36"},
|
||||
]
|
||||
pyqtwebengine = [
|
||||
{file = "PyQtWebEngine-5.15.5-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:5c77f71d88d871bc7400c68ef6433fadc5bd57b86d1a9c4d8094cea42f3607f1"},
|
||||
{file = "PyQtWebEngine-5.15.5-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:782aeee6bc8699bc029fe5c169a045c2bc9533d781cf3f5e9fb424b85a204e68"},
|
||||
|
||||
@ -17,15 +17,14 @@ psutil = "^5.9.0"
|
||||
PyQtWebEngine = "^5.15.5"
|
||||
pydub = "^0.25.1"
|
||||
PyQt5-sip = "^12.9.1"
|
||||
mypy = "^0.931"
|
||||
sqlalchemy-stubs = "^0.4"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
mypy = "^0.931"
|
||||
pytest = "^7.0.0"
|
||||
ipdb = "^0.13.9"
|
||||
sqlalchemy-stubs = "^0.4"
|
||||
pytest-qt = "^4.0.2"
|
||||
sqlalchemy-stubs = "^0.4"
|
||||
PyQt5-stubs = "^5.15.2"
|
||||
mypy = "^0.931"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user