Compare commits

..

No commits in common. "c626d91f26ca12426b9af0c6f240f496e4a3182f" and "5769e34412bf3d3edfce3839be11e62e485073f0" have entirely different histories.

7 changed files with 165 additions and 313 deletions

View File

@ -81,11 +81,8 @@ class MusicMusterSignals(QObject):
add_track_to_header_signal = pyqtSignal(int, int, int)
add_track_to_playlist_signal = pyqtSignal(int, int, int, str)
begin_reset_model_signal = pyqtSignal(int)
enable_escape_signal = pyqtSignal(bool)
end_reset_model_signal = pyqtSignal(int)
next_track_changed_signal = pyqtSignal()
row_order_changed_signal = pyqtSignal(int)
search_songfacts_signal = pyqtSignal(str)
search_wikipedia_signal = pyqtSignal(str)
show_warning_signal = pyqtSignal(str, str)
@ -139,10 +136,10 @@ class PlaylistTrack:
Update with new plr information
"""
session.add(plr)
self.plr_rownum = plr.plr_rownum
if not plr.track:
return
session.add(plr)
track = plr.track
self.artist = track.artist
@ -152,6 +149,7 @@ class PlaylistTrack:
self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.plr_rownum = plr.plr_rownum
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.start_time = None

View File

@ -6,7 +6,6 @@ from config import Config
from dbconfig import scoped_session
from datetime import datetime
from pprint import pprint
from typing import List, Optional, Sequence
from sqlalchemy.ext.associationproxy import association_proxy
@ -315,7 +314,6 @@ class Playlists(Base):
"""Mark playlist as loaded and used now"""
self.open = True
self.last_used = datetime.now()
@staticmethod
def name_is_available(session: scoped_session, name: str) -> bool:

View File

@ -221,8 +221,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.active_tab = lambda: self.tabPlaylist.currentWidget()
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()
if Config.CARTS_HIDE:
@ -612,10 +610,10 @@ class Window(QMainWindow, Ui_MainWindow):
Cut rows ready for pasting.
"""
# Save the selected PlaylistRows items ready for a later
# paste
self.move_source_rows = self.active_tab().get_selected_rows()
self.move_source_model = self.active_model()
with Session() as session:
# Save the selected PlaylistRows items ready for a later
# paste
self.selected_plrs = self.active_tab().get_selected_playlistrows(session)
def debug(self):
"""Invoke debugger"""
@ -927,61 +925,106 @@ class Window(QMainWindow, Ui_MainWindow):
self.signals.search_wikipedia_signal.emit(track_info.title)
def move_playlist_rows(self, row_numbers: List[int]) -> None:
def move_playlist_rows(
self, session: scoped_session, playlistrows: Sequence[PlaylistRows]
) -> None:
"""
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
playlists = []
visible_tab = self.active_tab()
source_playlist_id = visible_tab.playlist_id
with Session() as session:
for playlist in Playlists.get_all(session):
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
for playlist in Playlists.get_all(session):
if playlist.id == source_playlist_id:
continue
else:
to_row = 0
playlists.append(playlist)
# Move rows
self.active_model().move_rows_between_playlists(
row_numbers, to_row, to_playlist_id
)
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
dlg.exec()
if not dlg.playlist:
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:
"""
Move selected rows to another playlist
"""
selected_rows = self.active_tab().get_selected_rows()
if not selected_rows:
return
with Session() as session:
selected_plrs = self.active_tab().get_selected_playlistrows(session)
if not selected_plrs:
return
self.move_playlist_rows(selected_rows)
self.move_playlist_rows(session, selected_plrs)
def move_unplayed(self) -> None:
"""
Move unplayed rows to another playlist
"""
unplayed_rows = self.active_model().get_unplayed_rows()
if not unplayed_rows:
return
self.move_playlist_rows(unplayed_rows)
playlist_id = self.active_tab().playlist_id
with Session() as session:
unplayed_plrs = PlaylistRows.get_unplayed_rows(session, playlist_id)
if helpers.ask_yes_no(
"Move tracks", f"Move {len(unplayed_plrs)} tracks:" " Are you sure?"
):
self.move_playlist_rows(session, unplayed_plrs)
def new_from_template(self) -> None:
"""Create new playlist from template"""
@ -1022,25 +1065,69 @@ class Window(QMainWindow, Ui_MainWindow):
def paste_rows(self) -> None:
"""
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 self.move_source_rows is None or self.move_source_model is None:
if not self.selected_plrs:
return
to_playlist_id = self.active_tab().playlist_id
selected_rows = self.active_tab().get_selected_rows()
if selected_rows:
destination_row = selected_rows[0]
else:
destination_row = self.active_model().rowCount()
playlist_tab = self.active_tab()
dst_playlist_id = playlist_tab.playlist_id
dst_row = self.active_tab().get_new_row_number()
if to_playlist_id == self.move_source_model.playlist_id:
self.move_source_model.move_rows(self.move_source_rows, destination_row)
else:
self.move_source_model.move_rows_between_playlists(
self.move_source_rows, destination_row, to_playlist_id
with Session() as session:
# Create space in destination playlist
PlaylistRows.move_rows_down(
session, dst_playlist_id, dst_row, len(self.selected_plrs)
)
self.move_source_rows = self.move_source_model = None
session.commit()
# 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:
"""

View File

@ -2,7 +2,6 @@ from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import auto, Enum
from operator import attrgetter
from pprint import pprint
from typing import List, Optional
from PyQt6.QtCore import (
@ -125,9 +124,6 @@ class PlaylistModel(QAbstractTableModel):
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.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:
# Ensure row numbers in playlist are contiguous
@ -234,15 +230,6 @@ class PlaylistModel(QAbstractTableModel):
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:
"""Standard function for view"""
@ -297,7 +284,11 @@ class PlaylistModel(QAbstractTableModel):
# Find next track
# Get all unplayed track rows
next_row = None
unplayed_rows = self.get_unplayed_rows()
unplayed_rows = [
a.plr_rownum
for a in PlaylistRows.get_unplayed_rows(session, self.playlist_id)
]
if unplayed_rows:
try:
# Find next row after current track
@ -366,7 +357,7 @@ class PlaylistModel(QAbstractTableModel):
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
self.refresh_data(session)
self.row_order_changed(self.playlist_id)
self.update_track_times()
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
"""
@ -412,18 +403,6 @@ class PlaylistModel(QAbstractTableModel):
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]:
"""
Return a list of duplicate rows. If track appears in rows 2, 3 and 4, return [3, 4]
@ -539,13 +518,6 @@ class PlaylistModel(QAbstractTableModel):
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(
self,
section: int,
@ -703,8 +675,7 @@ class PlaylistModel(QAbstractTableModel):
self.refresh_data(session)
super().endInsertRows()
self.row_order_changed(self.playlist_id)
self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows))))
self.invalidate_rows(list(range(plr.plr_rownum, len(self.playlist_rows))))
return plr
@ -802,63 +773,8 @@ class PlaylistModel(QAbstractTableModel):
self.refresh_data(session)
# Update display
self.signals.row_order_changed_signal.emit(self.playlist_id)
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()
self.invalidate_rows(list(row_map.keys()))
def open_in_audacity(self, row_number: int) -> None:
"""
@ -935,63 +851,11 @@ class PlaylistModel(QAbstractTableModel):
self.refresh_row(session, 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:
"""Standard function for view"""
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:
"""
Return True if the selection is sortable. That means:

View File

@ -7,7 +7,6 @@ import threading
import obsws_python as obs # type: ignore
from datetime import datetime, timedelta
from pprint import pprint
from typing import Any, Callable, cast, List, Optional, Tuple, TYPE_CHECKING
from PyQt6.QtCore import (
@ -542,7 +541,7 @@ class PlaylistTab(QTableView):
# Mark unplayed
if track_row and model.is_unplayed_row(row_number):
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
@ -558,22 +557,22 @@ class PlaylistTab(QTableView):
sort_menu = self.menu.addMenu("Sort")
self._add_context_menu(
"by title",
lambda: model.sort_by_title(self.get_selected_rows()),
lambda: model.sort_by_title(self._get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"by artist",
lambda: model.sort_by_artist(self.get_selected_rows()),
lambda: model.sort_by_artist(self._get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"by duration",
lambda: model.sort_by_duration(self.get_selected_rows()),
lambda: model.sort_by_duration(self._get_selected_rows()),
parent_menu=sort_menu,
)
self._add_context_menu(
"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,
)
@ -653,7 +652,7 @@ class PlaylistTab(QTableView):
- 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)
if row_count < 1:
return
@ -664,9 +663,9 @@ class PlaylistTab(QTableView):
return
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"""
# Use a set to deduplicate result (a selected row will have all
@ -940,13 +939,13 @@ class PlaylistTab(QTableView):
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 len(selected_rows) == 0:
self.musicmuster.lblSumPlaytime.setText("")
else:
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:
self.musicmuster.lblSumPlaytime.setText(
f"Selected duration: {ms_to_mmss(selected_duration)}"

View File

@ -131,12 +131,12 @@ def test_playlist_open_and_close(session):
assert len(Playlists.get_open(session)) == 0
assert len(Playlists.get_closed(session)) == 1
playlist.mark_open()
playlist.mark_open(session, tab_index=0)
assert len(Playlists.get_open(session)) == 1
assert len(Playlists.get_closed(session)) == 0
playlist.close()
playlist.close(session)
assert len(Playlists.get_open(session)) == 0
assert len(Playlists.get_closed(session)) == 1

View File

@ -1,11 +1,8 @@
from pprint import pprint
from typing import Optional
from app.models import (
Playlists,
Tracks,
)
from PyQt6.QtCore import Qt, QModelIndex
from PyQt6.QtCore import Qt
from app.helpers import get_file_metadata
from app import playlistmodel
@ -22,8 +19,8 @@ test_tracks = [
]
def create_model_with_tracks(session: scoped_session, name: Optional[str] = None) -> "playlistmodel.PlaylistModel":
playlist = Playlists(session, name or "test playlist")
def create_model_with_tracks(session: scoped_session) -> "playlistmodel.PlaylistModel":
playlist = Playlists(session, "test playlist")
model = playlistmodel.PlaylistModel(playlist.id)
for row in range(len(test_tracks)):
@ -37,9 +34,9 @@ def create_model_with_tracks(session: scoped_session, name: Optional[str] = None
def create_model_with_playlist_rows(
session: scoped_session, rows: int, name: Optional[str] = None
session: scoped_session, rows: int
) -> "playlistmodel.PlaylistModel":
playlist = Playlists(session, name or "test playlist")
playlist = Playlists(session, "test playlist")
# Create a model
model = playlistmodel.PlaylistModel(playlist.id)
for row in range(rows):
@ -271,97 +268,6 @@ 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
# monkeypatch.setattr(playlistmodel, "Session", session)