WIP V3: context menu started
Sort by title implemented
This commit is contained in:
parent
1b4411d7e5
commit
d5871fe77f
@ -17,7 +17,13 @@ from PyQt6.QtGui import (
|
|||||||
from classes import track_sequence, MusicMusterSignals, PlaylistTrack
|
from classes import track_sequence, MusicMusterSignals, PlaylistTrack
|
||||||
from config import Config
|
from config import Config
|
||||||
from dbconfig import scoped_session, Session
|
from dbconfig import scoped_session, Session
|
||||||
from helpers import file_is_unreadable, get_embedded_time, ms_to_mmss
|
from helpers import (
|
||||||
|
file_is_unreadable,
|
||||||
|
get_embedded_time,
|
||||||
|
open_in_audacity,
|
||||||
|
ms_to_mmss,
|
||||||
|
set_track_metadata,
|
||||||
|
)
|
||||||
from log import log
|
from log import log
|
||||||
from models import Playdates, PlaylistRows, Tracks
|
from models import Playdates, PlaylistRows, Tracks
|
||||||
|
|
||||||
@ -57,6 +63,7 @@ class PlaylistRowData:
|
|||||||
self.plrid: int = plr.id
|
self.plrid: int = plr.id
|
||||||
self.plr_rownum: int = plr.plr_rownum
|
self.plr_rownum: int = plr.plr_rownum
|
||||||
self.note: str = plr.note
|
self.note: str = plr.note
|
||||||
|
self.track_id = plr.track_id
|
||||||
if plr.track:
|
if plr.track:
|
||||||
self.start_gap = plr.track.start_gap
|
self.start_gap = plr.track.start_gap
|
||||||
self.title = plr.track.title
|
self.title = plr.track.title
|
||||||
@ -328,6 +335,14 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Fall through to no-op
|
# Fall through to no-op
|
||||||
return QVariant()
|
return QVariant()
|
||||||
|
|
||||||
|
def delete_rows(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Delete passed rows from model
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
print(f"Delete rows {row_numbers=}")
|
||||||
|
|
||||||
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
|
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
|
||||||
"""
|
"""
|
||||||
Return text for display
|
Return text for display
|
||||||
@ -517,8 +532,9 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
section_end_time = track_sequence.now.end_time + timedelta(
|
section_end_time = track_sequence.now.end_time + timedelta(
|
||||||
milliseconds=duration
|
milliseconds=duration
|
||||||
)
|
)
|
||||||
end_time_str = ", section end time " + section_end_time.strftime(
|
end_time_str = (
|
||||||
Config.TRACK_TIME_FORMAT
|
", section end time "
|
||||||
|
+ section_end_time.strftime(Config.TRACK_TIME_FORMAT)
|
||||||
)
|
)
|
||||||
stripped_note = prd.note[:-1].strip()
|
stripped_note = prd.note[:-1].strip()
|
||||||
if stripped_note:
|
if stripped_note:
|
||||||
@ -547,6 +563,13 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return self.playlist_rows[row_number].path == ""
|
return self.playlist_rows[row_number].path == ""
|
||||||
|
|
||||||
|
def is_unplayed_row(self, row_number: int) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if row is an unplayed track row, else False
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.playlist_rows[row_number].played
|
||||||
|
|
||||||
def insert_header_row(self, row_number: Optional[int], text: str) -> None:
|
def insert_header_row(self, row_number: Optional[int], text: str) -> None:
|
||||||
"""
|
"""
|
||||||
Insert a header row.
|
Insert a header row.
|
||||||
@ -624,6 +647,14 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
for modified_row in modified_rows:
|
for modified_row in modified_rows:
|
||||||
self.invalidate_row(modified_row)
|
self.invalidate_row(modified_row)
|
||||||
|
|
||||||
|
def mark_unplayed(self, row_number: int) -> None:
|
||||||
|
"""
|
||||||
|
Mark row as unplayed
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.playlist_rows[row_number].played = False
|
||||||
|
self.invalidate_row(row_number)
|
||||||
|
|
||||||
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
Move the playlist rows given to to_row and below.
|
Move the playlist rows given to to_row and below.
|
||||||
@ -677,6 +708,15 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Update display
|
# Update display
|
||||||
self.invalidate_rows(list(row_map.keys()))
|
self.invalidate_rows(list(row_map.keys()))
|
||||||
|
|
||||||
|
def open_in_audacity(self, row_number: int) -> None:
|
||||||
|
"""
|
||||||
|
Open track at passed row number in Audacity
|
||||||
|
"""
|
||||||
|
|
||||||
|
path = self.playlist_rows[row_number].path
|
||||||
|
if path:
|
||||||
|
open_in_audacity(path)
|
||||||
|
|
||||||
def previous_track_ended(self) -> None:
|
def previous_track_ended(self) -> None:
|
||||||
"""
|
"""
|
||||||
Notification from musicmuster that the previous track has ended.
|
Notification from musicmuster that the previous track has ended.
|
||||||
@ -718,16 +758,68 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
p = PlaylistRows.deep_row(session, self.playlist_id, row_number)
|
p = PlaylistRows.deep_row(session, self.playlist_id, row_number)
|
||||||
self.playlist_rows[row_number] = PlaylistRowData(p)
|
self.playlist_rows[row_number] = PlaylistRowData(p)
|
||||||
|
|
||||||
|
def remove_track(self, row_number: int) -> None:
|
||||||
|
"""
|
||||||
|
Remove track from row
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
print(f"remove_track({row_number=})")
|
||||||
|
|
||||||
|
def rescan_track(self, row_number: int) -> None:
|
||||||
|
"""
|
||||||
|
Rescan track at passed row number
|
||||||
|
"""
|
||||||
|
|
||||||
|
track_id = self.playlist_rows[row_number].track_id
|
||||||
|
if track_id:
|
||||||
|
with Session() as session:
|
||||||
|
track = session.get(Tracks, track_id)
|
||||||
|
set_track_metadata(track)
|
||||||
|
self.refresh_row(session, row_number)
|
||||||
|
self.invalidate_row(row_number)
|
||||||
|
|
||||||
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 set_next_row(self, row_number: int) -> None:
|
def selection_is_sortable(self, row_numbers: List[int]) -> bool:
|
||||||
"""
|
"""
|
||||||
Set row_number as next track
|
Return True if the selection is sortable. That means:
|
||||||
|
- at least two rows selected
|
||||||
|
- selected rows are contiguous
|
||||||
|
- selected rows do not include any header rows
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# at least two rows selected
|
||||||
|
if len(row_numbers) < 2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# selected rows are contiguous
|
||||||
|
if sorted(row_numbers) != list(range(min(row_numbers), max(row_numbers) + 1)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# selected rows do not include any header rows
|
||||||
|
for row_number in row_numbers:
|
||||||
|
if self.is_header_row(row_number):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_next_row(self, row_number: Optional[int]) -> None:
|
||||||
|
"""
|
||||||
|
Set row_number as next track. If row_number is None, clear next track.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if row_number is None:
|
||||||
|
next_row_was = track_sequence.next.plr_rownum
|
||||||
|
if next_row_was is None:
|
||||||
|
return
|
||||||
|
track_sequence.next = PlaylistTrack()
|
||||||
|
self.invalidate_row(next_row_was)
|
||||||
|
return
|
||||||
|
|
||||||
# Update playing_trtack
|
# Update playing_trtack
|
||||||
with Session() as session:
|
with Session() as session:
|
||||||
track_sequence.next = PlaylistTrack()
|
track_sequence.next = PlaylistTrack()
|
||||||
@ -800,6 +892,40 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def sort_by_artist(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Sort selected rows by artist
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sort_by_duration(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Sort selected rows by duration
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Sort selected rows by lastplayed
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sort_by_title(self, row_numbers: List[int]) -> None:
|
||||||
|
"""
|
||||||
|
Sort selected rows by title
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a subset of playlist_rows with the rows we are
|
||||||
|
# interested in
|
||||||
|
shortlist_rows = {k: self.playlist_rows[k] for k in row_numbers}
|
||||||
|
sorted_list = [
|
||||||
|
k for k, v in sorted(shortlist_rows.items(), key=lambda item: item[1].title)
|
||||||
|
]
|
||||||
|
self.move_rows(sorted_list, min(sorted_list))
|
||||||
|
|
||||||
def supportedDropActions(self) -> Qt.DropAction:
|
def supportedDropActions(self) -> Qt.DropAction:
|
||||||
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
|
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
|
||||||
|
|
||||||
|
|||||||
210
app/playlists.py
210
app/playlists.py
@ -37,7 +37,7 @@ from PyQt6.QtWidgets import (
|
|||||||
|
|
||||||
from dbconfig import Session, scoped_session
|
from dbconfig import Session, scoped_session
|
||||||
from dialogs import TrackSelectDialog
|
from dialogs import TrackSelectDialog
|
||||||
from classes import MusicMusterSignals
|
from classes import MusicMusterSignals, track_sequence
|
||||||
from config import Config
|
from config import Config
|
||||||
from helpers import (
|
from helpers import (
|
||||||
ask_yes_no,
|
ask_yes_no,
|
||||||
@ -1058,114 +1058,94 @@ class PlaylistTab(QTableView):
|
|||||||
"""Used to process context (right-click) menu, which is defined here"""
|
"""Used to process context (right-click) menu, which is defined here"""
|
||||||
|
|
||||||
self.menu.clear()
|
self.menu.clear()
|
||||||
row_number = item.row()
|
|
||||||
# track_id = self._get_row_track_id(row_number)
|
|
||||||
# track_row = bool(track_id)
|
|
||||||
header_row = False
|
|
||||||
model = cast(PlaylistModel, self.model())
|
model = cast(PlaylistModel, self.model())
|
||||||
if model:
|
if not model:
|
||||||
header_row = model.is_header_row(row_number)
|
return
|
||||||
# current = row_number == self._get_current_track_row_number()
|
|
||||||
# next_row = row_number == self._get_next_track_row_number()
|
|
||||||
|
|
||||||
# # Play with mplayer
|
row_number = item.row()
|
||||||
# if track_row and not current:
|
header_row = model.is_header_row(row_number)
|
||||||
# self._add_context_menu(
|
track_row = not header_row
|
||||||
# "Play with mplayer", lambda: self._mplayer_play(row_number)
|
current_row = row_number == track_sequence.now.plr_rownum
|
||||||
# )
|
next_row = row_number == track_sequence.next.plr_rownum
|
||||||
|
|
||||||
# # Paste
|
# Open in Audacity
|
||||||
# self._add_context_menu(
|
if track_row and not current_row:
|
||||||
# "Paste",
|
self._add_context_menu(
|
||||||
# lambda: self.musicmuster.paste_rows(),
|
"Open in Audacity", lambda: model.open_in_audacity(row_number)
|
||||||
# self.musicmuster.selected_plrs is None,
|
)
|
||||||
# )
|
|
||||||
|
|
||||||
# # Open in Audacity
|
# Rescan
|
||||||
# if track_row and not current:
|
if track_row and not current_row:
|
||||||
# self._add_context_menu(
|
self._add_context_menu("Rescan track", lambda: self._rescan(row_number))
|
||||||
# "Open in Audacity", lambda: self._open_in_audacity(row_number)
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Rescan
|
# ----------------------
|
||||||
# if track_row and not current:
|
|
||||||
# self._add_context_menu(
|
|
||||||
# "Rescan track", lambda: self._rescan(row_number, track_id)
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # ----------------------
|
|
||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
|
|
||||||
# # Remove row
|
# Delete row
|
||||||
# if not current and not next_row:
|
if not current_row and not next_row:
|
||||||
# self._add_context_menu("Delete row", self._delete_rows)
|
self._add_context_menu(
|
||||||
|
"Delete row", lambda: model.delete_rows(self._get_selected_rows())
|
||||||
|
)
|
||||||
|
|
||||||
# # Move to playlist
|
# Remove track from row
|
||||||
# if not current and not next_row:
|
if track_row and not current_row and not next_row:
|
||||||
# self._add_context_menu(
|
self._add_context_menu(
|
||||||
# "Move to playlist...", self.musicmuster.move_selected
|
"Remove track from row", lambda: model.remove_track(row_number)
|
||||||
# )
|
)
|
||||||
|
|
||||||
# # ----------------------
|
|
||||||
# self.menu.addSeparator()
|
|
||||||
|
|
||||||
# # Remove track from row
|
|
||||||
# if track_row and not current and not next_row:
|
|
||||||
# self._add_context_menu(
|
|
||||||
# "Remove track from row", lambda: self._remove_track(row_number)
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Add track to section header (ie, make this a track row)
|
# Add track to section header (ie, make this a track row)
|
||||||
|
# TODO
|
||||||
if header_row:
|
if header_row:
|
||||||
self._add_context_menu("Add a track", lambda: self._add_track(row_number))
|
self._add_context_menu("Add a track", lambda: print("Add a track"))
|
||||||
|
|
||||||
# # Mark unplayed
|
|
||||||
# if self._get_row_userdata(row_number, self.PLAYED):
|
|
||||||
# self._add_context_menu("Mark unplayed", self._mark_unplayed)
|
|
||||||
|
|
||||||
# # Unmark as next
|
|
||||||
# if next_row:
|
|
||||||
# self._add_context_menu("Unmark as next track", self.clear_next)
|
|
||||||
|
|
||||||
# # ----------------------
|
# # ----------------------
|
||||||
self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
|
|
||||||
# # Sort
|
# Mark unplayed
|
||||||
# sort_menu = self.menu.addMenu("Sort")
|
if track_row and model.is_unplayed_row(row_number):
|
||||||
# self._add_context_menu(
|
self._add_context_menu("Mark unplayed", lambda: model.mark_unplayed(row_number))
|
||||||
# "by title", lambda: self._sort_selection(TITLE), parent_menu=sort_menu
|
|
||||||
# )
|
|
||||||
# self._add_context_menu(
|
|
||||||
# "by artist", lambda: self._sort_selection(ARTIST), parent_menu=sort_menu
|
|
||||||
# )
|
|
||||||
# self._add_context_menu(
|
|
||||||
# "by duration", lambda: self._sort_selection(DURATION), parent_menu=sort_menu
|
|
||||||
# )
|
|
||||||
# self._add_context_menu(
|
|
||||||
# "by last played",
|
|
||||||
# lambda: self._sort_selection(LASTPLAYED),
|
|
||||||
# parent_menu=sort_menu,
|
|
||||||
# )
|
|
||||||
# if sort_menu:
|
|
||||||
# sort_menu.setEnabled(self._sortable())
|
|
||||||
# self._add_context_menu("Undo sort", self._sort_undo, not bool(self.sort_undo))
|
|
||||||
|
|
||||||
# # Build submenu
|
# Unmark as next
|
||||||
|
if next_row:
|
||||||
|
self._add_context_menu("Unmark as next track", lambda: model.set_next_row(None))
|
||||||
|
|
||||||
# # ----------------------
|
# ----------------------
|
||||||
# self.menu.addSeparator()
|
self.menu.addSeparator()
|
||||||
|
|
||||||
# # Info
|
# Sort
|
||||||
# if track_row:
|
sort_menu = self.menu.addMenu("Sort")
|
||||||
# self._add_context_menu("Info", lambda: self._info_row(track_id))
|
self._add_context_menu(
|
||||||
|
"by title",
|
||||||
|
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()),
|
||||||
|
parent_menu=sort_menu,
|
||||||
|
)
|
||||||
|
self._add_context_menu(
|
||||||
|
"by duration",
|
||||||
|
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()),
|
||||||
|
parent_menu=sort_menu,
|
||||||
|
)
|
||||||
|
if sort_menu:
|
||||||
|
sort_menu.setEnabled(model.selection_is_sortable(self._get_selected_rows()))
|
||||||
|
self._add_context_menu("Undo sort", self._sort_undo, not bool(self.sort_undo))
|
||||||
|
|
||||||
# # Track path
|
# Info TODO
|
||||||
# if track_row:
|
if track_row:
|
||||||
# self._add_context_menu(
|
self._add_context_menu("Info", lambda: print("Track info"))
|
||||||
# "Copy track path", lambda: self._copy_path(row_number)
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # return super(PlaylistTab, self).eventFilter(source, event)
|
# Track path TODO
|
||||||
|
if track_row:
|
||||||
|
self._add_context_menu(
|
||||||
|
"Copy track path", lambda: print("Track path"))
|
||||||
|
|
||||||
def _calculate_end_time(
|
def _calculate_end_time(
|
||||||
self, start: Optional[datetime], duration: int
|
self, start: Optional[datetime], duration: int
|
||||||
@ -1739,30 +1719,11 @@ class PlaylistTab(QTableView):
|
|||||||
for i in reversed(sorted(source_row_numbers)):
|
for i in reversed(sorted(source_row_numbers)):
|
||||||
self.removeRow(i)
|
self.removeRow(i)
|
||||||
|
|
||||||
def _rescan(self, row_number: int, track_id: int) -> None:
|
def _rescan(self, row_number: int) -> None:
|
||||||
"""Rescan track"""
|
"""Rescan track"""
|
||||||
|
|
||||||
with Session() as session:
|
model = cast(PlaylistModel, self.model())
|
||||||
track = session.get(Tracks, track_id)
|
model.rescan_track(row_number)
|
||||||
if track:
|
|
||||||
if file_is_unreadable(track.path):
|
|
||||||
self._set_row_colour_unreadable(row_number)
|
|
||||||
else:
|
|
||||||
self._set_row_colour_default(row_number)
|
|
||||||
set_track_metadata(track)
|
|
||||||
self._update_row_track_info(session, row_number, track)
|
|
||||||
else:
|
|
||||||
_ = self._set_row_track_id(row_number, 0)
|
|
||||||
note_text = self._get_row_note(row_number)
|
|
||||||
if note_text is None:
|
|
||||||
note_text = ""
|
|
||||||
else:
|
|
||||||
note_text += f"{track_id=} not found"
|
|
||||||
self._set_row_header_text(session, row_number, note_text)
|
|
||||||
log.error(f"playlists._rescan({track_id=}): " "Track not found")
|
|
||||||
self._set_row_colour_unreadable(row_number)
|
|
||||||
|
|
||||||
self._update_start_end_times(session)
|
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def _reset_next(self, old_plrid: int, new_plrid: int) -> None:
|
def _reset_next(self, old_plrid: int, new_plrid: int) -> None:
|
||||||
@ -2299,33 +2260,6 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def _sortable(self) -> bool:
|
|
||||||
"""
|
|
||||||
Return True if the selection is sortable. That means:
|
|
||||||
- at least two rows selected
|
|
||||||
- selected rows are contiguous
|
|
||||||
- selected rows do not include any header rows
|
|
||||||
"""
|
|
||||||
|
|
||||||
selectionModel = self.selectionModel()
|
|
||||||
if not selectionModel:
|
|
||||||
return False
|
|
||||||
source_rows = selectionModel.selectedRows()
|
|
||||||
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)
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
|
|
||||||
for row in sorted_source_rows:
|
|
||||||
if self._get_row_track_id(row) == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _sort_selection(self, sort_column: int) -> None:
|
def _sort_selection(self, sort_column: int) -> None:
|
||||||
"""
|
"""
|
||||||
Algorithm:
|
Algorithm:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user