Compare commits

...

5 Commits

Author SHA1 Message Date
Keith Edmunds
4eaab98745 WIP: progressing no sessions in app files 2025-04-04 16:19:17 +01:00
Keith Edmunds
2fce0b34be WIP: clean up imports 2025-04-04 16:17:51 +01:00
Keith Edmunds
ed7ac0758c No more sessions in playlists!
Save/restore playlist column widths.
2025-03-30 13:55:17 +01:00
Keith Edmunds
098ce7198e Remove stack dump from ApplicationError dialog 2025-03-30 13:54:10 +01:00
Keith Edmunds
38b166737b WIP: add track to header
Logic works; playlistrow.py doesn't yet update database but prints a
message saying db needs to be updated.
2025-03-30 13:30:04 +01:00
10 changed files with 183 additions and 252 deletions

View File

@ -194,7 +194,7 @@ class MusicMusterSignals(QObject):
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)
signal_add_track_to_header = pyqtSignal(int, int) signal_add_track_to_header = pyqtSignal(InsertTrack)
signal_begin_insert_rows = pyqtSignal(InsertRows) signal_begin_insert_rows = pyqtSignal(InsertRows)
signal_end_insert_rows = pyqtSignal(int) signal_end_insert_rows = pyqtSignal(int)
signal_insert_track = pyqtSignal(InsertTrack) signal_insert_track = pyqtSignal(InsertTrack)

View File

@ -21,20 +21,14 @@ from PyQt6.QtWidgets import (
) )
# Third party imports # Third party imports
from sqlalchemy.orm.session import Session
# App imports # App imports
from classes import ApplicationError, InsertTrack, MusicMusterSignals from classes import ApplicationError, InsertTrack, MusicMusterSignals
from helpers import ( from helpers import (
ask_yes_no,
get_relative_date, get_relative_date,
ms_to_mmss, ms_to_mmss,
) )
from log import log
from models import Settings, Tracks
from playlistmodel import PlaylistModel
import repository import repository
from ui import dlg_TrackSelect_ui
class TrackInsertDialog(QDialog): class TrackInsertDialog(QDialog):
@ -111,6 +105,8 @@ class TrackInsertDialog(QDialog):
# height = record.f_int or 600 # height = record.f_int or 600
# self.resize(width, height) # self.resize(width, height)
self.signals = MusicMusterSignals()
def update_list(self, text: str) -> None: def update_list(self, text: str) -> None:
self.track_list.clear() self.track_list.clear()
if text.strip() == "": if text.strip() == "":
@ -145,7 +141,6 @@ class TrackInsertDialog(QDialog):
return return
insert_track_data = InsertTrack(self.playlist_id, track_id, note_text) insert_track_data = InsertTrack(self.playlist_id, track_id, note_text)
self.signals.signal_insert_track.emit(insert_track_data)
self.title_edit.clear() self.title_edit.clear()
self.note_edit.clear() self.note_edit.clear()
@ -153,17 +148,13 @@ class TrackInsertDialog(QDialog):
self.title_edit.setFocus() self.title_edit.setFocus()
if self.add_to_header: if self.add_to_header:
self.signals.signal_add_track_to_header.emit(insert_track_data)
self.accept() self.accept()
else:
self.signals.signal_insert_track.emit(insert_track_data)
def add_and_close_clicked(self): def add_and_close_clicked(self):
track_id = self.get_selected_track_id() self.add_clicked()
if track_id is not None:
note_text = self.note_edit.text()
insert_track_data = InsertTrack(
playlist_id=self.playlist_id, track_id=self.track_id, note=self.note
)
insert_track_data = InsertTrack(self.playlist_id, track_id, note_text)
self.signals.signal_insert_track.emit(insert_track_data)
self.accept() self.accept()
def selection_changed(self) -> None: def selection_changed(self) -> None:

View File

@ -4,7 +4,6 @@ from dataclasses import dataclass, field
from fuzzywuzzy import fuzz # type: ignore from fuzzywuzzy import fuzz # type: ignore
import os.path import os.path
import threading import threading
from typing import Optional, Sequence
import os import os
import shutil import shutil
@ -37,13 +36,15 @@ from classes import (
from config import Config from config import Config
from helpers import ( from helpers import (
file_is_unreadable, file_is_unreadable,
get_audio_metadata,
get_tags, get_tags,
normalise_track,
show_OK, show_OK,
) )
from log import log from log import log
from playlistrow import TrackSequence from playlistrow import TrackSequence
from playlistmodel import PlaylistModel from playlistmodel import PlaylistModel
import helpers import repository
@dataclass @dataclass
@ -67,7 +68,7 @@ class TrackFileData:
destination_path: str = "" destination_path: str = ""
import_this_file: bool = False import_this_file: bool = False
error: str = "" error: str = ""
file_path_to_remove: Optional[str] = None file_path_to_remove: str | None = None
track_id: int = 0 track_id: int = 0
track_match_data: list[TrackMatchData] = field(default_factory=list) track_match_data: list[TrackMatchData] = field(default_factory=list)
@ -250,7 +251,7 @@ class FileImporter:
artist_match=artist_score, artist_match=artist_score,
title=existing_track.title, title=existing_track.title,
title_match=title_score, title_match=title_score,
track_id=existing_track.id, track_id=existing_track.track_id,
) )
) )
@ -383,12 +384,12 @@ class FileImporter:
else: else:
tfd.destination_path = existing_track_path tfd.destination_path = existing_track_path
def _get_existing_track(self, track_id: int) -> Tracks: def _get_existing_track(self, track_id: int) -> TrackDTO:
""" """
Lookup in existing track in the local cache and return it Lookup in existing track in the local cache and return it
""" """
existing_track_records = [a for a in self.existing_tracks if a.id == track_id] existing_track_records = [a for a in self.existing_tracks if a.track_id == track_id]
if len(existing_track_records) != 1: if len(existing_track_records) != 1:
raise ApplicationError( raise ApplicationError(
f"Internal error in _get_existing_track: {existing_track_records=}" f"Internal error in _get_existing_track: {existing_track_records=}"
@ -461,13 +462,12 @@ class FileImporter:
# file). Check that because the path field in the database is # file). Check that because the path field in the database is
# unique and so adding a duplicate will give a db integrity # unique and so adding a duplicate will give a db integrity
# error. # error.
with db.Session() as session: if repository.track_with_path(tfd.destination_path):
if Tracks.get_by_path(session, tfd.destination_path): tfd.error = (
tfd.error = ( "Importing a new track but destination path already exists "
"Importing a new track but destination path already exists " f"in database ({tfd.destination_path})"
f"in database ({tfd.destination_path})" )
) return False
return False
# Check track_id # Check track_id
if tfd.track_id < 0: if tfd.track_id < 0:
@ -589,7 +589,7 @@ class DoTrackImport(QThread):
tags: Tags, tags: Tags,
destination_path: str, destination_path: str,
track_id: int, track_id: int,
file_path_to_remove: Optional[str] = None, file_path_to_remove: str | None = None,
) -> None: ) -> None:
""" """
Save parameters Save parameters
@ -620,7 +620,7 @@ class DoTrackImport(QThread):
) )
# Get audio metadata in this thread rather than calling function to save interactive time # Get audio metadata in this thread rather than calling function to save interactive time
self.audio_metadata = helpers.get_audio_metadata(self.import_file_path) self.audio_metadata = get_audio_metadata(self.import_file_path)
# Remove old file if so requested # Remove old file if so requested
if self.file_path_to_remove and os.path.exists(self.file_path_to_remove): if self.file_path_to_remove and os.path.exists(self.file_path_to_remove):
@ -659,7 +659,7 @@ class DoTrackImport(QThread):
return return
session.commit() session.commit()
helpers.normalise_track(self.destination_track_path) normalise_track(self.destination_track_path)
self.signals.status_message_signal.emit( self.signals.status_message_signal.emit(
f"{os.path.basename(self.import_file_path)} imported", 10000 f"{os.path.basename(self.import_file_path)} imported", 10000

View File

@ -102,8 +102,8 @@ def handle_exception(exc_type, exc_value, exc_traceback):
print(stackprinter.format(exc_value, suppressed_paths=['/.venv'], style='darkbg')) print(stackprinter.format(exc_value, suppressed_paths=['/.venv'], style='darkbg'))
msg = stackprinter.format(exc_value) stack = stackprinter.format(exc_value)
log.error(msg) log.error(stack)
log.error(error_msg) log.error(error_msg)
print("Critical error:", error_msg) # Consider logging instead of print print("Critical error:", error_msg) # Consider logging instead of print
@ -114,7 +114,7 @@ def handle_exception(exc_type, exc_value, exc_traceback):
Config.ERRORS_TO, Config.ERRORS_TO,
Config.ERRORS_FROM, Config.ERRORS_FROM,
"Exception (log_uncaught_exceptions) from musicmuster", "Exception (log_uncaught_exceptions) from musicmuster",
msg, stack,
) )
if QApplication.instance() is not None: if QApplication.instance() is not None:
fname = os.path.split(exc_traceback.tb_frame.f_code.co_filename)[1] fname = os.path.split(exc_traceback.tb_frame.f_code.co_filename)[1]

View File

@ -2321,7 +2321,7 @@ class Window(QMainWindow):
track.intro = intro track.intro = intro
session.commit() session.commit()
self.preview_manager.set_intro(intro) self.preview_manager.set_intro(intro)
self.current.base_model.refresh_row(session, row_number) self.current.base_model.refresh_row(row_number)
roles = [ roles = [
Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.DisplayRole,
] ]

View File

@ -34,6 +34,7 @@ from classes import (
ApplicationError, ApplicationError,
Col, Col,
InsertRows, InsertRows,
InsertTrack,
MusicMusterSignals, MusicMusterSignals,
) )
from config import Config from config import Config
@ -144,16 +145,12 @@ class PlaylistModel(QAbstractTableModel):
return header_row return header_row
@log_call @log_call
def add_track_to_header( def add_track_to_header(self, track_details: InsertTrack) -> None:
self, playlist_id: int, track_id: int, note: str = ""
) -> None:
""" """
Handle signal_add_track_to_header Handle signal_add_track_to_header
""" """
log.debug(f"{self}: add_track_to_header({playlist_id=}, {track_id=}, {note=}") if track_details.playlist_id != self.playlist_id:
if playlist_id != self.playlist_id:
return return
if not self.selected_rows: if not self.selected_rows:
@ -173,8 +170,8 @@ class PlaylistModel(QAbstractTableModel):
return return
if selected_row.note: if selected_row.note:
selected_row.note += " " + note selected_row.note += " " + track_details.note
selected_row.track_id = track_id selected_row.track_id = track_details.track_id
# Update local copy # Update local copy
self.refresh_row(selected_row.row_number) self.refresh_row(selected_row.row_number)
@ -288,12 +285,7 @@ class PlaylistModel(QAbstractTableModel):
f"current_track_started() called with no track_id ({playlist_dto=})" f"current_track_started() called with no track_id ({playlist_dto=})"
) )
# TODO: ensure Playdates is updated repository.update_playdates(track_id)
# with db.Session() as session:
# # Update Playdates in database
# log.debug(f"{self}: update playdates {track_id=}")
# Playdates(session, track_id)
# session.commit()
# Mark track as played in playlist # Mark track as played in playlist
playlist_dto.played = True playlist_dto.played = True
@ -359,23 +351,23 @@ class PlaylistModel(QAbstractTableModel):
row = index.row() row = index.row()
column = index.column() column = index.column()
# rat for playlist row data as it's used a lot # plr for playlist row data as it's used a lot
rat = self.playlist_rows[row] plr = self.playlist_rows[row]
# These are ordered in approximately the frequency with which # These are ordered in approximately the frequency with which
# they are called # they are called
if role == Qt.ItemDataRole.BackgroundRole: if role == Qt.ItemDataRole.BackgroundRole:
return self._background_role(row, column, rat) return self._background_role(row, column, plr)
elif role == Qt.ItemDataRole.DisplayRole: elif role == Qt.ItemDataRole.DisplayRole:
return self._display_role(row, column, rat) return self._display_role(row, column, plr)
elif role == Qt.ItemDataRole.EditRole: elif role == Qt.ItemDataRole.EditRole:
return self._edit_role(row, column, rat) return self._edit_role(row, column, plr)
elif role == Qt.ItemDataRole.FontRole: elif role == Qt.ItemDataRole.FontRole:
return self._font_role(row, column, rat) return self._font_role(row, column, plr)
elif role == Qt.ItemDataRole.ForegroundRole: elif role == Qt.ItemDataRole.ForegroundRole:
return self._foreground_role(row, column, rat) return self._foreground_role(row, column, plr)
elif role == Qt.ItemDataRole.ToolTipRole: elif role == Qt.ItemDataRole.ToolTipRole:
return self._tooltip_role(row, column, rat) return self._tooltip_role(row, column, plr)
return QVariant() return QVariant()
@ -402,7 +394,7 @@ class PlaylistModel(QAbstractTableModel):
self.track_sequence.update() self.track_sequence.update()
self.update_track_times() self.update_track_times()
def _display_role(self, row: int, column: int, rat: PlaylistRow) -> str: def _display_role(self, row: int, column: int, plr: PlaylistRow) -> str:
""" """
Return text for display Return text for display
""" """
@ -420,42 +412,42 @@ class PlaylistModel(QAbstractTableModel):
if header_row: if header_row:
if column == HEADER_NOTES_COLUMN: if column == HEADER_NOTES_COLUMN:
header_text = self.header_text(rat) header_text = self.header_text(plr)
if not header_text: if not header_text:
return Config.SECTION_HEADER return Config.SECTION_HEADER
else: else:
formatted_header = self.header_text(rat) formatted_header = self.header_text(plr)
trimmed_header = self.remove_section_timer_markers(formatted_header) trimmed_header = self.remove_section_timer_markers(formatted_header)
return trimmed_header return trimmed_header
else: else:
return "" return ""
if column == Col.START_TIME.value: if column == Col.START_TIME.value:
start_time = rat.forecast_start_time start_time = plr.forecast_start_time
if start_time: if start_time:
return start_time.strftime(Config.TRACK_TIME_FORMAT) return start_time.strftime(Config.TRACK_TIME_FORMAT)
return "" return ""
if column == Col.END_TIME.value: if column == Col.END_TIME.value:
end_time = rat.forecast_end_time end_time = plr.forecast_end_time
if end_time: if end_time:
return end_time.strftime(Config.TRACK_TIME_FORMAT) return end_time.strftime(Config.TRACK_TIME_FORMAT)
return "" return ""
if column == Col.INTRO.value: if column == Col.INTRO.value:
if rat.intro: if plr.intro:
return f"{rat.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}" return f"{plr.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}"
else: else:
return "" return ""
dispatch_table: dict[int, str] = { dispatch_table: dict[int, str] = {
Col.ARTIST.value: rat.artist, Col.ARTIST.value: plr.artist,
Col.BITRATE.value: str(rat.bitrate), Col.BITRATE.value: str(plr.bitrate),
Col.DURATION.value: ms_to_mmss(rat.duration), Col.DURATION.value: ms_to_mmss(plr.duration),
Col.LAST_PLAYED.value: get_relative_date(rat.lastplayed), Col.LAST_PLAYED.value: get_relative_date(plr.lastplayed),
Col.NOTE.value: rat.note, Col.NOTE.value: plr.note,
Col.START_GAP.value: str(rat.start_gap), Col.START_GAP.value: str(plr.start_gap),
Col.TITLE.value: rat.title, Col.TITLE.value: plr.title,
} }
if column in dispatch_table: if column in dispatch_table:
return dispatch_table[column] return dispatch_table[column]
@ -470,13 +462,12 @@ class PlaylistModel(QAbstractTableModel):
if playlist_id != self.playlist_id: if playlist_id != self.playlist_id:
return return
with db.Session() as session: self.refresh_data()
self.refresh_data(session)
super().endResetModel() super().endResetModel()
self.track_sequence.update() self.track_sequence.update()
self.update_track_times() self.update_track_times()
def _edit_role(self, row: int, column: int, rat: PlaylistRow) -> str | int: def _edit_role(self, row: int, column: int, plr: PlaylistRow) -> str | int:
""" """
Return value for editing Return value for editing
""" """
@ -484,31 +475,25 @@ class PlaylistModel(QAbstractTableModel):
# If this is a header row and we're being asked for the # If this is a header row and we're being asked for the
# HEADER_NOTES_COLUMN, return the note value # HEADER_NOTES_COLUMN, return the note value
if self.is_header_row(row) and column == HEADER_NOTES_COLUMN: if self.is_header_row(row) and column == HEADER_NOTES_COLUMN:
return rat.note return plr.note
if column == Col.INTRO.value: if column == Col.INTRO.value:
return rat.intro or 0 return plr.intro or 0
if column == Col.TITLE.value: if column == Col.TITLE.value:
return rat.title return plr.title
if column == Col.ARTIST.value: if column == Col.ARTIST.value:
return rat.artist return plr.artist
if column == Col.NOTE.value: if column == Col.NOTE.value:
return rat.note return plr.note
return "" return ""
def _foreground_role(self, row: int, column: int, rat: PlaylistRow) -> QBrush: def _foreground_role(self, row: int, column: int, plr: PlaylistRow) -> QBrush:
"""Return header foreground colour or QBrush() if none""" """Return header foreground colour or QBrush() if none"""
if self.is_header_row(row): plr.row_fg = repository.get_colour(plr.note, foreground=True)
if rat.row_fg is None: if plr.row_fg:
with db.Session() as session: return QBrush(QColor(plr.row_fg))
rat.row_fg = NoteColours.get_colour(
session, rat.note, foreground=True
)
if rat.row_fg:
return QBrush(QColor(rat.row_fg))
return QBrush() return QBrush()
def flags(self, index: QModelIndex) -> Qt.ItemFlag: def flags(self, index: QModelIndex) -> Qt.ItemFlag:
@ -534,7 +519,7 @@ class PlaylistModel(QAbstractTableModel):
return default return default
def _font_role(self, row: int, column: int, rat: PlaylistRow) -> QFont: def _font_role(self, row: int, column: int, plr: PlaylistRow) -> QFont:
""" """
Return font Return font
""" """
@ -673,21 +658,21 @@ class PlaylistModel(QAbstractTableModel):
return QVariant() return QVariant()
def header_text(self, rat: PlaylistRow) -> str: def header_text(self, plr: PlaylistRow) -> str:
""" """
Process possible section timing directives embeded in header Process possible section timing directives embeded in header
""" """
if rat.note.endswith(Config.SECTION_STARTS): if plr.note.endswith(Config.SECTION_STARTS):
return self.start_of_timed_section_header(rat) return self.start_of_timed_section_header(plr)
elif rat.note.endswith("="): elif plr.note.endswith("="):
return self.section_subtotal_header(rat) return self.section_subtotal_header(plr)
elif rat.note == "-": elif plr.note == "-":
# If the hyphen is the only thing on the line, echo the note # If the hyphen is the only thing on the line, echo the note
# that started the section without the trailing "+". # that started the section without the trailing "+".
for row_number in range(rat.row_number - 1, -1, -1): for row_number in range(plr.row_number - 1, -1, -1):
row_rat = self.playlist_rows[row_number] row_rat = self.playlist_rows[row_number]
if self.is_header_row(row_number): if self.is_header_row(row_number):
if row_rat.note.endswith("-"): if row_rat.note.endswith("-"):
@ -697,7 +682,7 @@ class PlaylistModel(QAbstractTableModel):
return f"[End: {row_rat.note[:-1]}]" return f"[End: {row_rat.note[:-1]}]"
return "-" return "-"
return rat.note return plr.note
def hide_played_tracks(self, hide: bool) -> None: def hide_played_tracks(self, hide: bool) -> None:
""" """
@ -855,31 +840,6 @@ class PlaylistModel(QAbstractTableModel):
if to_row_number in from_rows: if to_row_number in from_rows:
return False # Destination within rows to be moved return False # Destination within rows to be moved
# # Remove rows from bottom to top to avoid index shifting
# for row in sorted(from_rows, reverse=True):
# self.beginRemoveRows(QModelIndex(), row, row)
# del self.playlist_rows[row]
# # At this point self.playlist_rows has been updated but the
# # underlying database has not (that's done below after
# # inserting the rows)
# self.endRemoveRows()
# # Adjust insertion point after removal
# if to_row_number > max(from_rows):
# rows_below_dest = len([r for r in from_rows if r < to_row_number])
# insertion_point = to_row_number - rows_below_dest
# else:
# insertion_point = to_row_number
# # Insert rows at the destination
# plrid_to_new_row_number: list[dict[int, int]] = []
# for offset, row_data in enumerate(rows_to_move):
# row_number = insertion_point + offset
# self.beginInsertRows(QModelIndex(), row_number, row_number)
# self.playlist_rows[row_number] = row_data
# plrid_to_new_row_number.append({row_data.playlistrow_id: row_number})
# self.endInsertRows()
# Notify model going to change # Notify model going to change
self.beginResetModel() self.beginResetModel()
# Update database # Update database
@ -887,25 +847,6 @@ class PlaylistModel(QAbstractTableModel):
# Notify model changed # Notify model changed
self.endResetModel() self.endResetModel()
# Handle the moves in row_group chunks
for row_group in row_groups:
# Tell model we will be moving rows
# See https://doc.qt.io/qt-6/qabstractitemmodel.html#beginMoveRows
# for how destination is calculated
destination = to_row_number
if to_row_number > max(row_group):
destination = to_row_number - max(row_group) + 1
super().beginMoveRows(QModelIndex(),
min(row_group),
max(row_group),
QModelIndex(),
destination
)
# Update database
repository.move_rows_within_playlist(self.playlist_id, row_group, to_row_number)
# Tell model we have finished moving rows
super().endMoveRows()
# Update display # Update display
self.refresh_data() self.refresh_data()
self.track_sequence.update() self.track_sequence.update()
@ -916,26 +857,7 @@ class PlaylistModel(QAbstractTableModel):
# Qt.ItemDataRole.DisplayRole, # Qt.ItemDataRole.DisplayRole,
# ] # ]
# self.invalidate_rows(list(row_map.keys()), roles) # self.invalidate_rows(list(row_map.keys()), roles)
return True
def begin_insert_rows(self, insert_rows: InsertRows) -> None:
"""
Prepare model to insert rows
"""
if insert_rows.playlist_id != self.playlist_id:
return
super().beginInsertRows(QModelIndex(), insert_rows.from_row, insert_rows.to_row)
def end_insert_rows(self, playlist_id: int) -> None:
"""
End insert rows
"""
if playlist_id != self.playlist_id:
return
super().endInsertRows()
@log_call @log_call
def move_rows_between_playlists( def move_rows_between_playlists(
@ -974,11 +896,10 @@ class PlaylistModel(QAbstractTableModel):
to_row_number + len(row_group) to_row_number + len(row_group)
) )
self.signals.signal_begin_insert_rows.emit(insert_rows) self.signals.signal_begin_insert_rows.emit(insert_rows)
repository.move_rows_to_playlist(from_rows=row_group, repository.move_rows(from_rows=row_group,
from_playlist_id=self.playlist_id, from_playlist_id=self.playlist_id,
to_row=to_row_number, to_row=to_row_number,
to_playlist_id=to_playlist_id to_playlist_id=to_playlist_id)
)
self.signals.signal_end_insert_rows.emit(to_playlist_id) self.signals.signal_end_insert_rows.emit(to_playlist_id)
super().endRemoveRows() super().endRemoveRows()
@ -986,6 +907,26 @@ class PlaylistModel(QAbstractTableModel):
self.track_sequence.update() self.track_sequence.update()
self.update_track_times() self.update_track_times()
def begin_insert_rows(self, insert_rows: InsertRows) -> None:
"""
Prepare model to insert rows
"""
if insert_rows.playlist_id != self.playlist_id:
return
super().beginInsertRows(QModelIndex(), insert_rows.from_row, insert_rows.to_row)
def end_insert_rows(self, playlist_id: int) -> None:
"""
End insert rows
"""
if playlist_id != self.playlist_id:
return
super().endInsertRows()
@log_call @log_call
def move_track_add_note( def move_track_add_note(
self, new_row_number: int, existing_plr: PlaylistRow, note: str self, new_row_number: int, existing_plr: PlaylistRow, note: str
@ -1007,23 +948,6 @@ class PlaylistModel(QAbstractTableModel):
self.move_rows([existing_plr.row_number], new_row_number) self.move_rows([existing_plr.row_number], new_row_number)
self.signals.resize_rows_signal.emit(self.playlist_id) self.signals.resize_rows_signal.emit(self.playlist_id)
@log_call
def move_track_to_header(
self,
header_row_number: int,
existing_rat: RowAndTrack,
note: Optional[str],
) -> None:
"""
Add the existing_rat track details to the existing header at header_row_number
"""
if existing_rat.track_id:
if note and existing_rat.note:
note += "\n" + existing_rat.note
self.add_track_to_header(header_row_number, existing_rat.track_id, note)
self.delete_rows([existing_rat.row_number])
@log_call @log_call
def obs_scene_change(self, row_number: int) -> None: def obs_scene_change(self, row_number: int) -> None:
""" """
@ -1170,17 +1094,7 @@ class PlaylistModel(QAbstractTableModel):
looking up the playlistrow_id and retrieving the row number from the database. looking up the playlistrow_id and retrieving the row number from the database.
""" """
# Check the track_sequence.next, current and previous plrs and self.track_sequence.update()
# update the row number
with db.Session() as session:
for ts in [
track_sequence.next,
track_sequence.current,
track_sequence.previous,
]:
if ts:
ts.update_playlist_and_row(session)
session.commit()
self.update_track_times() self.update_track_times()
@ -1302,7 +1216,7 @@ class PlaylistModel(QAbstractTableModel):
return len(self.playlist_rows) return len(self.playlist_rows)
def section_subtotal_header(self, rat: PlaylistRow) -> str: def section_subtotal_header(self, plr: PlaylistRow) -> str:
""" """
Process this row as subtotal within a timed section and Process this row as subtotal within a timed section and
return display text for this row return display text for this row
@ -1312,12 +1226,12 @@ class PlaylistModel(QAbstractTableModel):
unplayed_count: int = 0 unplayed_count: int = 0
duration: int = 0 duration: int = 0
if rat.row_number == 0: if plr.row_number == 0:
# Meaningless to have a subtotal on row 0 # Meaningless to have a subtotal on row 0
return Config.SUBTOTAL_ON_ROW_ZERO return Config.SUBTOTAL_ON_ROW_ZERO
# Show subtotal # Show subtotal
for row_number in range(rat.row_number - 1, -1, -1): for row_number in range(plr.row_number - 1, -1, -1):
row_rat = self.playlist_rows[row_number] row_rat = self.playlist_rows[row_number]
if self.is_header_row(row_number) or row_number == 0: if self.is_header_row(row_number) or row_number == 0:
if row_rat.note.endswith(Config.SECTION_STARTS) or row_number == 0: if row_rat.note.endswith(Config.SECTION_STARTS) or row_number == 0:
@ -1330,7 +1244,7 @@ class PlaylistModel(QAbstractTableModel):
and ( and (
row_number row_number
< self.track_sequence.current.row_number < self.track_sequence.current.row_number
< rat.row_number < plr.row_number
) )
): ):
section_end_time = ( section_end_time = (
@ -1341,7 +1255,7 @@ class PlaylistModel(QAbstractTableModel):
", section end time " ", section end time "
+ section_end_time.strftime(Config.TRACK_TIME_FORMAT) + section_end_time.strftime(Config.TRACK_TIME_FORMAT)
) )
clean_header = self.remove_section_timer_markers(rat.note) clean_header = self.remove_section_timer_markers(plr.note)
if clean_header: if clean_header:
return ( return (
f"{clean_header} [" f"{clean_header} ["
@ -1421,15 +1335,15 @@ class PlaylistModel(QAbstractTableModel):
) )
return return
rat = self.selected_rows[0] plr = self.selected_rows[0]
if rat.track_id is None: if plr.track_id is None:
raise ApplicationError(f"set_next_row: no track_id ({rat=})") raise ApplicationError(f"set_next_row: no track_id ({plr=})")
old_next_row: Optional[int] = None old_next_row: Optional[int] = None
if self.track_sequence.next: if self.track_sequence.next:
old_next_row = self.track_sequence.next.row_number old_next_row = self.track_sequence.next.row_number
self.track_sequence.set_next(rat) self.track_sequence.set_next(plr)
roles = [ roles = [
Qt.ItemDataRole.BackgroundRole, Qt.ItemDataRole.BackgroundRole,
@ -1438,7 +1352,7 @@ class PlaylistModel(QAbstractTableModel):
# only invalidate required roles # only invalidate required roles
self.invalidate_row(old_next_row, roles) self.invalidate_row(old_next_row, roles)
# only invalidate required roles # only invalidate required roles
self.invalidate_row(rat.row_number, roles) self.invalidate_row(plr.row_number, roles)
self.signals.next_track_changed_signal.emit() self.signals.next_track_changed_signal.emit()
self.update_track_times() self.update_track_times()
@ -1552,7 +1466,7 @@ class PlaylistModel(QAbstractTableModel):
self.sort_by_attribute(row_numbers, "title") self.sort_by_attribute(row_numbers, "title")
def start_of_timed_section_header(self, rat: PlaylistRow) -> str: def start_of_timed_section_header(self, plr: PlaylistRow) -> str:
""" """
Process this row as the start of a timed section and Process this row as the start of a timed section and
return display text for this row return display text for this row
@ -1562,9 +1476,9 @@ class PlaylistModel(QAbstractTableModel):
unplayed_count: int = 0 unplayed_count: int = 0
duration: int = 0 duration: int = 0
clean_header = self.remove_section_timer_markers(rat.note) clean_header = self.remove_section_timer_markers(plr.note)
for row_number in range(rat.row_number + 1, len(self.playlist_rows)): for row_number in range(plr.row_number + 1, len(self.playlist_rows)):
row_rat = self.playlist_rows[row_number] row_rat = self.playlist_rows[row_number]
if self.is_header_row(row_number): if self.is_header_row(row_number):
if row_rat.note.endswith(Config.SECTION_ENDINGS): if row_rat.note.endswith(Config.SECTION_ENDINGS):
@ -1588,7 +1502,7 @@ class PlaylistModel(QAbstractTableModel):
def supportedDropActions(self) -> Qt.DropAction: def supportedDropActions(self) -> Qt.DropAction:
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
def _tooltip_role(self, row: int, column: int, rat: PlaylistRow) -> str: def _tooltip_role(self, row: int, column: int, plr: PlaylistRow) -> str:
""" """
Return tooltip. Currently only used for last_played column. Return tooltip. Currently only used for last_played column.
""" """
@ -1659,20 +1573,20 @@ class PlaylistModel(QAbstractTableModel):
next_track_row = self.track_sequence.next.row_number next_track_row = self.track_sequence.next.row_number
for row_number in range(row_count): for row_number in range(row_count):
rat = self.playlist_rows[row_number] plr = self.playlist_rows[row_number]
# Don't update times for tracks that have been played, for # Don't update times for tracks that have been played, for
# unreadable tracks or for the current track, handled above. # unreadable tracks or for the current track, handled above.
if ( if (
rat.played plr.played
or row_number == current_track_row or row_number == current_track_row
or (rat.path and file_is_unreadable(rat.path)) or (plr.path and file_is_unreadable(plr.path))
): ):
continue continue
# Reset start time if timing in header # Reset start time if timing in header
if self.is_header_row(row_number): if self.is_header_row(row_number):
header_time = get_embedded_time(rat.note) header_time = get_embedded_time(plr.note)
if header_time: if header_time:
next_start_time = header_time next_start_time = header_time
continue continue
@ -1683,7 +1597,7 @@ class PlaylistModel(QAbstractTableModel):
and self.track_sequence.current and self.track_sequence.current
and self.track_sequence.current.end_time and self.track_sequence.current.end_time
): ):
next_start_time = rat.set_forecast_start_time( next_start_time = plr.set_forecast_start_time(
update_rows, self.track_sequence.current.end_time update_rows, self.track_sequence.current.end_time
) )
continue continue
@ -1691,11 +1605,11 @@ class PlaylistModel(QAbstractTableModel):
# If we're between the current and next row, zero out # If we're between the current and next row, zero out
# times # times
if (current_track_row or row_count) < row_number < (next_track_row or 0): if (current_track_row or row_count) < row_number < (next_track_row or 0):
rat.set_forecast_start_time(update_rows, None) plr.set_forecast_start_time(update_rows, None)
continue continue
# Set start/end # Set start/end
rat.forecast_start_time = next_start_time plr.forecast_start_time = next_start_time
# Update start/stop times of rows that have changed # Update start/stop times of rows that have changed
for updated_row in update_rows: for updated_row in update_rows:

View File

@ -110,13 +110,13 @@ class PlaylistRow:
Adding a track_id should only happen to a header row. Adding a track_id should only happen to a header row.
""" """
if self.track_id: if self.track_id > 0:
raise ApplicationError("Attempting to add track to row with existing track ({self=}") raise ApplicationError("Attempting to add track to row with existing track ({self=}")
# TODO: set up write access to track_id. Should only update if # TODO: set up write access to track_id. Should only update if
# track_id == 0. Need to update all other track fields at the # track_id == 0. Need to update all other track fields at the
# same time. # same time.
print("set track_id attribute for {self=}, {value=}") print(f"set track_id attribute for {self=}, {value=}")
pass pass
# Expose PlaylistRowDTO fields as properties # Expose PlaylistRowDTO fields as properties
@ -127,7 +127,7 @@ class PlaylistRow:
@note.setter @note.setter
def note(self, value: str) -> None: def note(self, value: str) -> None:
# TODO set up write access to db # TODO set up write access to db
print("set note attribute for {self=}, {value=}") print(f"set note attribute for {self=}, {value=}")
# self.dto.note = value # self.dto.note = value
@property @property
@ -137,7 +137,7 @@ class PlaylistRow:
@played.setter @played.setter
def played(self, value: bool = True) -> None: def played(self, value: bool = True) -> None:
# TODO set up write access to db # TODO set up write access to db
print("set played attribute for {self=}") print(f"set played attribute for {self=}")
# self.dto.played = value # self.dto.played = value
@property @property

View File

@ -35,7 +35,13 @@ from PyQt6.QtWidgets import (
# App imports # App imports
from audacity_controller import AudacityController from audacity_controller import AudacityController
from classes import ApplicationError, Col, MusicMusterSignals, PlaylistStyle, TrackInfo from classes import (
ApplicationError,
Col,
MusicMusterSignals,
PlaylistStyle,
TrackInfo
)
from config import Config from config import Config
from dialogs import TrackInsertDialog from dialogs import TrackInsertDialog
from helpers import ( from helpers import (
@ -48,6 +54,7 @@ from log import log, log_call
from models import db, Settings from models import db, Settings
from playlistrow import TrackSequence from playlistrow import TrackSequence
from playlistmodel import PlaylistModel, PlaylistProxyModel from playlistmodel import PlaylistModel, PlaylistProxyModel
import repository
if TYPE_CHECKING: if TYPE_CHECKING:
from musicmuster import Window from musicmuster import Window
@ -513,18 +520,12 @@ class PlaylistTab(QTableView):
def _add_track(self) -> None: def _add_track(self) -> None:
"""Add a track to a section header making it a normal track row""" """Add a track to a section header making it a normal track row"""
model_row_number = self.source_model_selected_row_number() dlg = TrackInsertDialog(
if model_row_number is None: parent=self.musicmuster,
return playlist_id=self.playlist_id,
add_to_header=True,
with db.Session() as session: )
dlg = TrackInsertDialog( dlg.exec()
parent=self.musicmuster,
playlist_id=self.playlist_id,
add_to_header=True,
)
dlg.exec()
session.commit()
def _build_context_menu(self, item: QTableWidgetItem) -> None: def _build_context_menu(self, item: QTableWidgetItem) -> None:
"""Used to process context (right-click) menu, which is defined here""" """Used to process context (right-click) menu, which is defined here"""
@ -685,11 +686,10 @@ class PlaylistTab(QTableView):
# Resize rows if necessary # Resize rows if necessary
self.resizeRowsToContents() self.resizeRowsToContents()
with db.Session() as session: # Save settings
attr_name = f"playlist_col_{column_number}_width" repository.set_setting(
record = Settings.get_setting(session, attr_name) f"playlist_col_{column_number}_width", self.columnWidth(column_number)
record.f_int = self.columnWidth(column_number) )
session.commit()
def _context_menu(self, pos): def _context_menu(self, pos):
"""Display right-click menu""" """Display right-click menu"""
@ -1063,14 +1063,13 @@ class PlaylistTab(QTableView):
return return
# Last column is set to stretch so ignore it here # Last column is set to stretch so ignore it here
with db.Session() as session: for column_number in range(header.count() - 1):
for column_number in range(header.count() - 1): attr_name = f"playlist_col_{column_number}_width"
attr_name = f"playlist_col_{column_number}_width" value = repository.get_setting(attr_name)
record = Settings.get_setting(session, attr_name) if value is not None:
if record.f_int is not None: self.setColumnWidth(column_number, value)
self.setColumnWidth(column_number, record.f_int) else:
else: self.setColumnWidth(column_number, Config.DEFAULT_COLUMN_WIDTH)
self.setColumnWidth(column_number, Config.DEFAULT_COLUMN_WIDTH)
def set_row_as_next_track(self) -> None: def set_row_as_next_track(self) -> None:
""" """

View File

@ -136,7 +136,7 @@ class QuerylistModel(QAbstractTableModel):
row = index.row() row = index.row()
column = index.column() column = index.column()
# rat for playlist row data as it's used a lot # plr for playlist row data as it's used a lot
qrow = self.querylist_rows[row] qrow = self.querylist_rows[row]
# Dispatch to role-specific functions # Dispatch to role-specific functions
@ -268,7 +268,7 @@ class QuerylistModel(QAbstractTableModel):
bottom_right = self.index(row, self.columnCount() - 1) bottom_right = self.index(row, self.columnCount() - 1)
self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.BackgroundRole]) self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.BackgroundRole])
def _tooltip_role(self, row: int, column: int, rat: PlaylistRow) -> str | QVariant: def _tooltip_role(self, row: int, column: int, plr: PlaylistRow) -> str | QVariant:
""" """
Return tooltip. Currently only used for last_played column. Return tooltip. Currently only used for last_played column.
""" """

View File

@ -238,6 +238,24 @@ def _tracks_where(where: BinaryExpression | ColumnElement[bool]) -> list[TrackDT
return results return results
def track_with_path(path: str) -> bool:
"""
Return True if a track with passed path exists, else False
"""
with db.Session() as session:
track = (
session.execute(
select(Tracks)
.where(Tracks.path == path)
)
.scalars()
.one_or_none()
)
return track is not None
def tracks_like_artist(filter_str: str) -> list[TrackDTO]: def tracks_like_artist(filter_str: str) -> list[TrackDTO]:
""" """
Return tracks where artist is like filter Return tracks where artist is like filter
@ -399,6 +417,15 @@ def move_rows(
_check_playlist_integrity(session, to_playlist_id, fix=False) _check_playlist_integrity(session, to_playlist_id, fix=False)
def update_playdates(track_id: int) -> None:
"""
Update playdates for passed track
"""
with db.Session() as session:
_ = Playdates(session, track_id)
def update_row_numbers( def update_row_numbers(
playlist_id: int, id_to_row_number: list[dict[int, int]] playlist_id: int, id_to_row_number: list[dict[int, int]]
) -> None: ) -> None:
@ -527,7 +554,7 @@ def get_playlist_row(playlistrow_id: int) -> PlaylistRowDTO | None:
def get_playlist_rows( def get_playlist_rows(
playlist_id: int, check_playlist_itegrity=True playlist_id: int, check_playlist_itegrity: bool = True
) -> list[PlaylistRowDTO]: ) -> list[PlaylistRowDTO]:
# Alias PlaydatesTable for subquery # Alias PlaydatesTable for subquery
LatestPlaydate = aliased(Playdates) LatestPlaydate = aliased(Playdates)
@ -724,7 +751,7 @@ def get_setting(name: str) -> int | None:
with db.Session() as session: with db.Session() as session:
record = session.execute( record = session.execute(
select(Settings).where(Settings.name == name) select(Settings).where(Settings.name == name)
).one_or_none() ).scalars().one_or_none()
if not record: if not record:
return None return None
@ -739,7 +766,7 @@ def set_setting(name: str, value: int) -> None:
with db.Session() as session: with db.Session() as session:
record = session.execute( record = session.execute(
select(Settings).where(Settings.name == name) select(Settings).where(Settings.name == name)
).one_or_none() ).scalars().one_or_none()
if not record: if not record:
record = Settings(session=session, name=name) record = Settings(session=session, name=name)
if not record: if not record: