Compare commits

..

No commits in common. "f22f2780a301093e2c79b6a6e3e228d644c6d4c4" and "a2fb6baba88b2347fb4aae57520f91cf8da02f4a" have entirely different histories.

7 changed files with 287 additions and 381 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (musicmuster)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Poetry (musicmuster) (2)" project-jdk-type="Python SDK" />
<component name="PythonCompatibilityInspectionAdvertiser"> <component name="PythonCompatibilityInspectionAdvertiser">
<option name="version" value="3" /> <option name="version" value="3" />
</component> </component>

View File

@ -4,7 +4,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" />
</content> </content>
<orderEntry type="jdk" jdkName="Poetry (musicmuster)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Poetry (musicmuster) (2)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="PyDocumentationSettings"> <component name="PyDocumentationSettings">

View File

@ -338,6 +338,9 @@ class Playlists(Base):
self.loaded = True self.loaded = True
self.last_used = datetime.now() self.last_used = datetime.now()
if self not in session:
session.add(self)
session.commit()
def remove_all_tracks(self, session: Session) -> None: def remove_all_tracks(self, session: Session) -> None:
""" """

View File

@ -88,6 +88,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.visible_playlist_tab().insert_track(session, track) self.visible_playlist_tab().insert_track(session, track)
def set_main_window_size(self) -> None: def set_main_window_size(self) -> None:
# TODO: V2 check
"""Set size of window from database""" """Set size of window from database"""
with Session() as session: with Session() as session:
@ -354,6 +355,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.end_of_track_actions() self.end_of_track_actions()
def insert_note(self): def insert_note(self):
#TODO: V2 check
"Add non-track row to playlist" "Add non-track row to playlist"
dlg = QInputDialog(self) dlg = QInputDialog(self)
@ -372,7 +374,6 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
for playlist in Playlists.get_open(session): for playlist in Playlists.get_open(session):
self.create_playlist_tab(session, playlist) self.create_playlist_tab(session, playlist)
playlist.mark_open(session)
def create_playlist_tab(self, session: Session, def create_playlist_tab(self, session: Session,
playlist: Playlists) -> None: playlist: Playlists) -> None:
@ -382,7 +383,7 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
playlist_tab: PlaylistTab = PlaylistTab( playlist_tab: PlaylistTab = PlaylistTab(
musicmuster=self, session=session, playlist_id=playlist.id) parent=self, session=session, playlist=playlist)
idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name) idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx) self.tabPlaylist.setCurrentIndex(idx)
@ -390,37 +391,47 @@ class Window(QMainWindow, Ui_MainWindow):
"""Move selected rows to another playlist""" """Move selected rows to another playlist"""
with Session() as session: 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) playlists = [p for p in Playlists.get_all(session)
if p.id != visible_tab_id] if p.id != self.visible_playlist_tab().id]
dlg = SelectPlaylistDialog(self, playlists=playlists) dlg = SelectPlaylistDialog(self, playlists=playlists)
dlg.exec() dlg.exec()
if not dlg.plid: if not dlg.plid:
return return
# Update database for both source and destination playlists # TODO: just update dest playlist and call populate if
rows = visible_tab.get_selected_rows() # visible
for row in rows:
PlaylistTracks.move_track(
session, visible_tab.playlist_id, row, dlg.plid
)
# Update destination playlist_tab if visible (if not visible, it # If destination playlist is visible, we need to add the moved
# will be re-populated when it is opened) # tracks to it. If not, they will be automatically loaded when
# the playlistis opened.
destination_visible_playlist_tab = None destination_visible_playlist_tab = None
for tab in range(self.tabPlaylist.count()): for tab in range(self.tabPlaylist.count()):
# Non-playlist tabs won't have a 'playlist_id' attribute # Non-playlist tabs won't have a 'playlist' attribute
if not hasattr(self.tabPlaylist.widget(tab), 'playlist_id'): if not hasattr(self.tabPlaylist.widget(tab), 'playlist'):
continue continue
if self.tabPlaylist.widget(tab).playlist_id == dlg.plid: if self.tabPlaylist.widget(tab).id == dlg.plid:
destination_visible_playlist_tab = ( destination_visible_playlist_tab = (
self.tabPlaylist.widget(tab)) self.tabPlaylist.widget(tab))
break break
rows = []
for (row, track) in (
self.visible_playlist_tab().get_selected_rows_and_tracks(
session)
):
rows.append(row)
if destination_visible_playlist_tab: if destination_visible_playlist_tab:
destination_visible_playlist_tab.populate(session, dlg.plid) # 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()
# Update source playlist # Update source playlist
self.visible_playlist_tab().remove_rows(rows) self.visible_playlist_tab().remove_rows(rows)
@ -566,8 +577,8 @@ class Window(QMainWindow, Ui_MainWindow):
dlg = SelectPlaylistDialog(self, playlists=playlists) dlg = SelectPlaylistDialog(self, playlists=playlists)
dlg.exec() dlg.exec()
if dlg.plid: if dlg.plid:
p = Playlists.get_by_id(session=session, playlist_id=dlg.plid) playlist = Playlists.get_by_id(session, dlg.plid)
self.create_playlist_tab(session, p) self.create_playlist_tab(session, playlist)
def select_next_row(self) -> None: def select_next_row(self) -> None:
"""Select next or first row in playlist""" """Select next or first row in playlist"""

