Compare commits
5 Commits
5769e34412
...
c626d91f26
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c626d91f26 | ||
|
|
551a574eac | ||
|
|
80c363c316 | ||
|
|
48b180e280 | ||
|
|
223fb3bdec |
@ -81,8 +81,11 @@ class MusicMusterSignals(QObject):
|
|||||||
|
|
||||||
add_track_to_header_signal = pyqtSignal(int, int, int)
|
add_track_to_header_signal = pyqtSignal(int, int, int)
|
||||||
add_track_to_playlist_signal = pyqtSignal(int, int, int, str)
|
add_track_to_playlist_signal = pyqtSignal(int, int, int, str)
|
||||||
|
begin_reset_model_signal = pyqtSignal(int)
|
||||||
enable_escape_signal = pyqtSignal(bool)
|
enable_escape_signal = pyqtSignal(bool)
|
||||||
|
end_reset_model_signal = pyqtSignal(int)
|
||||||
next_track_changed_signal = pyqtSignal()
|
next_track_changed_signal = pyqtSignal()
|
||||||
|
row_order_changed_signal = pyqtSignal(int)
|
||||||
search_songfacts_signal = pyqtSignal(str)
|
search_songfacts_signal = pyqtSignal(str)
|
||||||
search_wikipedia_signal = pyqtSignal(str)
|
search_wikipedia_signal = pyqtSignal(str)
|
||||||
show_warning_signal = pyqtSignal(str, str)
|
show_warning_signal = pyqtSignal(str, str)
|
||||||
@ -136,10 +139,10 @@ class PlaylistTrack:
|
|||||||
Update with new plr information
|
Update with new plr information
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
session.add(plr)
|
||||||
|
self.plr_rownum = plr.plr_rownum
|
||||||
if not plr.track:
|
if not plr.track:
|
||||||
return
|
return
|
||||||
|
|
||||||
session.add(plr)
|
|
||||||
track = plr.track
|
track = plr.track
|
||||||
|
|
||||||
self.artist = track.artist
|
self.artist = track.artist
|
||||||
@ -149,7 +152,6 @@ class PlaylistTrack:
|
|||||||
self.path = track.path
|
self.path = track.path
|
||||||
self.playlist_id = plr.playlist_id
|
self.playlist_id = plr.playlist_id
|
||||||
self.plr_id = plr.id
|
self.plr_id = plr.id
|
||||||
self.plr_rownum = plr.plr_rownum
|
|
||||||
self.silence_at = track.silence_at
|
self.silence_at = track.silence_at
|
||||||
self.start_gap = track.start_gap
|
self.start_gap = track.start_gap
|
||||||
self.start_time = None
|
self.start_time = None
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from config import Config
|
|||||||
from dbconfig import scoped_session
|
from dbconfig import scoped_session
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pprint import pprint
|
||||||
from typing import List, Optional, Sequence
|
from typing import List, Optional, Sequence
|
||||||
|
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
@ -314,6 +315,7 @@ class Playlists(Base):
|
|||||||
"""Mark playlist as loaded and used now"""
|
"""Mark playlist as loaded and used now"""
|
||||||
|
|
||||||
self.open = True
|
self.open = True
|
||||||
|
self.last_used = datetime.now()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def name_is_available(session: scoped_session, name: str) -> bool:
|
def name_is_available(session: scoped_session, name: str) -> bool:
|
||||||
|
|||||||
@ -221,6 +221,8 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
self.active_tab = lambda: self.tabPlaylist.currentWidget()
|
self.active_tab = lambda: self.tabPlaylist.currentWidget()
|
||||||
self.active_model = lambda: self.tabPlaylist.currentWidget().model()
|
self.active_model = lambda: self.tabPlaylist.currentWidget().model()
|
||||||
|
self.move_source_rows: Optional[List[int]] = None
|
||||||
|
self.move_source_model: Optional[PlaylistModel] = None
|
||||||
|
|
||||||
self.load_last_playlists()
|
self.load_last_playlists()
|
||||||
if Config.CARTS_HIDE:
|
if Config.CARTS_HIDE:
|
||||||
@ -610,10 +612,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
Cut rows ready for pasting.
|
Cut rows ready for pasting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with Session() as session:
|
# Save the selected PlaylistRows items ready for a later
|
||||||
# Save the selected PlaylistRows items ready for a later
|
# paste
|
||||||
# paste
|
self.move_source_rows = self.active_tab().get_selected_rows()
|
||||||
self.selected_plrs = self.active_tab().get_selected_playlistrows(session)
|
self.move_source_model = self.active_model()
|
||||||
|
|
||||||
def debug(self):
|
def debug(self):
|
||||||
"""Invoke debugger"""
|
"""Invoke debugger"""
|
||||||
@ -925,106 +927,61 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
self.signals.search_wikipedia_signal.emit(track_info.title)
|
self.signals.search_wikipedia_signal.emit(track_info.title)
|
||||||
|
|
||||||
def move_playlist_rows(
|
def move_playlist_rows(self, row_numbers: List[int]) -> None:
|
||||||
self, session: scoped_session, playlistrows: Sequence[PlaylistRows]
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Move passed playlist rows to another playlist
|
Move passed playlist rows to another playlist
|
||||||
|
|
||||||
Actions required:
|
|
||||||
- exclude current/next tracks from being moved
|
|
||||||
- identify destination playlist
|
|
||||||
- update playlist for the rows in the database
|
|
||||||
- remove them from the display
|
|
||||||
- update destination playlist display if loaded
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Remove current/next rows from list
|
|
||||||
plrs_to_move = [
|
|
||||||
plr
|
|
||||||
for plr in playlistrows
|
|
||||||
if plr.id not in [track_sequence.now.plr_id, track_sequence.next.plr_id]
|
|
||||||
]
|
|
||||||
|
|
||||||
rows_to_delete = [
|
|
||||||
plr.plr_rownum for plr in plrs_to_move if plr.plr_rownum is not None
|
|
||||||
]
|
|
||||||
if not rows_to_delete:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Identify destination playlist
|
# Identify destination playlist
|
||||||
playlists = []
|
playlists = []
|
||||||
visible_tab = self.active_tab()
|
visible_tab = self.active_tab()
|
||||||
source_playlist_id = visible_tab.playlist_id
|
source_playlist_id = visible_tab.playlist_id
|
||||||
|
|
||||||
for playlist in Playlists.get_all(session):
|
with Session() as session:
|
||||||
if playlist.id == source_playlist_id:
|
for playlist in Playlists.get_all(session):
|
||||||
continue
|
if playlist.id == source_playlist_id:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
playlists.append(playlist)
|
||||||
|
|
||||||
|
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
||||||
|
dlg.exec()
|
||||||
|
if not dlg.playlist:
|
||||||
|
return
|
||||||
|
to_playlist_id = dlg.playlist.id
|
||||||
|
|
||||||
|
# Get row number in destination playlist
|
||||||
|
last_row = PlaylistRows.get_last_used_row(session, to_playlist_id)
|
||||||
|
if last_row is not None:
|
||||||
|
to_row = last_row + 1
|
||||||
else:
|
else:
|
||||||
playlists.append(playlist)
|
to_row = 0
|
||||||
|
|
||||||
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
# Move rows
|
||||||
dlg.exec()
|
self.active_model().move_rows_between_playlists(
|
||||||
if not dlg.playlist:
|
row_numbers, to_row, to_playlist_id
|
||||||
return
|
)
|
||||||
destination_playlist_id = dlg.playlist.id
|
|
||||||
|
|
||||||
# Update destination playlist in the database
|
|
||||||
last_row = PlaylistRows.get_last_used_row(session, destination_playlist_id)
|
|
||||||
if last_row is not None:
|
|
||||||
next_row = last_row + 1
|
|
||||||
else:
|
|
||||||
next_row = 0
|
|
||||||
|
|
||||||
for plr in plrs_to_move:
|
|
||||||
plr.plr_rownum = next_row
|
|
||||||
next_row += 1
|
|
||||||
plr.playlist_id = destination_playlist_id
|
|
||||||
# Reset played as it's not been played on this playlist
|
|
||||||
plr.played = False
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
# Remove moved rows from display and save visible playlist
|
|
||||||
visible_tab.remove_rows(rows_to_delete)
|
|
||||||
visible_tab.save_playlist(session)
|
|
||||||
|
|
||||||
# Disable sort undo
|
|
||||||
self.sort_undo: List[int] = []
|
|
||||||
|
|
||||||
# Update destination playlist_tab if visible (if not visible, it
|
|
||||||
# will be re-populated when it is opened)
|
|
||||||
destination_playlist_tab = None
|
|
||||||
for tab in range(self.tabPlaylist.count()):
|
|
||||||
if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id:
|
|
||||||
destination_playlist_tab = self.tabPlaylist.widget(tab)
|
|
||||||
break
|
|
||||||
if destination_playlist_tab:
|
|
||||||
destination_playlist_tab.populate_display(session, dlg.playlist.id)
|
|
||||||
|
|
||||||
def move_selected(self) -> None:
|
def move_selected(self) -> None:
|
||||||
"""
|
"""
|
||||||
Move selected rows to another playlist
|
Move selected rows to another playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with Session() as session:
|
selected_rows = self.active_tab().get_selected_rows()
|
||||||
selected_plrs = self.active_tab().get_selected_playlistrows(session)
|
if not selected_rows:
|
||||||
if not selected_plrs:
|
return
|
||||||
return
|
|
||||||
|
|
||||||
self.move_playlist_rows(session, selected_plrs)
|
self.move_playlist_rows(selected_rows)
|
||||||
|
|
||||||
def move_unplayed(self) -> None:
|
def move_unplayed(self) -> None:
|
||||||
"""
|
"""
|
||||||
Move unplayed rows to another playlist
|
Move unplayed rows to another playlist
|
||||||
"""
|
"""
|
||||||
|
|
||||||
playlist_id = self.active_tab().playlist_id
|
unplayed_rows = self.active_model().get_unplayed_rows()
|
||||||
with Session() as session:
|
if not unplayed_rows:
|
||||||
unplayed_plrs = PlaylistRows.get_unplayed_rows(session, playlist_id)
|
return
|
||||||
if helpers.ask_yes_no(
|
self.move_playlist_rows(unplayed_rows)
|
||||||
"Move tracks", f"Move {len(unplayed_plrs)} tracks:" " Are you sure?"
|
|
||||||
):
|
|
||||||
self.move_playlist_rows(session, unplayed_plrs)
|
|
||||||
|
|
||||||
def new_from_template(self) -> None:
|
def new_from_template(self) -> None:
|
||||||
"""Create new playlist from template"""
|
"""Create new playlist from template"""
|
||||||
@ -1065,69 +1022,25 @@ class Window(QMainWindow, Ui_MainWindow):
|
|||||||
def paste_rows(self) -> None:
|
def paste_rows(self) -> None:
|
||||||
"""
|
"""
|
||||||
Paste earlier cut rows.
|
Paste earlier cut rows.
|
||||||
|
|
||||||
Process:
|
|
||||||
- ensure we have some cut rows
|
|
||||||
- if not pasting at end of playlist, move later rows down
|
|
||||||
- update plrs with correct playlist and row
|
|
||||||
- if moving between playlists: renumber source playlist rows
|
|
||||||
- else: check integrity of playlist rows
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.selected_plrs:
|
if self.move_source_rows is None or self.move_source_model is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
playlist_tab = self.active_tab()
|
to_playlist_id = self.active_tab().playlist_id
|
||||||
dst_playlist_id = playlist_tab.playlist_id
|
selected_rows = self.active_tab().get_selected_rows()
|
||||||
dst_row = self.active_tab().get_new_row_number()
|
if selected_rows:
|
||||||
|
destination_row = selected_rows[0]
|
||||||
|
else:
|
||||||
|
destination_row = self.active_model().rowCount()
|
||||||
|
|
||||||
with Session() as session:
|
if to_playlist_id == self.move_source_model.playlist_id:
|
||||||
# Create space in destination playlist
|
self.move_source_model.move_rows(self.move_source_rows, destination_row)
|
||||||
PlaylistRows.move_rows_down(
|
else:
|
||||||
session, dst_playlist_id, dst_row, len(self.selected_plrs)
|
self.move_source_model.move_rows_between_playlists(
|
||||||
|
self.move_source_rows, destination_row, to_playlist_id
|
||||||
)
|
)
|
||||||
session.commit()
|
self.move_source_rows = self.move_source_model = None
|
||||||
|
|
||||||
# Update plrs
|
|
||||||
row = dst_row
|
|
||||||
src_playlist_id = None
|
|
||||||
for plr in self.selected_plrs:
|
|
||||||
# Update moved rows
|
|
||||||
session.add(plr)
|
|
||||||
if not src_playlist_id:
|
|
||||||
src_playlist_id = plr.playlist_id
|
|
||||||
plr.playlist_id = dst_playlist_id
|
|
||||||
plr.plr_rownum = row
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
if not src_playlist_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
session.flush()
|
|
||||||
|
|
||||||
# Update display
|
|
||||||
self.active_tab().populate_display(
|
|
||||||
session, dst_playlist_id, scroll_to_top=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# If source playlist is not destination playlist, fixup row
|
|
||||||
# numbers and update display
|
|
||||||
if src_playlist_id != dst_playlist_id:
|
|
||||||
PlaylistRows.fixup_rownumbers(session, src_playlist_id)
|
|
||||||
# Update source playlist_tab if visible (if not visible, it
|
|
||||||
# will be re-populated when it is opened)
|
|
||||||
source_playlist_tab = None
|
|
||||||
for tab in range(self.tabPlaylist.count()):
|
|
||||||
if self.tabPlaylist.widget(tab).playlist_id == src_playlist_id:
|
|
||||||
source_playlist_tab = self.tabPlaylist.widget(tab)
|
|
||||||
break
|
|
||||||
if source_playlist_tab:
|
|
||||||
source_playlist_tab.populate_display(
|
|
||||||
session, src_playlist_id, scroll_to_top=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Reset so rows can't be repasted
|
|
||||||
self.selected_plrs = None
|
|
||||||
|
|
||||||
def play_next(self, position: Optional[float] = None) -> None:
|
def play_next(self, position: Optional[float] = None) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import auto, Enum
|
from enum import auto, Enum
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
from pprint import pprint
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
@ -124,6 +125,9 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
self.signals.add_track_to_header_signal.connect(self.add_track_to_header)
|
self.signals.add_track_to_header_signal.connect(self.add_track_to_header)
|
||||||
self.signals.add_track_to_playlist_signal.connect(self.add_track)
|
self.signals.add_track_to_playlist_signal.connect(self.add_track)
|
||||||
|
self.signals.begin_reset_model_signal.connect(self.begin_reset_model)
|
||||||
|
self.signals.end_reset_model_signal.connect(self.end_reset_model)
|
||||||
|
self.signals.row_order_changed_signal.connect(self.row_order_changed)
|
||||||
|
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
# Ensure row numbers in playlist are contiguous
|
# Ensure row numbers in playlist are contiguous
|
||||||
@ -230,6 +234,15 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return QBrush()
|
return QBrush()
|
||||||
|
|
||||||
|
def begin_reset_model(self, playlist_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Reset model if playlist_id is ours
|
||||||
|
"""
|
||||||
|
|
||||||
|
if playlist_id != self.playlist_id:
|
||||||
|
return
|
||||||
|
super().beginResetModel()
|
||||||
|
|
||||||
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||||
"""Standard function for view"""
|
"""Standard function for view"""
|
||||||
|
|
||||||
@ -284,11 +297,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Find next track
|
# Find next track
|
||||||
# Get all unplayed track rows
|
# Get all unplayed track rows
|
||||||
next_row = None
|
next_row = None
|
||||||
unplayed_rows = [
|
unplayed_rows = self.get_unplayed_rows()
|
||||||
a.plr_rownum
|
|
||||||
for a in PlaylistRows.get_unplayed_rows(session, self.playlist_id)
|
|
||||||
]
|
|
||||||
|
|
||||||
if unplayed_rows:
|
if unplayed_rows:
|
||||||
try:
|
try:
|
||||||
# Find next row after current track
|
# Find next row after current track
|
||||||
@ -357,7 +366,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
|
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
|
||||||
self.refresh_data(session)
|
self.refresh_data(session)
|
||||||
self.update_track_times()
|
self.row_order_changed(self.playlist_id)
|
||||||
|
|
||||||
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
|
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
|
||||||
"""
|
"""
|
||||||
@ -403,6 +412,18 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return QVariant()
|
return QVariant()
|
||||||
|
|
||||||
|
def end_reset_model(self, playlist_id: int) -> None:
|
||||||
|
"""
|
||||||
|
End model reset if this is our playlist
|
||||||
|
"""
|
||||||
|
|
||||||
|
if playlist_id != self.playlist_id:
|
||||||
|
return
|
||||||
|
with Session() as session:
|
||||||
|
self.refresh_data(session)
|
||||||
|
super().endResetModel()
|
||||||
|
self.row_order_changed(self.playlist_id)
|
||||||
|
|
||||||
def get_duplicate_rows(self) -> List[int]:
|
def get_duplicate_rows(self) -> List[int]:
|
||||||
"""
|
"""
|
||||||
Return a list of duplicate rows. If track appears in rows 2, 3 and 4, return [3, 4]
|
Return a list of duplicate rows. If track appears in rows 2, 3 and 4, return [3, 4]
|
||||||
@ -518,6 +539,13 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return self.playlist_rows[row_number]
|
return self.playlist_rows[row_number]
|
||||||
|
|
||||||
|
def get_unplayed_rows(self) -> List[int]:
|
||||||
|
"""
|
||||||
|
Return a list of unplayed row numbers
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [a.plr_rownum for a in self.playlist_rows.values() if not a.played]
|
||||||
|
|
||||||
def headerData(
|
def headerData(
|
||||||
self,
|
self,
|
||||||
section: int,
|
section: int,
|
||||||
@ -675,7 +703,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.refresh_data(session)
|
self.refresh_data(session)
|
||||||
super().endInsertRows()
|
super().endInsertRows()
|
||||||
|
|
||||||
self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows))))
|
self.row_order_changed(self.playlist_id)
|
||||||
|
self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows))))
|
||||||
|
|
||||||
return plr
|
return plr
|
||||||
|
|
||||||
@ -773,9 +802,64 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.refresh_data(session)
|
self.refresh_data(session)
|
||||||
|
|
||||||
# Update display
|
# Update display
|
||||||
self.update_track_times()
|
self.signals.row_order_changed_signal.emit(self.playlist_id)
|
||||||
self.invalidate_rows(list(row_map.keys()))
|
self.invalidate_rows(list(row_map.keys()))
|
||||||
|
|
||||||
|
def move_rows_between_playlists(
|
||||||
|
self, from_rows: List[int], to_row_number: int, to_playlist_id: int
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Move the playlist rows given to to_row and below of to_playlist.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Row removal must be wrapped in beginRemoveRows ..
|
||||||
|
# endRemoveRows and the row range must be contiguous. Process
|
||||||
|
# the highest rows first so the lower row numbers are unchanged
|
||||||
|
row_groups = self._reversed_contiguous_row_groups(from_rows)
|
||||||
|
next_to_row = to_row_number
|
||||||
|
|
||||||
|
# Prepare destination playlist for a reset
|
||||||
|
self.signals.begin_reset_model_signal.emit(to_playlist_id)
|
||||||
|
|
||||||
|
with Session() as session:
|
||||||
|
# Make room in destination playlist
|
||||||
|
max_destination_row_number = PlaylistRows.get_last_used_row(
|
||||||
|
session, to_playlist_id
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
max_destination_row_number
|
||||||
|
and to_row_number <= max_destination_row_number
|
||||||
|
):
|
||||||
|
PlaylistRows.move_rows_down(
|
||||||
|
session, to_playlist_id, to_row_number, len(from_rows)
|
||||||
|
)
|
||||||
|
|
||||||
|
for row_group in row_groups:
|
||||||
|
super().beginRemoveRows(QModelIndex(), min(row_group), max(row_group))
|
||||||
|
for plr in PlaylistRows.plrids_to_plrs(
|
||||||
|
session,
|
||||||
|
self.playlist_id,
|
||||||
|
[self.playlist_rows[a].plrid for a in row_group],
|
||||||
|
):
|
||||||
|
if plr.id == track_sequence.now.plr_id:
|
||||||
|
# Don't move current track
|
||||||
|
continue
|
||||||
|
plr.playlist_id = to_playlist_id
|
||||||
|
plr.plr_rownum = next_to_row
|
||||||
|
next_to_row += 1
|
||||||
|
self.refresh_data(session)
|
||||||
|
super().endRemoveRows()
|
||||||
|
# We need to remove gaps in row numbers after tracks have
|
||||||
|
# moved.
|
||||||
|
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
|
||||||
|
self.refresh_data(session)
|
||||||
|
|
||||||
|
# Reset of model must come after session has been closed
|
||||||
|
self.signals.row_order_changed_signal.emit(self.playlist_id)
|
||||||
|
self.signals.row_order_changed_signal.emit(to_playlist_id)
|
||||||
|
self.signals.end_reset_model_signal.emit(to_playlist_id)
|
||||||
|
self.update_track_times()
|
||||||
|
|
||||||
def open_in_audacity(self, row_number: int) -> None:
|
def open_in_audacity(self, row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
Open track at passed row number in Audacity
|
Open track at passed row number in Audacity
|
||||||
@ -851,11 +935,63 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.refresh_row(session, row_number)
|
self.refresh_row(session, row_number)
|
||||||
self.invalidate_row(row_number)
|
self.invalidate_row(row_number)
|
||||||
|
|
||||||
|
def _reversed_contiguous_row_groups(
|
||||||
|
self, row_numbers: List[int]
|
||||||
|
) -> List[List[int]]:
|
||||||
|
"""
|
||||||
|
Take the list of row numbers and split into groups of contiguous rows. Return as a list
|
||||||
|
of lists with the highest row numbers first.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
input [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21]
|
||||||
|
return: [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]]
|
||||||
|
"""
|
||||||
|
|
||||||
|
result: List[List[int]] = []
|
||||||
|
temp: List[int] = []
|
||||||
|
last_value = row_numbers[0] - 1
|
||||||
|
|
||||||
|
for idx in range(len(row_numbers)):
|
||||||
|
if row_numbers[idx] != last_value + 1:
|
||||||
|
result.append(temp)
|
||||||
|
temp = []
|
||||||
|
last_value = row_numbers[idx]
|
||||||
|
temp.append(last_value)
|
||||||
|
if temp:
|
||||||
|
result.append(temp)
|
||||||
|
result.reverse()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
||||||
"""Standard function for view"""
|
"""Standard function for view"""
|
||||||
|
|
||||||
return len(self.playlist_rows)
|
return len(self.playlist_rows)
|
||||||
|
|
||||||
|
def row_order_changed(self, playlist_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Signal handler for when row ordering has changed
|
||||||
|
"""
|
||||||
|
|
||||||
|
if playlist_id != self.playlist_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
with Session() as session:
|
||||||
|
if track_sequence.next.plr_rownum:
|
||||||
|
next_plr = session.get(PlaylistRows, track_sequence.next.plr_rownum)
|
||||||
|
if next_plr:
|
||||||
|
track_sequence.next.plr_rownum = next_plr.plr_rownum
|
||||||
|
if track_sequence.now.plr_rownum:
|
||||||
|
now_plr = session.get(PlaylistRows, track_sequence.now.plr_rownum)
|
||||||
|
if now_plr:
|
||||||
|
track_sequence.now.plr_rownum = now_plr.plr_rownum
|
||||||
|
if track_sequence.previous.plr_rownum:
|
||||||
|
previous_plr = session.get(PlaylistRows, track_sequence.previous.plr_rownum)
|
||||||
|
if previous_plr:
|
||||||
|
track_sequence.previous.plr_rownum = previous_plr.plr_rownum
|
||||||
|
|
||||||
|
self.update_track_times()
|
||||||
|
|
||||||
def selection_is_sortable(self, row_numbers: List[int]) -> bool:
|
def selection_is_sortable(self, row_numbers: List[int]) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if the selection is sortable. That means:
|
Return True if the selection is sortable. That means:
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import threading
|
|||||||
import obsws_python as obs # type: ignore
|
import obsws_python as obs # type: ignore
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from pprint import pprint
|
||||||
from typing import Any, Callable, cast, List, Optional, Tuple, TYPE_CHECKING
|
from typing import Any, Callable, cast, List, Optional, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
@ -541,7 +542,7 @@ class PlaylistTab(QTableView):
|
|||||||
# Mark unplayed
|
# Mark unplayed
|
||||||
if track_row and model.is_unplayed_row(row_number):
|
if track_row and model.is_unplayed_row(row_number):
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"Mark unplayed", lambda: self._mark_as_unplayed(self._get_selected_rows())
|
"Mark unplayed", lambda: self._mark_as_unplayed(self.get_selected_rows())
|
||||||
)
|
)
|
||||||
|
|
||||||
# Unmark as next
|
# Unmark as next
|
||||||
@ -557,22 +558,22 @@ class PlaylistTab(QTableView):
|
|||||||
sort_menu = self.menu.addMenu("Sort")
|
sort_menu = self.menu.addMenu("Sort")
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by title",
|
"by title",
|
||||||
lambda: model.sort_by_title(self._get_selected_rows()),
|
lambda: model.sort_by_title(self.get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by artist",
|
"by artist",
|
||||||
lambda: model.sort_by_artist(self._get_selected_rows()),
|
lambda: model.sort_by_artist(self.get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by duration",
|
"by duration",
|
||||||
lambda: model.sort_by_duration(self._get_selected_rows()),
|
lambda: model.sort_by_duration(self.get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"by last played",
|
"by last played",
|
||||||
lambda: model.sort_by_lastplayed(self._get_selected_rows()),
|
lambda: model.sort_by_lastplayed(self.get_selected_rows()),
|
||||||
parent_menu=sort_menu,
|
parent_menu=sort_menu,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -652,7 +653,7 @@ class PlaylistTab(QTableView):
|
|||||||
- Pass to model to do the deed
|
- Pass to model to do the deed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
rows_to_delete = self._get_selected_rows()
|
rows_to_delete = self.get_selected_rows()
|
||||||
row_count = len(rows_to_delete)
|
row_count = len(rows_to_delete)
|
||||||
if row_count < 1:
|
if row_count < 1:
|
||||||
return
|
return
|
||||||
@ -663,9 +664,9 @@ class PlaylistTab(QTableView):
|
|||||||
return
|
return
|
||||||
|
|
||||||
model = cast(PlaylistModel, self.model())
|
model = cast(PlaylistModel, self.model())
|
||||||
model.delete_rows(self._get_selected_rows())
|
model.delete_rows(self.get_selected_rows())
|
||||||
|
|
||||||
def _get_selected_rows(self) -> List[int]:
|
def get_selected_rows(self) -> List[int]:
|
||||||
"""Return a list of selected row numbers sorted by row"""
|
"""Return a list of selected row numbers sorted by row"""
|
||||||
|
|
||||||
# Use a set to deduplicate result (a selected row will have all
|
# Use a set to deduplicate result (a selected row will have all
|
||||||
@ -939,13 +940,13 @@ class PlaylistTab(QTableView):
|
|||||||
Toggle drag behaviour according to whether rows are selected
|
Toggle drag behaviour according to whether rows are selected
|
||||||
"""
|
"""
|
||||||
|
|
||||||
selected_rows = self._get_selected_rows()
|
selected_rows = self.get_selected_rows()
|
||||||
# If no rows are selected, we have nothing to do
|
# If no rows are selected, we have nothing to do
|
||||||
if len(selected_rows) == 0:
|
if len(selected_rows) == 0:
|
||||||
self.musicmuster.lblSumPlaytime.setText("")
|
self.musicmuster.lblSumPlaytime.setText("")
|
||||||
else:
|
else:
|
||||||
model = cast(PlaylistModel, self.model())
|
model = cast(PlaylistModel, self.model())
|
||||||
selected_duration = model.get_rows_duration(self._get_selected_rows())
|
selected_duration = model.get_rows_duration(self.get_selected_rows())
|
||||||
if selected_duration > 0:
|
if selected_duration > 0:
|
||||||
self.musicmuster.lblSumPlaytime.setText(
|
self.musicmuster.lblSumPlaytime.setText(
|
||||||
f"Selected duration: {ms_to_mmss(selected_duration)}"
|
f"Selected duration: {ms_to_mmss(selected_duration)}"
|
||||||
|
|||||||
@ -131,12 +131,12 @@ def test_playlist_open_and_close(session):
|
|||||||
assert len(Playlists.get_open(session)) == 0
|
assert len(Playlists.get_open(session)) == 0
|
||||||
assert len(Playlists.get_closed(session)) == 1
|
assert len(Playlists.get_closed(session)) == 1
|
||||||
|
|
||||||
playlist.mark_open(session, tab_index=0)
|
playlist.mark_open()
|
||||||
|
|
||||||
assert len(Playlists.get_open(session)) == 1
|
assert len(Playlists.get_open(session)) == 1
|
||||||
assert len(Playlists.get_closed(session)) == 0
|
assert len(Playlists.get_closed(session)) == 0
|
||||||
|
|
||||||
playlist.close(session)
|
playlist.close()
|
||||||
|
|
||||||
assert len(Playlists.get_open(session)) == 0
|
assert len(Playlists.get_open(session)) == 0
|
||||||
assert len(Playlists.get_closed(session)) == 1
|
assert len(Playlists.get_closed(session)) == 1
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
from pprint import pprint
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Playlists,
|
Playlists,
|
||||||
Tracks,
|
Tracks,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt, QModelIndex
|
||||||
|
|
||||||
from app.helpers import get_file_metadata
|
from app.helpers import get_file_metadata
|
||||||
from app import playlistmodel
|
from app import playlistmodel
|
||||||
@ -19,8 +22,8 @@ test_tracks = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def create_model_with_tracks(session: scoped_session) -> "playlistmodel.PlaylistModel":
|
def create_model_with_tracks(session: scoped_session, name: Optional[str] = None) -> "playlistmodel.PlaylistModel":
|
||||||
playlist = Playlists(session, "test playlist")
|
playlist = Playlists(session, name or "test playlist")
|
||||||
model = playlistmodel.PlaylistModel(playlist.id)
|
model = playlistmodel.PlaylistModel(playlist.id)
|
||||||
|
|
||||||
for row in range(len(test_tracks)):
|
for row in range(len(test_tracks)):
|
||||||
@ -34,9 +37,9 @@ def create_model_with_tracks(session: scoped_session) -> "playlistmodel.Playlist
|
|||||||
|
|
||||||
|
|
||||||
def create_model_with_playlist_rows(
|
def create_model_with_playlist_rows(
|
||||||
session: scoped_session, rows: int
|
session: scoped_session, rows: int, name: Optional[str] = None
|
||||||
) -> "playlistmodel.PlaylistModel":
|
) -> "playlistmodel.PlaylistModel":
|
||||||
playlist = Playlists(session, "test playlist")
|
playlist = Playlists(session, name or "test playlist")
|
||||||
# Create a model
|
# Create a model
|
||||||
model = playlistmodel.PlaylistModel(playlist.id)
|
model = playlistmodel.PlaylistModel(playlist.id)
|
||||||
for row in range(rows):
|
for row in range(rows):
|
||||||
@ -268,6 +271,97 @@ def test_insert_track_new_playlist(monkeypatch, session):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reverse_row_groups_one_row(monkeypatch, session):
|
||||||
|
monkeypatch.setattr(playlistmodel, "Session", session)
|
||||||
|
|
||||||
|
rows_to_move = [3]
|
||||||
|
|
||||||
|
model_src = create_model_with_playlist_rows(session, 5, name="source")
|
||||||
|
result = model_src._reversed_contiguous_row_groups(rows_to_move)
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == [3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reverse_row_groups_multiple_row(monkeypatch, session):
|
||||||
|
monkeypatch.setattr(playlistmodel, "Session", session)
|
||||||
|
|
||||||
|
rows_to_move = [2, 3, 4, 5, 7, 9, 10, 13, 17, 20, 21]
|
||||||
|
|
||||||
|
model_src = create_model_with_playlist_rows(session, 5, name="source")
|
||||||
|
result = model_src._reversed_contiguous_row_groups(rows_to_move)
|
||||||
|
|
||||||
|
assert result == [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_one_row_between_playlists_to_end(monkeypatch, session):
|
||||||
|
monkeypatch.setattr(playlistmodel, "Session", session)
|
||||||
|
|
||||||
|
create_rowcount = 5
|
||||||
|
from_rows = [3]
|
||||||
|
to_row = create_rowcount
|
||||||
|
|
||||||
|
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
||||||
|
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
||||||
|
|
||||||
|
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id)
|
||||||
|
model_dst.refresh_data(session)
|
||||||
|
|
||||||
|
assert len(model_src.playlist_rows) == create_rowcount - len(from_rows)
|
||||||
|
assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows)
|
||||||
|
assert sorted([a.plr_rownum for a in model_src.playlist_rows.values()]) == list(
|
||||||
|
range(len(model_src.playlist_rows))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_one_row_between_playlists_to_middle(monkeypatch, session):
|
||||||
|
monkeypatch.setattr(playlistmodel, "Session", session)
|
||||||
|
|
||||||
|
create_rowcount = 5
|
||||||
|
from_rows = [3]
|
||||||
|
to_row = 2
|
||||||
|
|
||||||
|
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
||||||
|
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
||||||
|
|
||||||
|
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id)
|
||||||
|
model_dst.refresh_data(session)
|
||||||
|
|
||||||
|
# Check the rows of the destination model
|
||||||
|
row_notes = []
|
||||||
|
for row_number in range(model_dst.rowCount()):
|
||||||
|
index = model_dst.index(row_number, playlistmodel.Col.TITLE.value, QModelIndex())
|
||||||
|
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
|
||||||
|
|
||||||
|
assert len(model_src.playlist_rows) == create_rowcount - len(from_rows)
|
||||||
|
assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows)
|
||||||
|
assert [int(a) for a in row_notes] == [0, 1, 3, 2, 3, 4]
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session):
|
||||||
|
monkeypatch.setattr(playlistmodel, "Session", session)
|
||||||
|
|
||||||
|
create_rowcount = 5
|
||||||
|
from_rows = [1, 3, 4]
|
||||||
|
to_row = 2
|
||||||
|
|
||||||
|
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
|
||||||
|
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
|
||||||
|
|
||||||
|
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id)
|
||||||
|
model_dst.refresh_data(session)
|
||||||
|
|
||||||
|
# Check the rows of the destination model
|
||||||
|
row_notes = []
|
||||||
|
for row_number in range(model_dst.rowCount()):
|
||||||
|
index = model_dst.index(row_number, playlistmodel.Col.TITLE.value, QModelIndex())
|
||||||
|
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
|
||||||
|
|
||||||
|
assert len(model_src.playlist_rows) == create_rowcount - len(from_rows)
|
||||||
|
assert len(model_dst.playlist_rows) == create_rowcount + len(from_rows)
|
||||||
|
assert [int(a) for a in row_notes] == [0, 1, 3, 4, 1, 2, 3, 4]
|
||||||
|
|
||||||
|
|
||||||
# def test_edit_header(monkeypatch, session): # edit header row in middle of playlist
|
# def test_edit_header(monkeypatch, session): # edit header row in middle of playlist
|
||||||
|
|
||||||
# monkeypatch.setattr(playlistmodel, "Session", session)
|
# monkeypatch.setattr(playlistmodel, "Session", session)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user