Compare commits

..

No commits in common. "d5871fe77f9b7395bbae955b09fa7872c87101b8" and "0d2dad9f3c148b82b745148ce64304c46d13502f" have entirely different histories.

4 changed files with 155 additions and 217 deletions

View File

@ -96,6 +96,4 @@ class Config(object):
TRACK_TIME_FORMAT = "%H:%M:%S" TRACK_TIME_FORMAT = "%H:%M:%S"
VOLUME_VLC_DEFAULT = 75 VOLUME_VLC_DEFAULT = 75
VOLUME_VLC_DROP3db = 65 VOLUME_VLC_DROP3db = 65
WARNING_MS_BEFORE_FADE = 5500
WARNING_MS_BEFORE_SILENCE = 5500
WEB_ZOOM_FACTOR = 1.2 WEB_ZOOM_FACTOR = 1.2

View File

@ -1137,10 +1137,6 @@ class Window(QMainWindow, Ui_MainWindow):
if self.btnDrop3db.isChecked(): if self.btnDrop3db.isChecked():
self.btnDrop3db.setChecked(False) self.btnDrop3db.setChecked(False)
# Show closing volume graph
if track_sequence.now.fade_graph:
track_sequence.now.fade_graph.plot()
# Play (new) current track # Play (new) current track
if not track_sequence.now.path: if not track_sequence.now.path:
return return
@ -1161,6 +1157,10 @@ class Window(QMainWindow, Ui_MainWindow):
break break
sleep(0.1) sleep(0.1)
# Show closing volume graph
if track_sequence.now.fade_graph:
track_sequence.now.fade_graph.plot()
# Notify model # Notify model
self.active_model().current_track_started() self.active_model().current_track_started()
@ -1597,7 +1597,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT)) self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT))
# Update carts # Update carts
# self.cart_tick() self.cart_tick()
def tick_1000ms(self) -> None: def tick_1000ms(self) -> None:
""" """
@ -1643,7 +1643,7 @@ class Window(QMainWindow, Ui_MainWindow):
# If silent in the next 5 seconds, put warning colour on # If silent in the next 5 seconds, put warning colour on
# time to silence box and enable play controls # time to silence box and enable play controls
if time_to_silence <= Config.WARNING_MS_BEFORE_SILENCE: if time_to_silence <= 5500:
css_silence = f"background: {Config.COLOUR_ENDING_TIMER}" css_silence = f"background: {Config.COLOUR_ENDING_TIMER}"
if self.frame_silent.styleSheet() != css_silence: if self.frame_silent.styleSheet() != css_silence:
self.frame_silent.setStyleSheet(css_silence) self.frame_silent.setStyleSheet(css_silence)
@ -1655,7 +1655,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.frame_silent.setStyleSheet(css_fade) self.frame_silent.setStyleSheet(css_fade)
# Five seconds before fade starts, set warning colour on # Five seconds before fade starts, set warning colour on
# time to silence box and enable play controls # time to silence box and enable play controls
elif time_to_fade <= Config.WARNING_MS_BEFORE_FADE: elif time_to_fade <= 5500:
self.frame_fade.setStyleSheet( self.frame_fade.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}" f"background: {Config.COLOUR_WARNING_TIMER}"
) )

View File

