Implement playlist range sort and unsort
This commit is contained in:
parent
06e457a3da
commit
87ab973439
@ -1,3 +1,4 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional
|
||||
@ -46,6 +47,7 @@ class Config(object):
|
||||
DEBUG_MODULES: List[Optional[str]] = ['dbconfig']
|
||||
DEFAULT_COLUMN_WIDTH = 200
|
||||
DISPLAY_SQL = False
|
||||
EPOCH = datetime.datetime(1970, 1, 1)
|
||||
ERRORS_FROM = ['noreply@midnighthax.com']
|
||||
ERRORS_TO = ['kae@midnighthax.com']
|
||||
FADE_CURVE_BACKGROUND = "lightyellow"
|
||||
|
||||
@ -119,7 +119,7 @@ def get_relative_date(
|
||||
@return: string
|
||||
"""
|
||||
|
||||
if not past_date:
|
||||
if not past_date or past_date == Config.EPOCH:
|
||||
return "Never"
|
||||
if not reference_date:
|
||||
reference_date = datetime.now()
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import re
|
||||
|
||||
from config import Config
|
||||
from dbconfig import scoped_session
|
||||
|
||||
from datetime import datetime
|
||||
@ -153,7 +154,7 @@ class Playdates(Base):
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def last_played(session: scoped_session, track_id: int) -> Optional[datetime]:
|
||||
def last_played(session: scoped_session, track_id: int) -> datetime:
|
||||
"""Return datetime track last played or None"""
|
||||
|
||||
last_played = session.execute(
|
||||
@ -166,7 +167,7 @@ class Playdates(Base):
|
||||
if last_played:
|
||||
return last_played[0]
|
||||
else:
|
||||
return None
|
||||
return Config.EPOCH
|
||||
|
||||
@staticmethod
|
||||
def played_after(session: scoped_session, since: datetime) -> List["Playdates"]:
|
||||
@ -194,6 +195,7 @@ class Playlists(Base):
|
||||
name: str = Column(String(32), nullable=False, unique=True)
|
||||
last_used = Column(DateTime, default=None, nullable=True)
|
||||
tab = Column(Integer, default=None, nullable=True, unique=True)
|
||||
# TODO sort_column is unused
|
||||
sort_column = Column(Integer, default=None, nullable=True, unique=False)
|
||||
is_template: bool = Column(Boolean, default=False, nullable=False)
|
||||
query = Column(String(256), default=None, nullable=True, unique=False)
|
||||
|
||||
@ -1103,6 +1103,9 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
visible_tab.remove_rows(rows_to_delete)
|
||||
visible_tab.save_playlist(session)
|
||||
|
||||
# Disable sort undo
|
||||
self.sort_undo = None
|
||||
|
||||
# Update destination playlist_tab if visible (if not visible, it
|
||||
# will be re-populated when it is opened)
|
||||
destination_playlist_tab = None
|
||||
|
||||
200
app/playlists.py
200
app/playlists.py
@ -8,7 +8,7 @@ import obsws_python as obs # type: ignore
|
||||
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable, cast, List, Optional, TYPE_CHECKING, Union
|
||||
from typing import Any, Callable, cast, List, Optional, TYPE_CHECKING, Union
|
||||
|
||||
from PyQt6.QtCore import (
|
||||
QEvent,
|
||||
@ -92,7 +92,10 @@ class EscapeDelegate(QStyledItemDelegate):
|
||||
super().__init__(parent)
|
||||
|
||||
def createEditor(
|
||||
self, parent: Optional[QWidget], option: QStyleOptionViewItem, index: QModelIndex
|
||||
self,
|
||||
parent: Optional[QWidget],
|
||||
option: QStyleOptionViewItem,
|
||||
index: QModelIndex,
|
||||
):
|
||||
"""
|
||||
Intercept createEditor call and make row just a little bit taller
|
||||
@ -137,6 +140,7 @@ class PlaylistTab(QTableWidget):
|
||||
PLAYLISTROW_ID = Qt.ItemDataRole.UserRole + 2
|
||||
TRACK_PATH = Qt.ItemDataRole.UserRole + 3
|
||||
PLAYED = Qt.ItemDataRole.UserRole + 4
|
||||
ROW_LAST_PLAYED = Qt.ItemDataRole.UserRole + 5
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -195,8 +199,8 @@ class PlaylistTab(QTableWidget):
|
||||
self.viewport().installEventFilter(self)
|
||||
|
||||
self.search_text: str = ""
|
||||
self.sort_undo: List[int] = []
|
||||
self.edit_cell_type: Optional[int]
|
||||
self.selecting_in_progress = False
|
||||
|
||||
# Connect signals
|
||||
self.horizontalHeader().sectionResized.connect(self._column_resize)
|
||||
@ -262,6 +266,8 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
# Reset drag mode to allow row selection by dragging
|
||||
self.setDragEnabled(False)
|
||||
# Disable sort undo
|
||||
self.sort_undo = None
|
||||
|
||||
with Session() as session:
|
||||
self.save_playlist(session)
|
||||
@ -1100,6 +1106,7 @@ class PlaylistTab(QTableWidget):
|
||||
)
|
||||
if sort_menu:
|
||||
sort_menu.setEnabled(self._sortable())
|
||||
self._add_context_menu("Undo sort", self._sort_undo, self.sort_undo is None)
|
||||
|
||||
# Build submenu
|
||||
|
||||
@ -1318,6 +1325,11 @@ class PlaylistTab(QTableWidget):
|
||||
else:
|
||||
return int(duration_udata)
|
||||
|
||||
def _get_row_last_played(self, row_number: int) -> Optional[datetime]:
|
||||
"""Return last played datetime associated with this row_number"""
|
||||
|
||||
return self._get_row_userdata(row_number, self.ROW_LAST_PLAYED)
|
||||
|
||||
def _get_row_note(self, row_number: int) -> str:
|
||||
"""Return note on this row_number or null string if none"""
|
||||
|
||||
@ -1402,9 +1414,7 @@ class PlaylistTab(QTableWidget):
|
||||
else:
|
||||
return str(path)
|
||||
|
||||
def _get_row_userdata(
|
||||
self, row_number: int, role: int
|
||||
) -> Optional[Union[str, int]]:
|
||||
def _get_row_userdata(self, row_number: int, role: int) -> Optional[Any]:
|
||||
"""
|
||||
Return the specified userdata, if any.
|
||||
"""
|
||||
@ -1663,6 +1673,25 @@ class PlaylistTab(QTableWidget):
|
||||
# Set track start/end times after track list is populated
|
||||
self._update_start_end_times(session)
|
||||
|
||||
def _reorder_rows(self, source_row_numbers: List[int]) -> None:
|
||||
"""
|
||||
Take the list of source row numbers and put those playlist rows in that order.
|
||||
|
||||
Algorithm: create new rows below the source rows and copy source rows in
|
||||
the correct order. When complete, delete source rows.
|
||||
"""
|
||||
|
||||
next_row = max(source_row_numbers) + 1
|
||||
for source_row_number in source_row_numbers:
|
||||
self.insertRow(next_row)
|
||||
for column in range(self.columnCount()):
|
||||
self.setItem(next_row, column, self.takeItem(source_row_number, column))
|
||||
next_row += 1
|
||||
|
||||
# Remove source rows
|
||||
for i in reversed(sorted(source_row_numbers)):
|
||||
self.removeRow(i)
|
||||
|
||||
def _rescan(self, row_number: int, track_id: int) -> None:
|
||||
"""Rescan track"""
|
||||
|
||||
@ -1689,6 +1718,43 @@ class PlaylistTab(QTableWidget):
|
||||
self._update_start_end_times(session)
|
||||
self.clear_selection()
|
||||
|
||||
def _reset_next(self, old_plrid: int, new_plrid: int) -> None:
|
||||
"""
|
||||
Called when set_next_track_signal signal received.
|
||||
|
||||
Actions required:
|
||||
- If old_plrid points to this playlist:
|
||||
- Remove existing next track
|
||||
- If new_plrid points to this playlist:
|
||||
- Set track as next
|
||||
- Display row as next track
|
||||
- Update start/stop times
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
# Get plrs
|
||||
old_plr = new_plr = None
|
||||
if old_plrid:
|
||||
old_plr = session.get(PlaylistRows, old_plrid)
|
||||
|
||||
# Unmark next track
|
||||
if old_plr and old_plr.playlist_id == self.playlist_id:
|
||||
self._set_row_colour_default(old_plr.plr_rownum)
|
||||
|
||||
# Mark next track
|
||||
if new_plrid:
|
||||
new_plr = session.get(PlaylistRows, new_plrid)
|
||||
if not new_plr:
|
||||
log.error(f"_reset_next({new_plrid=}): plr not found")
|
||||
return
|
||||
if new_plr.playlist_id == self.playlist_id:
|
||||
self._set_row_colour_next(new_plr.plr_rownum)
|
||||
|
||||
# Update start/stop times
|
||||
self._update_start_end_times(session)
|
||||
|
||||
self.clear_selection()
|
||||
|
||||
def _run_subprocess(self, args):
|
||||
"""Run args in subprocess"""
|
||||
|
||||
@ -1787,10 +1853,6 @@ class PlaylistTab(QTableWidget):
|
||||
If multiple rows are selected, display sum of durations in status bar.
|
||||
"""
|
||||
|
||||
# If we are in the process of selecting multiple tracks, no-op here
|
||||
if self.selecting_in_progress:
|
||||
return
|
||||
|
||||
selected_rows = self._get_selected_rows()
|
||||
# If no rows are selected, we have nothing to do
|
||||
if len(selected_rows) == 0:
|
||||
@ -1859,43 +1921,6 @@ class PlaylistTab(QTableWidget):
|
||||
|
||||
return item
|
||||
|
||||
def _reset_next(self, old_plrid: int, new_plrid: int) -> None:
|
||||
"""
|
||||
Called when set_next_track_signal signal received.
|
||||
|
||||
Actions required:
|
||||
- If old_plrid points to this playlist:
|
||||
- Remove existing next track
|
||||
- If new_plrid points to this playlist:
|
||||
- Set track as next
|
||||
- Display row as next track
|
||||
- Update start/stop times
|
||||
"""
|
||||
|
||||
with Session() as session:
|
||||
# Get plrs
|
||||
old_plr = new_plr = None
|
||||
if old_plrid:
|
||||
old_plr = session.get(PlaylistRows, old_plrid)
|
||||
|
||||
# Unmark next track
|
||||
if old_plr and old_plr.playlist_id == self.playlist_id:
|
||||
self._set_row_colour_default(old_plr.plr_rownum)
|
||||
|
||||
# Mark next track
|
||||
if new_plrid:
|
||||
new_plr = session.get(PlaylistRows, new_plrid)
|
||||
if not new_plr:
|
||||
log.error(f"_reset_next({new_plrid=}): plr not found")
|
||||
return
|
||||
if new_plr.playlist_id == self.playlist_id:
|
||||
self._set_row_colour_next(new_plr.plr_rownum)
|
||||
|
||||
# Update start/stop times
|
||||
self._update_start_end_times(session)
|
||||
|
||||
self.clear_selection()
|
||||
|
||||
def _set_played_row(self, session: scoped_session, row_number: int) -> None:
|
||||
"""Mark this row as played"""
|
||||
|
||||
@ -2063,13 +2088,15 @@ class PlaylistTab(QTableWidget):
|
||||
self._set_row_colour(row_number, note_colour)
|
||||
|
||||
def _set_row_last_played_time(
|
||||
self, row_number: int, last_played: Optional[datetime]
|
||||
self, row_number: int, last_played: datetime
|
||||
) -> QTableWidgetItem:
|
||||
"""Set row last played time"""
|
||||
"""Set row last played time. Also set in row metadata"""
|
||||
|
||||
last_played_str = get_relative_date(last_played)
|
||||
self._set_row_userdata(row_number, self.ROW_LAST_PLAYED, last_played)
|
||||
|
||||
return self._set_item_text(row_number, LASTPLAYED, last_played_str)
|
||||
return self._set_item_text(
|
||||
row_number, LASTPLAYED, get_relative_date(last_played)
|
||||
)
|
||||
|
||||
def _set_row_note_colour(self, session: scoped_session, row_number: int) -> None:
|
||||
"""
|
||||
@ -2202,7 +2229,7 @@ class PlaylistTab(QTableWidget):
|
||||
return self._set_row_userdata(row_number, self.TRACK_PATH, path)
|
||||
|
||||
def _set_row_userdata(
|
||||
self, row_number: int, role: int, value: Optional[Union[str, int]]
|
||||
self, row_number: int, role: int, value: Any
|
||||
) -> QTableWidgetItem:
|
||||
"""
|
||||
Set passed userdata in USERDATA column
|
||||
@ -2229,10 +2256,10 @@ class PlaylistTab(QTableWidget):
|
||||
if not selectionModel:
|
||||
return False
|
||||
source_rows = selectionModel.selectedRows()
|
||||
sorted_source_rows = sorted([a.row() for a in source_rows])
|
||||
if len(sorted_source_rows) < 2:
|
||||
if len(source_rows) < 2:
|
||||
return False
|
||||
|
||||
sorted_source_rows = sorted([a.row() for a in source_rows])
|
||||
if sorted_source_rows != list(
|
||||
range(min(sorted_source_rows), max(sorted_source_rows) + 1)
|
||||
):
|
||||
@ -2256,42 +2283,69 @@ class PlaylistTab(QTableWidget):
|
||||
- delete old rows
|
||||
"""
|
||||
|
||||
if not self._sortable():
|
||||
return
|
||||
|
||||
# Check selection is contiguous
|
||||
selectionModel = self.selectionModel()
|
||||
if not selectionModel:
|
||||
return
|
||||
source_rows = selectionModel.selectedRows()
|
||||
if not self._sortable():
|
||||
return
|
||||
|
||||
source_row_numbers = [a.row() for a in selectionModel.selectedRows()]
|
||||
# Copy (row-number, sort-field) to a list
|
||||
sorted_rows = []
|
||||
for index in source_rows:
|
||||
sort_item = self.item(index.row(), sort_column)
|
||||
sorted_rows: List[tuple[int, Any]] = []
|
||||
for row in source_row_numbers:
|
||||
if sort_column == DURATION:
|
||||
sorted_rows.append((row, self._get_row_duration(row)))
|
||||
elif sort_column == LASTPLAYED:
|
||||
sorted_rows.append((row, self._get_row_last_played(row)))
|
||||
else:
|
||||
sort_item = self.item(row, sort_column)
|
||||
if sort_item:
|
||||
sorted_rows.append((index.row(), sort_item.text()))
|
||||
sorted_rows.append((row, sort_item.text()))
|
||||
else:
|
||||
sorted_rows.append((row, None))
|
||||
|
||||
# Sort the list
|
||||
sorted_rows.sort(key=lambda row: row[1])
|
||||
if sort_column == LASTPLAYED:
|
||||
sorted_rows.reverse()
|
||||
|
||||
# Move rows
|
||||
source_row_numbers = [a[0] for a in sorted_rows]
|
||||
next_row = max(source_row_numbers) + 1
|
||||
for source_row_number in source_row_numbers:
|
||||
self.insertRow(next_row)
|
||||
for column in range(self.columnCount()):
|
||||
self.setItem(next_row, column, self.takeItem(source_row_number, column))
|
||||
next_row += 1
|
||||
# Reorder rows
|
||||
new_order = [a[0] for a in sorted_rows]
|
||||
self.sort_undo = [
|
||||
new_order.index(x) + min(new_order)
|
||||
for x in range(min(new_order), max(new_order) + 1)
|
||||
]
|
||||
self._reorder_rows(new_order)
|
||||
|
||||
# Remove source rows
|
||||
for i in reversed(source_rows):
|
||||
self.removeRow(i.row())
|
||||
# Reset drag mode to allow row selection by dragging
|
||||
self.setDragEnabled(False)
|
||||
|
||||
# Save playlist
|
||||
with Session() as session:
|
||||
self.save_playlist(session)
|
||||
self._update_start_end_times(session)
|
||||
|
||||
def _sort_undo(self):
|
||||
"""Undo last sort"""
|
||||
|
||||
if not self.sort_undo:
|
||||
return
|
||||
|
||||
new_order = self.sort_undo
|
||||
|
||||
self._reorder_rows(new_order)
|
||||
|
||||
self.sort_undo = [
|
||||
new_order.index(x) + min(new_order)
|
||||
for x in range(min(new_order), max(new_order) + 1)
|
||||
]
|
||||
|
||||
|
||||
# Reset drag mode to allow row selection by dragging
|
||||
self.setDragEnabled(False)
|
||||
|
||||
|
||||
def _track_time_between_rows(
|
||||
self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows
|
||||
) -> int:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user