Compare commits
2 Commits
a2fb6baba8
...
f22f2780a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f22f2780a3 | ||
|
|
f1aba41921 |
@ -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()
|
||||
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.
|
||||
|
||||
163
poetry.lock
generated
163
poetry.lock
generated
@ -35,6 +35,28 @@ six = "*"
|
||||
[package.extras]
|
||||
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]]
|
||||
name = "backcall"
|
||||
version = "0.2.0"
|
||||
@ -61,7 +83,7 @@ python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
description = "Get the currently executing AST node of a frame, and other information"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -78,6 +100,14 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||
[package.extras]
|
||||
docs = ["sphinx"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "ipdb"
|
||||
version = "0.13.9"
|
||||
@ -187,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"
|
||||
|
||||
@ -204,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 = "*"
|
||||
|
||||
@ -216,6 +246,17 @@ category = "main"
|
||||
optional = false
|
||||
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]]
|
||||
name = "parso"
|
||||
version = "0.8.3"
|
||||
@ -247,6 +288,18 @@ category = "dev"
|
||||
optional = false
|
||||
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]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.28"
|
||||
@ -288,6 +341,14 @@ python-versions = "*"
|
||||
[package.extras]
|
||||
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]]
|
||||
name = "pydub"
|
||||
version = "0.25.1"
|
||||
@ -304,6 +365,17 @@ category = "dev"
|
||||
optional = false
|
||||
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]]
|
||||
name = "pyqt5"
|
||||
version = "5.15.6"
|
||||
@ -332,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"
|
||||
@ -391,7 +474,7 @@ doc = ["sphinx", "sphinx-rtd-theme"]
|
||||
|
||||
[[package]]
|
||||
name = "python-vlc"
|
||||
version = "3.0.12118"
|
||||
version = "3.0.16120"
|
||||
description = "VLC bindings for python."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -437,6 +520,18 @@ postgresql_psycopg2cffi = ["psycopg2cffi"]
|
||||
pymysql = ["pymysql (<1)", "pymysql"]
|
||||
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]]
|
||||
name = "stack-data"
|
||||
version = "0.2.0"
|
||||
@ -476,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"
|
||||
|
||||
@ -493,9 +588,9 @@ test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.0.1"
|
||||
version = "4.1.1"
|
||||
description = "Backported and Experimental Type Hints for Python 3.6+"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
@ -510,7 +605,7 @@ python-versions = "*"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "0c1303cb7e23bd0c24c31b08e727cfe278bc6bdaa2ac3450a8c689c2ee7b74f2"
|
||||
content-hash = "c9f7a8c9a15812c1f3affeaa301fe6e6310388cca41a229bdb97fdd58c765e6f"
|
||||
|
||||
[metadata.files]
|
||||
alembic = [
|
||||
@ -525,6 +620,14 @@ asttokens = [
|
||||
{file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"},
|
||||
{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 = [
|
||||
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
|
||||
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
|
||||
@ -538,8 +641,8 @@ decorator = [
|
||||
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
|
||||
]
|
||||
executing = [
|
||||
{file = "executing-0.8.2-py2.py3-none-any.whl", hash = "sha256:32fc6077b103bd19e6494a72682d66d5763cf20a106d5aa7c5ccbea4e47b0df7"},
|
||||
{file = "executing-0.8.2.tar.gz", hash = "sha256:c23bf42e9a7b9b212f185b1b2c3c91feb895963378887bb10e64a2e612ec0023"},
|
||||
{file = "executing-0.8.3-py2.py3-none-any.whl", hash = "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9"},
|
||||
{file = "executing-0.8.3.tar.gz", hash = "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501"},
|
||||
]
|
||||
greenlet = [
|
||||
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
|
||||
@ -598,6 +701,10 @@ greenlet = [
|
||||
{file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"},
|
||||
{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 = [
|
||||
{file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"},
|
||||
]
|
||||
@ -696,6 +803,10 @@ mysqlclient = [
|
||||
{file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"},
|
||||
{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 = [
|
||||
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
|
||||
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
|
||||
@ -708,6 +819,10 @@ pickleshare = [
|
||||
{file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
|
||||
{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 = [
|
||||
{file = "prompt_toolkit-3.0.28-py3-none-any.whl", hash = "sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c"},
|
||||
{file = "prompt_toolkit-3.0.28.tar.gz", hash = "sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"},
|
||||
@ -754,6 +869,10 @@ pure-eval = [
|
||||
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
|
||||
{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 = [
|
||||
{file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"},
|
||||
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
|
||||
@ -762,6 +881,10 @@ pygments = [
|
||||
{file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
|
||||
{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 = [
|
||||
{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"},
|
||||
@ -798,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"},
|
||||
@ -820,8 +947,8 @@ pytest-qt = [
|
||||
{file = "pytest_qt-4.0.2-py2.py3-none-any.whl", hash = "sha256:e03847ac02a890ccaac0fde1748855b9dce425aceba62005c6cfced6cf7d5456"},
|
||||
]
|
||||
python-vlc = [
|
||||
{file = "python-vlc-3.0.12118.tar.gz", hash = "sha256:566f2f7c303f6800851cacc016df1c6eeec094ad63e0a49d87db9d698094f1fb"},
|
||||
{file = "python_vlc-3.0.12118-py3-none-any.whl", hash = "sha256:f88be06c6f819a4db2de1c586b193b5df1410ff10fca33b8c6f4e56037c46f7b"},
|
||||
{file = "python-vlc-3.0.16120.tar.gz", hash = "sha256:92f98fee088f72bd6d063b3b3312d0bd29b37e7ad65ddeb3a7303320300c2807"},
|
||||
{file = "python_vlc-3.0.16120-py3-none-any.whl", hash = "sha256:c409afb38fe9f788a663b4302ca583f31289ef0860ab2b1668da96bbe8f14bfc"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
@ -865,6 +992,10 @@ sqlalchemy = [
|
||||
{file = "SQLAlchemy-1.4.31-cp39-cp39-win_amd64.whl", hash = "sha256:9e4fb2895b83993831ba2401b6404de953fdbfa9d7d4fa6a4756294a83bbc94f"},
|
||||
{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 = [
|
||||
{file = "stack_data-0.2.0-py3-none-any.whl", hash = "sha256:999762f9c3132308789affa03e9271bbbe947bf78311851f4d485d8402ed858e"},
|
||||
{file = "stack_data-0.2.0.tar.gz", hash = "sha256:45692d41bd633a9503a5195552df22b583caf16f0b27c4e58c98d88c8b648e12"},
|
||||
@ -885,14 +1016,10 @@ traitlets = [
|
||||
{file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"},
|
||||
{file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"},
|
||||
{file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
|
||||
{file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
{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"},
|
||||
]
|
||||
|
||||
@ -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