View File

@ -65,12 +65,13 @@ class PlaylistTab(QTableWidget):
ROW_METADATA = Qt.UserRole ROW_METADATA = Qt.UserRole
CONTENT_OBJECT = Qt.UserRole + 1 CONTENT_OBJECT = Qt.UserRole + 1
def __init__(self, musicmuster: QMainWindow, session: Session, def __init__(self, parent: QMainWindow, session: Session,
playlist_id: int, *args, **kwargs): playlist: Playlists, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.musicmuster: QMainWindow = musicmuster self.parent: QMainWindow = parent # The MusicMuster process
self.playlist_id: int = playlist_id self.playlist: Playlists = playlist
self.playlist.mark_open(session)
self.menu: Optional[QMenu] = None self.menu: Optional[QMenu] = None
self.current_track_start_time: Optional[datetime] = None self.current_track_start_time: Optional[datetime] = None
@ -139,10 +140,13 @@ class PlaylistTab(QTableWidget):
self.cellEditingEnded.connect(self._cell_edit_ended) self.cellEditingEnded.connect(self._cell_edit_ended)
# Now load our tracks and notes # Now load our tracks and notes
self.populate(session, self.playlist_id) self._populate(session, playlist)
def __repr__(self) -> str: def __repr__(self) -> str:
return (f"<PlaylistTab(id={self.playlist_id}") return (
f"<PlaylistTab(id={self.playlist.id}, "
f"name={self.playlist.name}>"
)
# ########## Events ########## # ########## Events ##########
@ -281,7 +285,7 @@ class PlaylistTab(QTableWidget):
if ok: if ok:
with Session() as session: with Session() as session:
note: Notes = Notes( 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 self._insert_note(session, note, row, True) # checked
def get_selected_row(self) -> Optional[int]: def get_selected_row(self) -> Optional[int]:
@ -292,11 +296,19 @@ class PlaylistTab(QTableWidget):
else: else:
return self.selectionModel().selectedRows()[0].row() return self.selectionModel().selectedRows()[0].row()
def get_selected_rows(self) -> List[int]: def get_selected_rows_and_tracks(self, session: Session) \
"""Return a list of selected row numbers""" -> Optional[List[Tuple[int, Tracks]]]:
"""Return a list of selected (row-number, track) tuples"""
rows = self.selectionModel().selectedRows() if not self.selectionModel().hasSelection():
return [row.row() for row in rows] 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
def get_selected_title(self) -> Optional[str]: def get_selected_title(self) -> Optional[str]:
"""Return title of selected row or None""" """Return title of selected row or None"""
@ -436,49 +448,6 @@ 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) -> 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: def save_playlist(self, session) -> None:
""" """
Save playlist to database. Save playlist to database.
@ -492,7 +461,8 @@ class PlaylistTab(QTableWidget):
changes. changes.
""" """
playlist = Playlists.get_by_id(session, self.playlist_id) # Ensure we have a valid database class
session.add(self.playlist)
# Notes first # Notes first
# Create dictionaries indexed by note_id # Create dictionaries indexed by note_id
@ -507,7 +477,7 @@ class PlaylistTab(QTableWidget):
playlist_notes[note.id] = note playlist_notes[note.id] = note
# Database # Database
for note in playlist.notes: for note in self.playlist.notes:
database_notes[note.id] = note database_notes[note.id] = note
# We don't need to check for notes to add to the database as # We don't need to check for notes to add to the database as
@ -535,14 +505,14 @@ class PlaylistTab(QTableWidget):
# Tracks # Tracks
# Remove all tracks from this playlist # Remove all tracks from this playlist
playlist.remove_all_tracks(session) self.playlist.remove_all_tracks(session)
# Iterate on-screen playlist and add tracks back in # Iterate on-screen playlist and add tracks back in
for row in range(self.rowCount()): for row in range(self.rowCount()):
if row in notes_rows: if row in notes_rows:
continue continue
track_id: int = self.item( track_id: int = self.item(
row, self.COL_USERDATA).data(self.CONTENT_OBJECT) row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
playlist.add_track(session, track_id, row) self.playlist.add_track(session, track_id, row)
def select_next_row(self) -> None: def select_next_row(self) -> None:
""" """
@ -646,13 +616,17 @@ class PlaylistTab(QTableWidget):
- Show unplayed tracks in bold - 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 # Clear selection if required
if clear_selection: if clear_selection:
self.clearSelection() self.clearSelection()
current_row: Optional[int] = self._get_current_track_row() current_row: Optional[int] = self._get_current_track_row()
next_row: Optional[int] = self._get_next_track_row() next_row: Optional[int] = self._get_next_track_row()
notes: List[int] = self._get_notes_rows() notes: Optional[List[int]] = self._get_notes_rows()
played: Optional[List[int]] = self._get_played_track_rows() played: Optional[List[int]] = self._get_played_track_rows()
unreadable: Optional[List[int]] = self._get_unreadable_track_rows() unreadable: Optional[List[int]] = self._get_unreadable_track_rows()
@ -897,7 +871,7 @@ class PlaylistTab(QTableWidget):
with Session() as session: with Session() as session:
self.update_display(session) self.update_display(session)
self.musicmuster.enable_play_next_controls() self.parent.enable_play_next_controls()
def _cell_edit_started(self, row: int, column: int) -> None: def _cell_edit_started(self, row: int, column: int) -> None:
""" """
@ -909,28 +883,7 @@ class PlaylistTab(QTableWidget):
self.editing_cell = True self.editing_cell = True
# Disable play controls so that keyboard input doesn't disturb playing # Disable play controls so that keyboard input doesn't disturb playing
self.musicmuster.disable_play_next_controls() self.parent.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: def _delete_rows(self) -> None:
"""Delete mutliple rows""" """Delete mutliple rows"""
@ -979,67 +932,6 @@ class PlaylistTab(QTableWidget):
return (index.row() + 1 if self._is_below(event.pos(), index) return (index.row() + 1 if self._is_below(event.pos(), index)
else index.row()) 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 @staticmethod
def _get_note_text_time(text: str) -> Optional[datetime]: def _get_note_text_time(text: str) -> Optional[datetime]:
"""Return time specified at the end of text""" """Return time specified at the end of text"""
@ -1053,11 +945,6 @@ class PlaylistTab(QTableWidget):
except ValueError: except ValueError:
return None 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]: def _get_row_end_time(self, row) -> Optional[datetime]:
""" """
Return row end time as string Return row end time as string
@ -1074,6 +961,14 @@ class PlaylistTab(QTableWidget):
except ValueError: except ValueError:
return None 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) \ def _get_row_notes_object(self, row: int, session: Session) \
-> Optional[Notes]: -> Optional[Notes]:
"""Return note associated with this row""" """Return note associated with this row"""
@ -1082,11 +977,6 @@ class PlaylistTab(QTableWidget):
note = Notes.get_by_id(session, note_id) note = Notes.get_by_id(session, note_id)
return note 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]: def _get_row_start_time(self, row: int) -> Optional[datetime]:
try: try:
if self.item(row, self.COL_START_TIME): if self.item(row, self.COL_START_TIME):
@ -1099,24 +989,6 @@ class PlaylistTab(QTableWidget):
except ValueError: except ValueError:
return None 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: def _info_row(self, row: int) -> None:
"""Display popup with info re row""" """Display popup with info re row"""
@ -1199,6 +1071,43 @@ class PlaylistTab(QTableWidget):
and pos.y() >= rect.center().y() # noqa W503 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: def _meta_clear_attribute(self, row: int, attribute: int) -> None:
"""Clear given metadata for row""" """Clear given metadata for row"""
@ -1209,6 +1118,22 @@ class PlaylistTab(QTableWidget):
self.item(row, self.COL_USERDATA).setData( self.item(row, self.COL_USERDATA).setData(
self.ROW_METADATA, new_metadata) 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: def _meta_clear_next(self) -> None:
""" """
Clear next row if there is one. Clear next row if there is one.
@ -1218,11 +1143,46 @@ class PlaylistTab(QTableWidget):
if next_row is not None: if next_row is not None:
self._meta_clear_attribute(next_row, RowMeta.NEXT) 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]: def _meta_get(self, row: int) -> Optional[int]:
"""Return row metadata""" """Return row metadata"""
return self.item(row, self.COL_USERDATA).data(self.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]]: def _meta_notset(self, metadata: int) -> Union[List[int]]:
""" """
Search rows for metadata not set. Search rows for metadata not set.
@ -1283,21 +1243,6 @@ class PlaylistTab(QTableWidget):
self.item(row, self.COL_USERDATA).setData( self.item(row, self.COL_USERDATA).setData(
self.ROW_METADATA, new_metadata) 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: def _set_current_track_row(self, row: int) -> None:
"""Mark this row as current track""" """Mark this row as current track"""
@ -1325,6 +1270,65 @@ class PlaylistTab(QTableWidget):
self._meta_set_attribute(row, RowMeta.UNREADABLE) 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: def _select_event(self) -> None:
""" """
Called when item selection changes. Called when item selection changes.
@ -1342,31 +1346,10 @@ class PlaylistTab(QTableWidget):
# Only paint message if there are selected track rows # Only paint message if there are selected track rows
if ms > 0: if ms > 0:
self.musicmuster.lblSumPlaytime.setText( self.parent.lblSumPlaytime.setText(
f"Selected duration: {helpers.ms_to_mmss(ms)}") f"Selected duration: {helpers.ms_to_mmss(ms)}")
else: else:
self.musicmuster.lblSumPlaytime.setText("") self.parent.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: def _set_column_widths(self) -> None:
"""Column widths from settings""" """Column widths from settings"""
@ -1411,7 +1394,7 @@ class PlaylistTab(QTableWidget):
self._set_next_track_row(row) self._set_next_track_row(row)
# Notify musicmuster # Notify musicmuster
self.musicmuster.this_is_the_next_track(self, track) self.parent.this_is_the_next_track(self, track)
# Update display # Update display
self.update_display(session) self.update_display(session)
@ -1470,6 +1453,41 @@ class PlaylistTab(QTableWidget):
item: QTableWidgetItem = QTableWidgetItem(time_str) item: QTableWidgetItem = QTableWidgetItem(time_str)
self.setItem(row, self.COL_START_TIME, item) 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: def _update_row(self, session, row: int, track: Tracks) -> None:
""" """
Update the passed row with info from the passed track. Update the passed row with info from the passed track.

163
poetry.lock generated
View File

@ -35,28 +35,6 @@ six = "*"
[package.extras] [package.extras]
test = ["astroid", "pytest"] test = ["astroid", "pytest"]
[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "21.4.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
[[package]] [[package]]
name = "backcall" name = "backcall"
version = "0.2.0" version = "0.2.0"
@ -83,7 +61,7 @@ python-versions = ">=3.5"
[[package]] [[package]]
name = "executing" name = "executing"
version = "0.8.3" version = "0.8.2"
description = "Get the currently executing AST node of a frame, and other information" description = "Get the currently executing AST node of a frame, and other information"
category = "dev" category = "dev"
optional = false optional = false
@ -100,14 +78,6 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
[package.extras] [package.extras]
docs = ["sphinx"] docs = ["sphinx"]
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "ipdb" name = "ipdb"
version = "0.13.9" version = "0.13.9"
@ -217,7 +187,7 @@ python-versions = ">=3.5, <4"
name = "mypy" name = "mypy"
version = "0.931" version = "0.931"
description = "Optional static typing for Python" description = "Optional static typing for Python"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@ -234,7 +204,7 @@ python2 = ["typed-ast (>=1.4.0,<2)"]
name = "mypy-extensions" name = "mypy-extensions"
version = "0.4.3" version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker." description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
@ -246,17 +216,6 @@ category = "main"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]] [[package]]
name = "parso" name = "parso"
version = "0.8.3" version = "0.8.3"
@ -288,18 +247,6 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "prompt-toolkit" name = "prompt-toolkit"
version = "3.0.28" version = "3.0.28"
@ -341,14 +288,6 @@ python-versions = "*"
[package.extras] [package.extras]
tests = ["pytest"] tests = ["pytest"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "pydub" name = "pydub"
version = "0.25.1" version = "0.25.1"
@ -365,17 +304,6 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "pyparsing"
version = "3.0.7"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]] [[package]]
name = "pyqt5" name = "pyqt5"
version = "5.15.6" version = "5.15.6"
@ -404,17 +332,6 @@ category = "main"
optional = false optional = false
python-versions = ">=3.5" 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]] [[package]]
name = "pyqtwebengine" name = "pyqtwebengine"
version = "5.15.5" version = "5.15.5"
@ -474,7 +391,7 @@ doc = ["sphinx", "sphinx-rtd-theme"]
[[package]] [[package]]
name = "python-vlc" name = "python-vlc"
version = "3.0.16120" version = "3.0.12118"
description = "VLC bindings for python." description = "VLC bindings for python."
category = "main" category = "main"
optional = false optional = false
@ -520,18 +437,6 @@ postgresql_psycopg2cffi = ["psycopg2cffi"]
pymysql = ["pymysql (<1)", "pymysql"] pymysql = ["pymysql (<1)", "pymysql"]
sqlcipher = ["sqlcipher3-binary"] sqlcipher = ["sqlcipher3-binary"]
[[package]]
name = "sqlalchemy-stubs"
version = "0.4"
description = "SQLAlchemy stubs and mypy plugin"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
mypy = ">=0.790"
typing-extensions = ">=3.7.4"
[[package]] [[package]]
name = "stack-data" name = "stack-data"
version = "0.2.0" version = "0.2.0"
@ -571,7 +476,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
@ -588,9 +493,9 @@ test = ["pytest"]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.1.1" version = "4.0.1"
description = "Backported and Experimental Type Hints for Python 3.6+" description = "Backported and Experimental Type Hints for Python 3.6+"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@ -605,7 +510,7 @@ python-versions = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "c9f7a8c9a15812c1f3affeaa301fe6e6310388cca41a229bdb97fdd58c765e6f" content-hash = "0c1303cb7e23bd0c24c31b08e727cfe278bc6bdaa2ac3450a8c689c2ee7b74f2"
[metadata.files] [metadata.files]
alembic = [ alembic = [
@ -620,14 +525,6 @@ asttokens = [
{file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"}, {file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"},
{file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"}, {file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"},
] ]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
backcall = [ backcall = [
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
@ -641,8 +538,8 @@ decorator = [
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
] ]
executing = [ executing = [
{file = "executing-0.8.3-py2.py3-none-any.whl", hash = "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9"}, {file = "executing-0.8.2-py2.py3-none-any.whl", hash = "sha256:32fc6077b103bd19e6494a72682d66d5763cf20a106d5aa7c5ccbea4e47b0df7"},
{file = "executing-0.8.3.tar.gz", hash = "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501"}, {file = "executing-0.8.2.tar.gz", hash = "sha256:c23bf42e9a7b9b212f185b1b2c3c91feb895963378887bb10e64a2e612ec0023"},
] ]
greenlet = [ greenlet = [
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
@ -701,10 +598,6 @@ greenlet = [
{file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"},
{file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"},
] ]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
ipdb = [ ipdb = [
{file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"},
] ]
@ -803,10 +696,6 @@ mysqlclient = [
{file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"}, {file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"},
{file = "mysqlclient-2.1.0.tar.gz", hash = "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12"}, {file = "mysqlclient-2.1.0.tar.gz", hash = "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12"},
] ]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
parso = [ parso = [
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
@ -819,10 +708,6 @@ pickleshare = [
{file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
] ]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
prompt-toolkit = [ prompt-toolkit = [
{file = "prompt_toolkit-3.0.28-py3-none-any.whl", hash = "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c"}, {file = "prompt_toolkit-3.0.28-py3-none-any.whl", hash = "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c"},
{file = "prompt_toolkit-3.0.28.tar.gz", hash = "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"}, {file = "prompt_toolkit-3.0.28.tar.gz", hash = "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"},
@ -869,10 +754,6 @@ pure-eval = [
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
] ]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pydub = [ pydub = [
{file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"},
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
@ -881,10 +762,6 @@ pygments = [
{file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
{file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"},
] ]
pyparsing = [
{file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
{file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
]
pyqt5 = [ pyqt5 = [
{file = "PyQt5-5.15.6-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:33ced1c876f6a26e7899615a5a4efef2167c263488837c7beed023a64cebd051"}, {file = "PyQt5-5.15.6-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:33ced1c876f6a26e7899615a5a4efef2167c263488837c7beed023a64cebd051"},
{file = "PyQt5-5.15.6-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:9d6efad0377aa78bf081a20ac752ce86096ded18f04c592d98f5b92dc879ad0a"}, {file = "PyQt5-5.15.6-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:9d6efad0377aa78bf081a20ac752ce86096ded18f04c592d98f5b92dc879ad0a"},
@ -921,10 +798,6 @@ pyqt5-sip = [
{file = "PyQt5_sip-12.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:989d51c41456cc496cb96f0b341464932b957040d26561f0bb4cf5a0914d6b36"}, {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"}, {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 = [ pyqtwebengine = [
{file = "PyQtWebEngine-5.15.5-cp36-abi3-macosx_10_13_x86_64.whl", hash = "sha256:5c77f71d88d871bc7400c68ef6433fadc5bd57b86d1a9c4d8094cea42f3607f1"}, {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"}, {file = "PyQtWebEngine-5.15.5-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:782aeee6bc8699bc029fe5c169a045c2bc9533d781cf3f5e9fb424b85a204e68"},
@ -947,8 +820,8 @@ pytest-qt = [
{file = "pytest_qt-4.0.2-py2.py3-none-any.whl", hash = "sha256:e03847ac02a890ccaac0fde1748855b9dce425aceba62005c6cfced6cf7d5456"}, {file = "pytest_qt-4.0.2-py2.py3-none-any.whl", hash = "sha256:e03847ac02a890ccaac0fde1748855b9dce425aceba62005c6cfced6cf7d5456"},
] ]
python-vlc = [ python-vlc = [
{file = "python-vlc-3.0.16120.tar.gz", hash = "sha256:92f98fee088f72bd6d063b3b3312d0bd29b37e7ad65ddeb3a7303320300c2807"}, {file = "python-vlc-3.0.12118.tar.gz", hash = "sha256:566f2f7c303f6800851cacc016df1c6eeec094ad63e0a49d87db9d698094f1fb"},
{file = "python_vlc-3.0.16120-py3-none-any.whl", hash = "sha256:c409afb38fe9f788a663b4302ca583f31289ef0860ab2b1668da96bbe8f14bfc"}, {file = "python_vlc-3.0.12118-py3-none-any.whl", hash = "sha256:f88be06c6f819a4db2de1c586b193b5df1410ff10fca33b8c6f4e56037c46f7b"},
] ]
six = [ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@ -992,10 +865,6 @@ sqlalchemy = [
{file = "SQLAlchemy-1.4.31-cp39-cp39-win_amd64.whl", hash = "sha256:9e4fb2895b83993831ba2401b6404de953fdbfa9d7d4fa6a4756294a83bbc94f"}, {file = "SQLAlchemy-1.4.31-cp39-cp39-win_amd64.whl", hash = "sha256:9e4fb2895b83993831ba2401b6404de953fdbfa9d7d4fa6a4756294a83bbc94f"},
{file = "SQLAlchemy-1.4.31.tar.gz", hash = "sha256:582b59d1e5780a447aada22b461e50b404a9dc05768da1d87368ad8190468418"}, {file = "SQLAlchemy-1.4.31.tar.gz", hash = "sha256:582b59d1e5780a447aada22b461e50b404a9dc05768da1d87368ad8190468418"},
] ]
sqlalchemy-stubs = [
{file = "sqlalchemy-stubs-0.4.tar.gz", hash = "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae"},
{file = "sqlalchemy_stubs-0.4-py3-none-any.whl", hash = "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5"},
]
stack-data = [ stack-data = [
{file = "stack_data-0.2.0-py3-none-any.whl", hash = "sha256:999762f9c3132308789affa03e9271bbbe947bf78311851f4d485d8402ed858e"}, {file = "stack_data-0.2.0-py3-none-any.whl", hash = "sha256:999762f9c3132308789affa03e9271bbbe947bf78311851f4d485d8402ed858e"},
{file = "stack_data-0.2.0.tar.gz", hash = "sha256:45692d41bd633a9503a5195552df22b583caf16f0b27c4e58c98d88c8b648e12"}, {file = "stack_data-0.2.0.tar.gz", hash = "sha256:45692d41bd633a9503a5195552df22b583caf16f0b27c4e58c98d88c8b648e12"},
@ -1016,10 +885,14 @@ traitlets = [
{file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"}, {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"},
] ]
typing-extensions = [ typing-extensions = [
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
] ]
wcwidth = [ wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
] ]
typing-extensions = [
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
]

View File

@ -17,14 +17,15 @@ psutil = "^5.9.0"
PyQtWebEngine = "^5.15.5" PyQtWebEngine = "^5.15.5"
pydub = "^0.25.1" pydub = "^0.25.1"
PyQt5-sip = "^12.9.1" PyQt5-sip = "^12.9.1"
mypy = "^0.931"
sqlalchemy-stubs = "^0.4"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
mypy = "^0.931"
pytest = "^7.0.0" pytest = "^7.0.0"
ipdb = "^0.13.9" ipdb = "^0.13.9"
pytest-qt = "^4.0.2"
sqlalchemy-stubs = "^0.4" sqlalchemy-stubs = "^0.4"
PyQt5-stubs = "^5.15.2" pytest-qt = "^4.0.2"
mypy = "^0.931"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]