Fix move tracks
This commit is contained in:
parent
f1aba41921
commit
f22f2780a3
@ -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) (2)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (musicmuster)" project-jdk-type="Python SDK" />
|
||||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||||
<option name="version" value="3" />
|
<option name="version" value="3" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@ -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) (2)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Poetry (musicmuster)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PyDocumentationSettings">
|
<component name="PyDocumentationSettings">
|
||||||
|
|||||||
@ -338,9 +338,6 @@ 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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -88,7 +88,6 @@ 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:
|
||||||
@ -355,7 +354,6 @@ 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)
|
||||||
@ -374,6 +372,7 @@ 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:
|
||||||
@ -383,7 +382,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
playlist_tab: PlaylistTab = PlaylistTab(
|
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)
|
idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name)
|
||||||
self.tabPlaylist.setCurrentIndex(idx)
|
self.tabPlaylist.setCurrentIndex(idx)
|
||||||
|
|
||||||
@ -391,47 +390,37 @@ 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 != self.visible_playlist_tab().id]
|
if p.id != visible_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
|
||||||
|
|
||||||
# TODO: just update dest playlist and call populate if
|
# Update database for both source and destination playlists
|
||||||
# visible
|
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
|
# Update destination playlist_tab if visible (if not visible, it
|
||||||
# tracks to it. If not, they will be automatically loaded when
|
# will be re-populated when it is opened)
|
||||||
# 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' attribute
|
# Non-playlist tabs won't have a 'playlist_id' attribute
|
||||||
if not hasattr(self.tabPlaylist.widget(tab), 'playlist'):
|
if not hasattr(self.tabPlaylist.widget(tab), 'playlist_id'):
|
||||||
continue
|
continue
|
||||||
if self.tabPlaylist.widget(tab).id == dlg.plid:
|
if self.tabPlaylist.widget(tab).playlist_id == dlg.plid:
|
||||||
destination_visible_playlist_tab = (
|
destination_visible_playlist_tab = (
|
||||||
self.tabPlaylist.widget(tab))
|
self.tabPlaylist.widget(tab))
|
||||||
break
|
break
|
||||||
|
|
||||||
rows = []
|
if destination_visible_playlist_tab:
|
||||||
for (row, track) in (
|
destination_visible_playlist_tab.populate(session, dlg.plid)
|
||||||
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()
|
|
||||||
|
|
||||||
# Update source playlist
|
# Update source playlist
|
||||||
self.visible_playlist_tab().remove_rows(rows)
|
self.visible_playlist_tab().remove_rows(rows)
|
||||||
@ -577,8 +566,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:
|
||||||
playlist = Playlists.get_by_id(session, dlg.plid)
|
p = Playlists.get_by_id(session=session, playlist_id=dlg.plid)
|
||||||
self.create_playlist_tab(session, playlist)
|
self.create_playlist_tab(session, p)
|
||||||
|
|
||||||
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"""
|
||||||
|
|||||||
438
app/playlists.py
438
app/playlists.py
@ -65,13 +65,12 @@ class PlaylistTab(QTableWidget):
|
|||||||
ROW_METADATA = Qt.UserRole
|
ROW_METADATA = Qt.UserRole
|
||||||
CONTENT_OBJECT = Qt.UserRole + 1
|
CONTENT_OBJECT = Qt.UserRole + 1
|
||||||
|
|
||||||
def __init__(self, parent: QMainWindow, session: Session,
|
def __init__(self, musicmuster: QMainWindow, session: Session,
|
||||||
playlist: Playlists, *args, **kwargs):
|
playlist_id: int, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.parent: QMainWindow = parent # The MusicMuster process
|
self.musicmuster: QMainWindow = musicmuster
|
||||||
self.playlist: Playlists = playlist
|
self.playlist_id: int = playlist_id
|
||||||
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
|
||||||
|
|
||||||
@ -140,13 +139,10 @@ 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, playlist)
|
self.populate(session, self.playlist_id)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (f"<PlaylistTab(id={self.playlist_id}")
|
||||||
f"<PlaylistTab(id={self.playlist.id}, "
|
|
||||||
f"name={self.playlist.name}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ########## Events ##########
|
# ########## Events ##########
|
||||||
|
|
||||||
@ -285,7 +281,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]:
|
||||||
@ -296,19 +292,11 @@ class PlaylistTab(QTableWidget):
|
|||||||
else:
|
else:
|
||||||
return self.selectionModel().selectedRows()[0].row()
|
return self.selectionModel().selectedRows()[0].row()
|
||||||
|
|
||||||
def get_selected_rows_and_tracks(self, session: Session) \
|
def get_selected_rows(self) -> List[int]:
|
||||||
-> Optional[List[Tuple[int, Tracks]]]:
|
"""Return a list of selected row numbers"""
|
||||||
"""Return a list of selected (row-number, track) tuples"""
|
|
||||||
|
|
||||||
if not self.selectionModel().hasSelection():
|
rows = self.selectionModel().selectedRows()
|
||||||
return None
|
return [row.row() for row in rows]
|
||||||
|
|
||||||
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"""
|
||||||
@ -448,6 +436,49 @@ 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.
|
||||||
@ -461,8 +492,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
changes.
|
changes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Ensure we have a valid database class
|
playlist = Playlists.get_by_id(session, self.playlist_id)
|
||||||
session.add(self.playlist)
|
|
||||||
|
|
||||||
# Notes first
|
# Notes first
|
||||||
# Create dictionaries indexed by note_id
|
# Create dictionaries indexed by note_id
|
||||||
@ -477,7 +507,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
playlist_notes[note.id] = note
|
playlist_notes[note.id] = note
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
for note in self.playlist.notes:
|
for note in 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
|
||||||
@ -505,14 +535,14 @@ class PlaylistTab(QTableWidget):
|
|||||||
|
|
||||||
# Tracks
|
# Tracks
|
||||||
# Remove all tracks from this playlist
|
# 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
|
# 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)
|
||||||
self.playlist.add_track(session, track_id, row)
|
playlist.add_track(session, track_id, row)
|
||||||
|
|
||||||
def select_next_row(self) -> None:
|
def select_next_row(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -616,17 +646,13 @@ 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: Optional[List[int]] = self._get_notes_rows()
|
notes: 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()
|
||||||
|
|
||||||
@ -871,7 +897,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
with Session() as session:
|
with Session() as session:
|
||||||
self.update_display(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:
|
def _cell_edit_started(self, row: int, column: int) -> None:
|
||||||
"""
|
"""
|
||||||
@ -883,7 +909,28 @@ 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.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:
|
def _delete_rows(self) -> None:
|
||||||
"""Delete mutliple rows"""
|
"""Delete mutliple rows"""
|
||||||
@ -932,6 +979,67 @@ 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"""
|
||||||
@ -945,6 +1053,11 @@ 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
|
||||||
@ -961,14 +1074,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_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"""
|
||||||
@ -977,6 +1082,11 @@ 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):
|
||||||
@ -989,6 +1099,24 @@ 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"""
|
||||||
|
|
||||||
@ -1071,43 +1199,6 @@ 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"""
|
||||||
|
|
||||||
@ -1118,22 +1209,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 _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.
|
||||||
@ -1143,46 +1218,11 @@ 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.
|
||||||
@ -1243,6 +1283,21 @@ 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"""
|
||||||
|
|
||||||
@ -1270,65 +1325,6 @@ 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.
|
||||||
@ -1346,10 +1342,31 @@ 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.parent.lblSumPlaytime.setText(
|
self.musicmuster.lblSumPlaytime.setText(
|
||||||
f"Selected duration: {helpers.ms_to_mmss(ms)}")
|
f"Selected duration: {helpers.ms_to_mmss(ms)}")
|
||||||
else:
|
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:
|
def _set_column_widths(self) -> None:
|
||||||
"""Column widths from settings"""
|
"""Column widths from settings"""
|
||||||
@ -1394,7 +1411,7 @@ class PlaylistTab(QTableWidget):
|
|||||||
self._set_next_track_row(row)
|
self._set_next_track_row(row)
|
||||||
|
|
||||||
# Notify musicmuster
|
# Notify musicmuster
|
||||||
self.parent.this_is_the_next_track(self, track)
|
self.musicmuster.this_is_the_next_track(self, track)
|
||||||
|
|
||||||
# Update display
|
# Update display
|
||||||
self.update_display(session)
|
self.update_display(session)
|
||||||
@ -1453,41 +1470,6 @@ 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.
|
||||||
|
|||||||
27
poetry.lock
generated
27
poetry.lock
generated
@ -217,7 +217,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 = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
@ -234,7 +234,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 = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
@ -404,6 +404,17 @@ 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"
|
||||||
@ -513,7 +524,7 @@ sqlcipher = ["sqlcipher3-binary"]
|
|||||||
name = "sqlalchemy-stubs"
|
name = "sqlalchemy-stubs"
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
description = "SQLAlchemy stubs and mypy plugin"
|
description = "SQLAlchemy stubs and mypy plugin"
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
@ -560,7 +571,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 = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
@ -579,7 +590,7 @@ test = ["pytest"]
|
|||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.1.1"
|
version = "4.1.1"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.6+"
|
description = "Backported and Experimental Type Hints for Python 3.6+"
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
@ -594,7 +605,7 @@ python-versions = "*"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "6ea248abf2050b4d2fb08baaf78fee1b358e43109bb1bbf202a4d3e78a35ec53"
|
content-hash = "c9f7a8c9a15812c1f3affeaa301fe6e6310388cca41a229bdb97fdd58c765e6f"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alembic = [
|
alembic = [
|
||||||
@ -910,6 +921,10 @@ pyqt5-sip = [
|
|||||||
{file = "PyQt5_sip-12.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:989d51c41456cc496cb96f0b341464932b957040d26561f0bb4cf5a0914d6b36"},
|
{file = "PyQt5_sip-12.9.1-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"},
|
||||||
|
|||||||
@ -17,15 +17,14 @@ 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"
|
||||||
sqlalchemy-stubs = "^0.4"
|
|
||||||
pytest-qt = "^4.0.2"
|
pytest-qt = "^4.0.2"
|
||||||
|
sqlalchemy-stubs = "^0.4"
|
||||||
|
PyQt5-stubs = "^5.15.2"
|
||||||
|
mypy = "^0.931"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user