@ -17,13 +17,7 @@ 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 ( from helpers import file_is_unreadable, get_embedded_time, ms_to_mmss
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
@ -63,7 +57,6 @@ 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
@ -335,14 +328,6 @@ 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
@ -532,9 +517,8 @@ 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 = ( end_time_str = ", section end time " + section_end_time.strftime(
", section end time " Config.TRACK_TIME_FORMAT
+ 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:
@ -563,13 +547,6 @@ 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.
@ -647,14 +624,6 @@ 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.
@ -708,15 +677,6 @@ 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.
@ -758,68 +718,16 @@ 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 selection_is_sortable(self, row_numbers: List[int]) -> bool: def set_next_row(self, row_number: int) -> None:
""" """
Return True if the selection is sortable. That means: Set row_number as next track
- 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()
@ -892,40 +800,6 @@ 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

View File

@ -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, track_sequence from classes import MusicMusterSignals
from config import Config from config import Config
from helpers import ( from helpers import (
ask_yes_no, ask_yes_no,
@ -1058,94 +1058,114 @@ 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()
model = cast(PlaylistModel, self.model())
if not model:
return
row_number = item.row() 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())
if model:
header_row = model.is_header_row(row_number) header_row = model.is_header_row(row_number)
track_row = not header_row # current = row_number == self._get_current_track_row_number()
current_row = row_number == track_sequence.now.plr_rownum # next_row = row_number == self._get_next_track_row_number()
next_row = row_number == track_sequence.next.plr_rownum
# Open in Audacity # # Play with mplayer
if track_row and not current_row: # if track_row and not current:
self._add_context_menu( # self._add_context_menu(
"Open in Audacity", lambda: model.open_in_audacity(row_number) # "Play with mplayer", lambda: self._mplayer_play(row_number)
) # )
# Rescan # # Paste
if track_row and not current_row: # self._add_context_menu(
self._add_context_menu("Rescan track", lambda: self._rescan(row_number)) # "Paste",
# lambda: self.musicmuster.paste_rows(),
# self.musicmuster.selected_plrs is None,
# )
# ---------------------- # # Open in Audacity
self.menu.addSeparator() # if track_row and not current:
# self._add_context_menu(
# "Open in Audacity", lambda: self._open_in_audacity(row_number)
# )
# Delete row # # Rescan
if not current_row and not next_row: # if track_row and not current:
self._add_context_menu( # self._add_context_menu(
"Delete row", lambda: model.delete_rows(self._get_selected_rows()) # "Rescan track", lambda: self._rescan(row_number, track_id)
) # )
# Remove track from row
if track_row and not current_row and not next_row:
self._add_context_menu(
"Remove track from row", lambda: model.remove_track(row_number)
)
# Add track to section header (ie, make this a track row)
# TODO
if header_row:
self._add_context_menu("Add a track", lambda: print("Add a track"))
# # ---------------------- # # ----------------------
self.menu.addSeparator() self.menu.addSeparator()
# Mark unplayed # # Remove row
if track_row and model.is_unplayed_row(row_number): # if not current and not next_row:
self._add_context_menu("Mark unplayed", lambda: model.mark_unplayed(row_number)) # self._add_context_menu("Delete row", self._delete_rows)
# Unmark as next # # Move to playlist
if next_row: # if not current and not next_row:
self._add_context_menu("Unmark as next track", lambda: model.set_next_row(None)) # self._add_context_menu(
# "Move to playlist...", self.musicmuster.move_selected
# )
# ---------------------- # # ----------------------
# 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)
if header_row:
self._add_context_menu("Add a track", lambda: self._add_track(row_number))
# # 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 # # Sort
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: self._sort_selection(TITLE), parent_menu=sort_menu
lambda: model.sort_by_title(self._get_selected_rows()), # )
parent_menu=sort_menu, # self._add_context_menu(
) # "by artist", lambda: self._sort_selection(ARTIST), parent_menu=sort_menu
self._add_context_menu( # )
"by artist", # self._add_context_menu(
lambda: model.sort_by_artist(self._get_selected_rows()), # "by duration", lambda: self._sort_selection(DURATION), parent_menu=sort_menu
parent_menu=sort_menu, # )
) # self._add_context_menu(
self._add_context_menu( # "by last played",
"by duration", # lambda: self._sort_selection(LASTPLAYED),
lambda: model.sort_by_duration(self._get_selected_rows()), # parent_menu=sort_menu,
parent_menu=sort_menu, # )
) # if sort_menu:
self._add_context_menu( # sort_menu.setEnabled(self._sortable())
"by last played", # self._add_context_menu("Undo sort", self._sort_undo, not bool(self.sort_undo))
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))
# Info TODO # # Build submenu
if track_row:
self._add_context_menu("Info", lambda: print("Track info"))
# Track path TODO # # ----------------------
if track_row: # self.menu.addSeparator()
self._add_context_menu(
"Copy track path", lambda: print("Track path")) # # Info
# if track_row:
# self._add_context_menu("Info", lambda: self._info_row(track_id))
# # Track path
# if track_row:
# self._add_context_menu(
# "Copy track path", lambda: self._copy_path(row_number)
# )
# # return super(PlaylistTab, self).eventFilter(source, event)
def _calculate_end_time( def _calculate_end_time(
self, start: Optional[datetime], duration: int self, start: Optional[datetime], duration: int
@ -1719,11 +1739,30 @@ 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) -> None: def _rescan(self, row_number: int, track_id: int) -> None:
"""Rescan track""" """Rescan track"""
model = cast(PlaylistModel, self.model()) with Session() as session:
model.rescan_track(row_number) track = session.get(Tracks, track_id)
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:
@ -2260,6 +2299,33 @@ 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: