diff --git a/app/playlistmodel.py b/app/playlistmodel.py
index b132c8a..a07f596 100644
--- a/app/playlistmodel.py
+++ b/app/playlistmodel.py
@@ -44,11 +44,8 @@ from helpers import (
get_embedded_time,
get_relative_date,
ms_to_mmss,
- remove_substring_case_insensitive,
- set_track_metadata,
)
from log import log, log_call
-from models import db, NoteColours, Playdates, PlaylistRows, Tracks
from playlistrow import PlaylistRow, TrackSequence
import repository
@@ -61,6 +58,9 @@ class PlaylistModel(QAbstractTableModel):
"""
The Playlist Model
+ Cache the database info in self.playlist_rows, a dictionary of
+ PlaylistRow objects indexed by row_number.
+
Update strategy: update the database and then refresh the
row-indexed cached copy (self.playlist_rows). Do not edit
self.playlist_rows directly because keeping it and the
@@ -145,7 +145,7 @@ class PlaylistModel(QAbstractTableModel):
return header_row
@log_call
- def add_track_to_header(self, track_details: InsertTrack) -> None:
+ def add_track_to_header(self, track_details: InsertTrack) -> None:
"""
Handle signal_add_track_to_header
"""
@@ -943,8 +943,6 @@ class PlaylistModel(QAbstractTableModel):
playlist_row.note = note
self.refresh_row(existing_plr.row_number)
- # Carry out the move outside of the session context to ensure
- # database updated with any note change
self.move_rows([existing_plr.row_number], new_row_number)
self.signals.resize_rows_signal.emit(self.playlist_id)
@@ -1067,21 +1065,16 @@ class PlaylistModel(QAbstractTableModel):
Rescan track at passed row number
"""
- track_id = self.playlist_rows[row_number].track_id
- if track_id:
- with db.Session() as session:
- track = session.get(Tracks, track_id)
- set_track_metadata(track)
- self.refresh_row(row_number)
- self.update_track_times()
- roles = [
- Qt.ItemDataRole.BackgroundRole,
- Qt.ItemDataRole.DisplayRole,
- ]
- # only invalidate required roles
- self.invalidate_row(row_number, roles)
- self.signals.resize_rows_signal.emit(self.playlist_id)
- session.commit()
+ track = self.playlist_rows[row_number]
+ _ = repository.update_track(track.path, track.track_id)
+
+ roles = [
+ Qt.ItemDataRole.BackgroundRole,
+ Qt.ItemDataRole.DisplayRole,
+ ]
+ # only invalidate required roles
+ self.invalidate_row(row_number, roles)
+ self.signals.resize_rows_signal.emit(self.playlist_id)
@log_call
def reset_track_sequence_row_numbers(self) -> None:
@@ -1113,21 +1106,8 @@ class PlaylistModel(QAbstractTableModel):
):
return
- with db.Session() as session:
- for row_number in row_numbers:
- playlist_row = session.get(
- PlaylistRows, self.playlist_rows[row_number].playlistrow_id
- )
- if playlist_row.track_id:
- playlist_row.note = ""
- # We can't use refresh_data() because its
- # optimisations mean it won't update comments in
- # self.playlist_rows
- # The "correct" approach would be to re-read from the
- # database but we optimise here by simply updating
- # self.playlist_rows directly.
- self.playlist_rows[row_number].note = ""
- session.commit()
+ repository.remove_comments(self.playlist_id, row_numbers)
+
# only invalidate required roles
roles = [
Qt.ItemDataRole.BackgroundRole,
@@ -1185,31 +1165,7 @@ class PlaylistModel(QAbstractTableModel):
header_text = header_text[0:-1]
# Parse passed header text and remove the first colour match string
- with db.Session() as session:
- for rec in NoteColours.get_all(session):
- if not rec.strip_substring:
- continue
- if rec.is_regex:
- flags = re.UNICODE
- if not rec.is_casesensitive:
- flags |= re.IGNORECASE
- p = re.compile(rec.substring, flags)
- if p.match(header_text):
- header_text = re.sub(p, "", header_text)
- break
- else:
- if rec.is_casesensitive:
- if rec.substring.lower() in header_text.lower():
- header_text = remove_substring_case_insensitive(
- header_text, rec.substring
- )
- break
- else:
- if rec.substring in header_text:
- header_text = header_text.replace(rec.substring, "")
- break
-
- return header_text
+ return repository.remove_colour_substring(header_text)
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
"""Standard function for view"""
@@ -1357,6 +1313,7 @@ class PlaylistModel(QAbstractTableModel):
self.signals.next_track_changed_signal.emit()
self.update_track_times()
+ @log_call
def setData(
self,
index: QModelIndex,
@@ -1372,46 +1329,23 @@ class PlaylistModel(QAbstractTableModel):
row_number = index.row()
column = index.column()
+ plr = self.playlist_rows[row_number]
- with db.Session() as session:
- playlist_row = session.get(
- PlaylistRows, self.playlist_rows[row_number].playlistrow_id
- )
- if not playlist_row:
- log.error(
- f"{self}: Error saving data: {row_number=}, {column=}, {value=}"
- )
- return False
+ if column == Col.TITLE.value:
+ plr.title = str(value)
+ elif column == Col.ARTIST.value:
+ plr.artist = str(value)
+ elif column == Col.INTRO.value:
+ plr.intro = int(round(float(value), 1) * 1000)
+ elif column == Col.NOTE.value:
+ plr.note = str(value)
+ else:
+ raise ApplicationError(f"setData called with unexpected column ({column=})")
- if playlist_row.track_id:
- if column in [Col.TITLE.value, Col.ARTIST.value, Col.INTRO.value]:
- track = session.get(Tracks, playlist_row.track_id)
- if not track:
- log.error(f"{self}: Error retreiving track: {playlist_row=}")
- return False
- if column == Col.TITLE.value:
- track.title = str(value)
- elif column == Col.ARTIST.value:
- track.artist = str(value)
- elif column == Col.INTRO.value:
- track.intro = int(round(float(value), 1) * 1000)
- else:
- log.error(f"{self}: Error updating track: {column=}, {value=}")
- return False
- elif column == Col.NOTE.value:
- playlist_row.note = str(value)
+ self.refresh_row(row_number)
+ self.dataChanged.emit(index, index, [Qt.ItemDataRole.DisplayRole, role])
- else:
- # This is a header row
- if column == HEADER_NOTES_COLUMN:
- playlist_row.note = str(value)
-
- # commit changes before refreshing data
- session.commit()
- self.refresh_row(row_number)
- self.dataChanged.emit(index, index, [Qt.ItemDataRole.DisplayRole, role])
-
- return True
+ return True
def sort_by_artist(self, row_numbers: list[int]) -> None:
"""
@@ -1509,17 +1443,12 @@ class PlaylistModel(QAbstractTableModel):
if column != Col.LAST_PLAYED.value:
return ""
- with db.Session() as session:
- track_id = self.playlist_rows[row].track_id
- if not track_id:
- return ""
- playdates = Playdates.last_playdates(session, track_id)
- return "
".join(
- [
- a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
- for a in playdates
- ]
- )
+
+ track_id = self.playlist_rows[row].track_id
+ if not track_id:
+ return ""
+
+ return repository.get_last_played_dates(track_id)
@log_call
def update_or_insert(self, track_id: int, row_number: int) -> None:
diff --git a/app/playlistrow.py b/app/playlistrow.py
index e52cf25..7118a67 100644
--- a/app/playlistrow.py
+++ b/app/playlistrow.py
@@ -64,6 +64,10 @@ class PlaylistRow:
def artist(self):
return self.dto.artist
+ @artist.setter
+ def artist(self, value: str) -> None:
+ print(f"set artist attribute for {self=}, {value=}")
+
@property
def bitrate(self):
return self.dto.bitrate
@@ -80,6 +84,10 @@ class PlaylistRow:
def intro(self):
return self.dto.intro
+ @intro.setter
+ def intro(self, value: int) -> None:
+ print(f"set intro attribute for {self=}, {value=}")
+
@property
def lastplayed(self):
return self.dto.lastplayed
@@ -100,6 +108,10 @@ class PlaylistRow:
def title(self):
return self.dto.title
+ @title.setter
+ def title(self, value: str) -> None:
+ print(f"set title attribute for {self=}, {value=}")
+
@property
def track_id(self):
return self.dto.track_id
diff --git a/app/playlists.py b/app/playlists.py
index 5be1d09..325fb42 100644
--- a/app/playlists.py
+++ b/app/playlists.py
@@ -51,7 +51,6 @@ from helpers import (
show_warning,
)
from log import log, log_call
-from models import db, Settings
from playlistrow import TrackSequence
from playlistmodel import PlaylistModel, PlaylistProxyModel
import repository
diff --git a/app/repository.py b/app/repository.py
index a438b23..6604802 100644
--- a/app/repository.py
+++ b/app/repository.py
@@ -19,7 +19,7 @@ from classes import ApplicationError, PlaylistRowDTO
from classes import PlaylistDTO, TrackDTO
from config import Config
import helpers
-from log import log
+from log import log, log_call
from models import (
db,
NoteColours,
@@ -32,17 +32,11 @@ from models import (
# Notecolour functions
-def get_colour(text: str, foreground: bool = False) -> str:
+def _get_colour_record(text: str) -> tuple[NoteColours | None, str]:
"""
- Parse text and return background (foreground if foreground==True)
- colour string if matched, else None
+ Parse text and return first matching colour record or None
"""
- if not text:
- return ""
-
- match = False
-
with db.Session() as session:
for rec in NoteColours.get_all(session):
if rec.is_regex:
@@ -51,21 +45,49 @@ def get_colour(text: str, foreground: bool = False) -> str:
flags |= re.IGNORECASE
p = re.compile(rec.substring, flags)
if p.match(text):
- match = True
+ if rec.strip_substring:
+ return_text = re.sub(p, "", text)
+ else:
+ return_text = text
+ return (rec, return_text)
else:
if rec.is_casesensitive:
if rec.substring in text:
- match = True
+ return_text = text.replace(rec.substring, "")
+ return (rec, return_text)
else:
if rec.substring.lower() in text.lower():
- match = True
+ return_text = helpers.remove_substring_case_insensitive(
+ text, rec.substring
+ )
+ return (rec, return_text)
- if match:
- if foreground:
- return rec.foreground or ""
- else:
- return rec.colour
+ return (None, text)
+
+
+def get_colour(text: str, foreground: bool = False) -> str:
+ """
+ Parse text and return background (foreground if foreground==True)
+ colour string if matched, else None
+ """
+
+ (rec, _) = _get_colour_record(text)
+ if rec is None:
return ""
+ elif foreground:
+ return rec.foreground or ""
+ else:
+ return rec.colour
+
+
+def remove_colour_substring(text: str) -> str:
+ """
+ Remove text that identifies the colour to be used if strip_substring is True
+ """
+
+ (rec, stripped_text) = _get_colour_record(text)
+
+ return stripped_text
# Track functions
@@ -115,6 +137,34 @@ def create_track(path: str) -> TrackDTO:
return new_track
+def update_track(path: str, track_id: int) -> TrackDTO:
+ """
+ Update an existing track db entry return the DTO
+ """
+
+ metadata = helpers.get_all_track_metadata(path)
+ with db.Session() as session:
+ track = session.get(Tracks, track_id)
+ if not track:
+ raise ApplicationError(f"Can't retrieve Track ({track_id=})")
+ track.path = (str(metadata["path"]),)
+ track.title = (str(metadata["title"]),)
+ track.artist = (str(metadata["artist"]),)
+ track.duration = (int(metadata["duration"]),)
+ track.start_gap = (int(metadata["start_gap"]),)
+ track.fade_at = (int(metadata["fade_at"]),)
+ track.silence_at = (int(metadata["silence_at"]),)
+ track.bitrate = (int(metadata["bitrate"]),)
+
+ session.commit()
+
+ updated_track = track_by_id(track_id)
+ if not updated_track:
+ raise ApplicationError("Unable to retrieve updated track")
+
+ return updated_track
+
+
def get_all_tracks() -> list[TrackDTO]:
"""Return a list of all tracks"""
@@ -245,10 +295,7 @@ def track_with_path(path: str) -> bool:
with db.Session() as session:
track = (
- session.execute(
- select(Tracks)
- .where(Tracks.path == path)
- )
+ session.execute(select(Tracks).where(Tracks.path == path))
.scalars()
.one_or_none()
)
@@ -272,6 +319,28 @@ def tracks_like_title(filter_str: str) -> list[TrackDTO]:
return _tracks_where(Tracks.title.ilike(f"%{filter_str}%"))
+def get_last_played_dates(track_id: int, limit: int = 5) -> str:
+ """
+ Return the most recent 'limit' dates that this track has been played
+ as a text list
+ """
+
+ with db.Session() as session:
+ playdates = session.scalars(
+ Playdates.select()
+ .where(Playdates.track_id == track_id)
+ .order_by(Playdates.lastplayed.desc())
+ .limit(limit)
+ ).all()
+
+ return "
".join(
+ [
+ a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
+ for a in playdates
+ ]
+ )
+
+
# Playlist functions
def _check_playlist_integrity(
session: Session, playlist_id: int, fix: bool = False
@@ -305,6 +374,7 @@ def _check_playlist_integrity(
raise ApplicationError(msg)
+@log_call
def _shift_rows(
session: Session, playlist_id: int, starting_row: int, shift_by: int
) -> None:
@@ -313,8 +383,6 @@ def _shift_rows(
down; if -ve, shift them up.
"""
- log.debug(f"(_shift_rows_down({playlist_id=}, {starting_row=}, {shift_by=}")
-
session.execute(
update(PlaylistRows)
.where(
@@ -325,8 +393,12 @@ def _shift_rows(
)
+@log_call
def move_rows(
- from_rows: list[int], from_playlist_id: int, to_row: int, to_playlist_id: int | None = None
+ from_rows: list[int],
+ from_playlist_id: int,
+ to_row: int,
+ to_playlist_id: int | None = None,
) -> None:
"""
Move rows with or between playlists.
@@ -341,10 +413,6 @@ def move_rows(
- Sanity check row numbers
"""
- log.debug(
- f"move_rows_to_playlist({from_rows=}, {from_playlist_id=}, {to_row=}, {to_playlist_id=})"
- )
-
# If to_playlist_id isn't specified, we're moving within the one
# playlist.
if to_playlist_id is None:
@@ -690,6 +758,25 @@ def insert_row(
return new_playlist_row
+@log_call
+def remove_comments(playlist_id: int, row_numbers: list[int]) -> None:
+ """
+ Remove comments from rows in playlist
+ """
+
+ with db.Session() as session:
+ session.execute(
+ update(PlaylistRows)
+ .where(
+ PlaylistRows.playlist_id == playlist_id,
+ PlaylistRows.row_number.in_(row_numbers),
+ )
+ .values(note="")
+ )
+ session.commit()
+
+
+@log_call
def remove_rows(playlist_id: int, row_numbers: list[int]) -> None:
"""
Remove rows from playlist
@@ -697,8 +784,6 @@ def remove_rows(playlist_id: int, row_numbers: list[int]) -> None:
Delete from highest row back so that not yet deleted row numbers don't change.
"""
- log.debug(f"remove_rows({playlist_id=}, {row_numbers=}")
-
with db.Session() as session:
for row_number in sorted(row_numbers, reverse=True):
session.execute(
@@ -749,9 +834,11 @@ def get_setting(name: str) -> int | None:
"""
with db.Session() as session:
- record = session.execute(
- select(Settings).where(Settings.name == name)
- ).scalars().one_or_none()
+ record = (
+ session.execute(select(Settings).where(Settings.name == name))
+ .scalars()
+ .one_or_none()
+ )
if not record:
return None
@@ -764,9 +851,11 @@ def set_setting(name: str, value: int) -> None:
"""
with db.Session() as session:
- record = session.execute(
- select(Settings).where(Settings.name == name)
- ).scalars().one_or_none()
+ record = (
+ session.execute(select(Settings).where(Settings.name == name))
+ .scalars()
+ .one_or_none()
+ )
if not record:
record = Settings(session=session, name=name)
if not record: