From 4f03306affb26b06f1bec61fa5a540567f271f95 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Wed, 3 Aug 2022 20:14:26 +0100 Subject: [PATCH] SQLA2: WIP, playlists load --- app/helpers.py | 103 ++++--- app/models.py | 29 +- app/playlists.py | 747 ++++++++++++++++++++++++----------------------- 3 files changed, 452 insertions(+), 427 deletions(-) diff --git a/app/helpers.py b/app/helpers.py index b4af652..8252a3a 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -1,8 +1,8 @@ -# import os +import os # import psutil # # from config import Config -# from datetime import datetime +from datetime import datetime # from pydub import AudioSegment # from PyQt5.QtWidgets import QMessageBox # from tinytag import TinyTag @@ -44,7 +44,22 @@ # # if there is no trailing silence, return lenght of track (it's less # # the chunk_size, but for chunk_size = 10ms, this may be ignored) # return int(trim_ms) -# + + +def file_is_readable(path: str, check_colon: bool = True) -> bool: + """ + Returns True if passed path is readable, else False + + vlc cannot read files with a colon in the path + """ + + if os.access(path, os.R_OK): + if check_colon: + return ':' not in path + else: + return True + + return False # # def get_audio_segment(path: str) -> Optional[AudioSegment]: # try: @@ -70,47 +85,47 @@ # path=path # ) # return d -# -# -# def get_relative_date(past_date: datetime, reference_date: datetime = None) \ -# -> str: -# """ -# Return how long before reference_date past_date is as string. -# -# Params: -# @past_date: datetime -# @reference_date: datetime, defaults to current date and time -# -# @return: string -# """ -# -# if not past_date: -# return "Never" -# if not reference_date: -# reference_date = datetime.now() -# -# # Check parameters -# if past_date > reference_date: -# return "get_relative_date() past_date is after relative_date" -# -# days: int -# days_str: str -# weeks: int -# weeks_str: str -# -# weeks, days = divmod((reference_date.date() - past_date.date()).days, 7) -# if weeks == days == 0: -# # Played today, so return time instead -# return past_date.strftime("%H:%M") -# if weeks == 1: -# weeks_str = "week" -# else: -# weeks_str = "weeks" -# if days == 1: -# days_str = "day" -# else: -# days_str = "days" -# return f"{weeks} {weeks_str}, {days} {days_str} ago" + + +def get_relative_date(past_date: datetime, reference_date: datetime = None) \ + -> str: + """ + Return how long before reference_date past_date is as string. + + Params: + @past_date: datetime + @reference_date: datetime, defaults to current date and time + + @return: string + """ + + if not past_date: + return "Never" + if not reference_date: + reference_date = datetime.now() + + # Check parameters + if past_date > reference_date: + return "get_relative_date() past_date is after relative_date" + + days: int + days_str: str + weeks: int + weeks_str: str + + weeks, days = divmod((reference_date.date() - past_date.date()).days, 7) + if weeks == days == 0: + # Same day so return time instead + return past_date.strftime("%H:%M") + if weeks == 1: + weeks_str = "week" + else: + weeks_str = "weeks" + if days == 1: + days_str = "day" + else: + days_str = "days" + return f"{weeks} {weeks_str}, {days} {days_str} ago" # # # def leading_silence( diff --git a/app/models.py b/app/models.py index 0c9118c..1eb3a9c 100644 --- a/app/models.py +++ b/app/models.py @@ -232,19 +232,22 @@ class Playdates(Base): # self.track_id = track_id # session.add(self) # session.flush() -# -# @staticmethod -# def last_played(session: Session, track_id: int) -> Optional[datetime]: -# """Return datetime track last played or None""" -# -# last_played: Optional[Playdates] = session.query( -# Playdates.lastplayed).filter( -# (Playdates.track_id == track_id) -# ).order_by(Playdates.lastplayed.desc()).first() -# if last_played: -# return last_played[0] -# else: -# return None + + @staticmethod + def last_played(session: Session, track_id: int) -> Optional[datetime]: + """Return datetime track last played or None""" + + last_played = session.execute( + select(Playdates.lastplayed) + .where(Playdates.track_id == track_id) + .order_by(Playdates.lastplayed.desc()) + .limit(1) + ).first() + + if last_played: + return last_played[0] + else: + return None # # @staticmethod # def played_after(session: Session, since: datetime) -> List["Playdates"]: diff --git a/app/playlists.py b/app/playlists.py index 975e091..ccbee19 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -1,15 +1,19 @@ from collections import namedtuple # from enum import Enum, auto +from typing import List, Optional # from typing import Dict, List, Optional, Set, Tuple, Union # # from PyQt5 import QtCore from PyQt5.QtCore import Qt -# from PyQt5.Qt import QFont -# from PyQt5.QtGui import QColor, QDropEvent +from PyQt5.Qt import QFont +from PyQt5.QtGui import ( + QColor, + # QDropEvent +) # from PyQt5 import QtWidgets from PyQt5.QtWidgets import ( - # QAbstractItemView, + QAbstractItemView, # QApplication, # QInputDialog, # QLineEdit, @@ -28,29 +32,33 @@ import helpers # import threading # from config import Config -# from datetime import datetime, timedelta -# from helpers import get_relative_date, open_in_audacity +from datetime import datetime #, timedelta +from helpers import ( + get_relative_date, + # open_in_audacity +) # from log import log.debug, log.error from models import ( # Notes, - # Playdates, + Playdates, Playlists, PlaylistRows, Settings, - # Tracks, + Tracks, # NoteColours ) from dbconfig import Session # start_time_re = re.compile(r"@\d\d:\d\d:\d\d") -# -# -# class RowMeta: -# CLEAR = 0 -# NOTE = 1 -# UNREADABLE = 2 -# NEXT = 3 -# CURRENT = 4 -# PLAYED = 5 + + +class RowMeta: + CLEAR = 0 + NOTE = 1 + UNREADABLE = 2 + NEXT = 3 + CURRENT = 4 + PLAYED = 5 + # Columns Column = namedtuple("Column", ['idx', 'heading']) @@ -89,8 +97,8 @@ class PlaylistTab(QTableWidget): # Qt.UserRoles ROW_METADATA = Qt.UserRole - # CONTENT_OBJECT = Qt.UserRole + 1 - ROW_DURATION = Qt.UserRole + 2 + ROW_TRACK_ID = Qt.UserRole + 1 + PLAYLISTROW_ID = Qt.UserRole + 2 def __init__(self, musicmuster: QMainWindow, session: Session, playlist_id: int, *args, **kwargs): @@ -144,7 +152,7 @@ class PlaylistTab(QTableWidget): # # self.itemSelectionChanged.connect(self._select_event) # -# self.row_filter: Optional[str] = None + self.row_filter: Optional[str] = None # self.editing_cell: bool = False # self.selecting_in_progress = False # Connect signals @@ -394,15 +402,14 @@ class PlaylistTab(QTableWidget): duration_item = QTableWidgetItem( helpers.ms_to_mmss(row_data.track.duration)) - self._set_row_duration(row, row_data.track.duration) last_playtime = Playdates.last_played(session, row_data.track.id) last_played_str = get_relative_date(last_playtime) last_played_item = QTableWidgetItem(last_played_str) - self.setItem(row, columns['lastplayed'], last_played_item) + self.setItem(row, columns['lastplayed'].idx, last_played_item) # Mark track if file is unreadable - if not self._file_is_readable(row_data.track.path): + if not helpers.file_is_readable(row_data.track.path): self._set_unreadable_row(row) else: @@ -423,9 +430,18 @@ class PlaylistTab(QTableWidget): self.setItem(row, columns['end_time'].idx, end_item) self.setItem(row, columns['row_notes'].idx, notes_item) + # Save track_id and playlistrow_id + userdata_item = QTableWidgetItem() + if row_data.track_id: + userdata_item.setData(self.ROW_TRACK_ID, row_data.track_id) + else: + userdata_item.setData(self.ROW_TRACK_ID, 0) + userdata_item.setData(self.PLAYLISTROW_ID, row_data.id) + self.setItem(row, columns['userdata'].idx, userdata_item) + + # Span note across table if not row_data.track_id: - # Span note across table - self.setSpan(row, 0, len(columns), 1) + self.setSpan(row, columns['row_notes'].idx, len(columns), 1) # Scroll to new row self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter) @@ -815,211 +831,200 @@ class PlaylistTab(QTableWidget): # # with Session() as session: # self._set_next(row, session) -# -# def update_display(self, session, clear_selection: bool = True) -> None: -# """ -# Set row colours, fonts, etc -# -# Actions required: -# - Clear selection if required -# - Render notes in correct colour -# - Render current, next and unplayable tracks in correct colour -# - Set start and end times -# - Show unplayed tracks in bold -# """ -# -# # Clear selection if required -# if clear_selection: -# self.clearSelection() -# -# current_row: Optional[int] = self._get_current_track_row() -# next_row: Optional[int] = self._get_next_track_row() -# notes: List[int] = self._get_notes_rows() -# played: Optional[List[int]] = self._get_played_track_rows() -# unreadable: List[int] = self._get_unreadable_track_rows() -# -# if self.row_filter: -# filter_text = self.row_filter.lower() -# else: -# filter_text = None -# hide_played = self.musicmuster.hide_played_tracks -# last_played_str: str -# last_playedtime: Optional[datetime] -# next_start_time: Optional[datetime] = None -# note_colour: str -# note_start_time: Optional[str] -# note_text: str -# row: int -# row_time: Optional[datetime] -# section_start_row: Optional[int] = None -# section_time: int = 0 -# start_time: Optional[datetime] -# start_times_row: Optional[int] -# track: Optional[Tracks] -# -# # Start time calculations -# # Don't change start times for tracks that have been played. -# # For unplayed tracks, if there's a 'current' or 'next' -# # track marked, populate start times from then onwards. A note -# # with a start time will reset the next track start time. -# -# # Cycle through all rows -# for row in range(self.rowCount()): -# -# # Render notes in correct colour -# if row in notes: -# # Extract note text from database to ignore section timings -# note_text = self._get_row_notes_object(row, session).note -# if filter_text: -# if filter_text not in note_text.lower(): -# self.hideRow(row) -# continue -# else: -# self.showRow(row) -# else: -# self.showRow(row) -# # Does the note have a start time? -# row_time = self._get_note_text_time(note_text) -# if row_time: -# next_start_time = row_time -# # Does it delimit a section? -# if section_start_row is not None: -# if note_text.endswith("-"): -# self._set_timed_section(session, section_start_row, -# section_time) -# section_start_row = None -# section_time = 0 -# elif note_text.endswith("+"): -# section_start_row = row -# section_time = 0 -# # Set colour -# note_colour = NoteColours.get_colour(session, note_text) -# if not note_colour: -# note_colour = Config.COLOUR_NOTES_PLAYLIST -# self._set_row_colour( -# row, QColor(note_colour) -# ) -# # Notes are always bold -# self._set_row_bold(row) -# continue -# -# # Render unplayable tracks in correct colour -# if row in unreadable: -# self._set_row_colour( -# row, QColor(Config.COLOUR_UNREADABLE) -# ) -# self._set_row_bold(row) -# continue -# -# # Current row is a track row -# track = self._get_row_track_object(row, session) -# # Add track time to section time if in timed section -# if section_start_row is not None: -# section_time += track.duration -# # Render current track -# if filter_text: -# try: -# if (track.title -# and filter_text not in track.title.lower() -# and track.artist -# and filter_text not in track.artist.lower()): -# self.hideRow(row) -# continue -# else: -# self.showRow(row) -# except TypeError: -# print(f"TypeError: {track=}") -# else: -# self.showRow(row) -# if row == current_row: -# # Set start time -# self._set_row_start_time( -# row, self.current_track_start_time) -# -# # Set last played time -# last_played_str = get_relative_date( -# self.current_track_start_time) -# self.item(row, self.COL_LAST_PLAYED).setText( -# last_played_str) -# -# # Calculate next_start_time -# next_start_time = self._calculate_row_end_time( -# row, self.current_track_start_time) -# -# # Set end time -# self._set_row_end_time(row, next_start_time) -# -# # Set colour -# self._set_row_colour(row, QColor( -# Config.COLOUR_CURRENT_PLAYLIST)) -# -# # Make bold -# self._set_row_bold(row) -# continue -# -# # Render next track -# if row == next_row: -# # if there's a track playing, set start time from that -# if current_row is not None: -# start_time = self._calculate_row_end_time( -# current_row, self.current_track_start_time) -# else: -# # No current track to base from, but don't change -# # time if it's already set -# start_time = self._get_row_start_time(row) -# if not start_time: -# start_time = next_start_time -# self._set_row_start_time(row, start_time) -# -# # Set end time -# next_start_time = self._calculate_row_end_time(row, start_time) -# self._set_row_end_time(row, next_start_time) -# -# # Set colour -# self._set_row_colour( -# row, QColor(Config.COLOUR_NEXT_PLAYLIST)) -# -# # Make bold -# self._set_row_bold(row) -# -# else: -# # This is a track row other than next or current -# if row in played: -# # Played today, so update last played column -# last_playedtime = track.lastplayed -# last_played_str = get_relative_date(last_playedtime) -# self.item(row, self.COL_LAST_PLAYED).setText( -# last_played_str) -# if hide_played: -# self.hideRow(row) -# else: -# self._set_row_not_bold(row) -# else: -# # Set start/end times as we haven't played it yet -# if next_start_time: -# self._set_row_start_time(row, next_start_time) -# next_start_time = self._calculate_row_end_time( -# row, next_start_time) -# # Set end time -# self._set_row_end_time(row, next_start_time) -# else: -# # Clear start and end time -# self._set_row_start_time(row, None) -# self._set_row_end_time(row, None) -# # Don't dim unplayed tracks -# self._set_row_bold(row) -# # Stripe rows -# if row % 2: -# self._set_row_colour( -# row, QColor(Config.COLOUR_ODD_PLAYLIST)) -# else: -# self._set_row_colour( -# row, QColor(Config.COLOUR_EVEN_PLAYLIST)) -# -# # Have we had a section start but not end? -# if section_start_row is not None: -# self._set_timed_section( -# session, section_start_row, section_time, no_end=True) + + def update_display(self, session, clear_selection: bool = True) -> None: + """ + Set row colours, fonts, etc + + Actions required: + - Clear selection if required + - Render notes in correct colour + - Render current, next and unplayable tracks in correct colour + - Set start and end times + - Show unplayed tracks in bold + """ + + # Clear selection if required + if clear_selection: + self.clearSelection() + + current_row: Optional[int] = self._get_current_track_row() + next_row: Optional[int] = self._get_next_track_row() + played: Optional[List[int]] = self._get_played_track_rows() + unreadable: List[int] = self._get_unreadable_track_rows() + + if self.row_filter: + filter_text = self.row_filter.lower() + else: + filter_text = None + next_start_time = None + section_start_row = None + section_time = 0 + + # Start time calculations + # Don't change start times for tracks that have been played. + # For unplayed tracks, if there's a 'current' or 'next' + # track marked, populate start times from then onwards. A note + # with a start time will reset the next track start time. + + # Cycle through all rows + for row in range(self.rowCount()): + + # Get track if there is one + track_id = self._get_row_track_id(row) + track = None + if track_id: + track = session.get(Tracks, track_id) + + if track: + # Render unplayable tracks in correct colour + if not helpers.file_is_readable(track.path): + self._set_row_colour(row, QColor(Config.COLOUR_UNREADABLE)) + self._set_row_bold(row) + continue + + # Add track time to section time if in timed section + if section_start_row is not None: + section_time += track.duration + + # If filtering, only show matching tracks + if filter_text: + try: + if (track.title + and filter_text not in track.title.lower() + and track.artist + and filter_text not in track.artist.lower()): + self.hideRow(row) + continue + else: + self.showRow(row) + except TypeError: + print(f"TypeError: {track=}") + else: + self.showRow(row) + + # Render playing track + if row == current_row: + # Set start time + self._set_row_start_time( + row, self.current_track_start_time) + # Set last played time to "Today" + self.item(row, self.COL_LAST_PLAYED).setText("Today") + # Calculate next_start_time + next_start_time = self._calculate_end_time( + self.current_track_start_time, track.duration) + # Set end time + self._set_row_end_time(row, next_start_time) + # Set colour + self._set_row_colour(row, QColor( + Config.COLOUR_CURRENT_PLAYLIST)) + # Make bold + self._set_row_bold(row) + continue + + # Render next track + if row == next_row: + # Set start time + # if there's a track playing, set start time from that + if current_row is not None: + start_time = self._calculate_end_time( + self.current_track_start_time, track.duration) + else: + # No current track to base from, but don't change + # time if it's already set + start_time = self._get_row_start_time(row) + if not start_time: + start_time = next_start_time + self._set_row_start_time(row, start_time) + # Calculate next_start_time + next_start_time = self._calculate_end_time(start_time, + track.duration) + # Set end time + self._set_row_end_time(row, next_start_time) + # Set colour + self._set_row_colour( + row, QColor(Config.COLOUR_NEXT_PLAYLIST)) + # Make bold + self._set_row_bold(row) + continue + + # This is a track row other than next or current + if row in played: + # Played today, so update last played column + last_playedtime = track.lastplayed + last_played_str = get_relative_date(last_playedtime) + self.item(row, self.COL_LAST_PLAYED).setText( + last_played_str) + if self.musicmuster.hide_played_tracks: + self.hideRow(row) + else: + self._set_row_not_bold(row) + else: + # Set start/end times as we haven't played it yet + if next_start_time: + self._set_row_start_time(row, next_start_time) + next_start_time = self._calculate_end_time( + next_start_time, track.duration) + # Set end time + self._set_row_end_time(row, next_start_time) + else: + # Clear start and end time + self._set_row_start_time(row, None) + self._set_row_end_time(row, None) + # Don't dim unplayed tracks + self._set_row_bold(row) + # Stripe rows + if row % 2: + self._set_row_colour( + row, QColor(Config.COLOUR_ODD_PLAYLIST)) + else: + self._set_row_colour( + row, QColor(Config.COLOUR_EVEN_PLAYLIST)) + + continue + + # No track associated, so this row is a section header + # Extract note text from database to ignore section timings + playlist_row = session.get(PlaylistTracks, + self._get_playlistrow_id(row)) + note_text = playlist_row.note + if filter_text: + if filter_text not in note_text.lower(): + self.hideRow(row) + continue + else: + self.showRow(row) + else: + self.showRow(row) + # Does the note have a start time? + row_time = self._get_note_text_time(note_text) + if row_time: + next_start_time = row_time + # Does it delimit a section? + if section_start_row is not None: + if note_text.endswith("-"): + self._set_timed_section(session, section_start_row, + section_time) + section_start_row = None + section_time = 0 + elif note_text.endswith("+"): + section_start_row = row + section_time = 0 + # Set colour + note_colour = NoteColours.get_colour(session, note_text) + if not note_colour: + note_colour = Config.COLOUR_NOTES_PLAYLIST + self._set_row_colour( + row, QColor(note_colour) + ) + # Notes are always bold + self._set_row_bold(row) + continue + + # Have we had a section start but not end? + if section_start_row is not None: + self._set_timed_section( + session, section_start_row, section_time, no_end=True) # # # ########## Internally called functions ########## # @@ -1034,16 +1039,15 @@ class PlaylistTab(QTableWidget): # with Session() as session: # track: Tracks = self._get_row_track_object(row, session) # open_in_audacity(track.path) -# -# def _calculate_row_end_time(self, row, start: Optional[datetime]) \ -# -> Optional[datetime]: -# """Return this row's end time given its start time""" -# -# if start is None: -# return None -# -# duration = self._get_row_duration(row) -# return start + timedelta(milliseconds=duration) + + def _calculate_end_time(self, start: Optional[datetime], + duration: int) -> Optional[datetime]: + """Return datetime 'duration' ms after 'start'""" + + if start is None: + return None + + return start + timedelta(milliseconds=duration) # # def _context_menu(self, pos): # review # @@ -1239,25 +1243,28 @@ class PlaylistTab(QTableWidget): # if column in [self.COL_TITLE, self.COL_ARTIST]: # self.editItem(item) # -# @staticmethod -# def _file_is_readable(path: str) -> bool: -# """ -# Returns True if track path is readable, else False -# -# vlc cannot read files with a colon in the path -# """ -# -# if os.access(path, os.R_OK): -# if ':' not in path: -# return True -# -# return False -# # def _get_notes_rows(self) -> List[int]: # """Return rows marked as notes, or None""" # # return self._meta_search(RowMeta.NOTE, one=False) # + + def _get_playlistrow_id(self, row): + """Return the playlistrow_id associated with this row""" + + playlistrow_id = (self.item(row, columns['userdata'].idx) + .data(self.PLAYLISTROW_ID)) + + return playlistrow_id + + def _get_row_track_id(self, row): + """Return the track_id associated with this row or None""" + + track_id = (self.item(row, columns['userdata'].idx) + .data(self.ROW_TRACK_ID)) + + return track_id + # def _find_next_track_row(self, starting_row: int = None) -> Optional[int]: # """ # Find next track to play. If a starting row is given, start there; @@ -1284,24 +1291,24 @@ class PlaylistTab(QTableWidget): # return row # # return None -# -# def _get_current_track_row(self) -> Optional[int]: -# """Return row marked as current, or None""" -# -# row = self._meta_search(RowMeta.CURRENT) -# if len(row) > 0: -# return row[0] -# else: -# return None -# -# def _get_next_track_row(self) -> Optional[int]: -# """Return row marked as next, or None""" -# -# row = self._meta_search(RowMeta.NEXT) -# if len(row) > 0: -# return row[0] -# else: -# return None + + def _get_current_track_row(self) -> Optional[int]: + """Return row marked as current, or None""" + + row = self._meta_search(RowMeta.CURRENT) + if len(row) > 0: + return row[0] + else: + return None + + def _get_next_track_row(self) -> Optional[int]: + """Return row marked as next, or None""" + + row = self._meta_search(RowMeta.NEXT) + if len(row) > 0: + return row[0] + else: + return None # # @staticmethod # def _get_note_text_time(text: str) -> Optional[datetime]: @@ -1316,6 +1323,11 @@ class PlaylistTab(QTableWidget): # Config.NOTE_TIME_FORMAT) # except ValueError: # return None + + def _get_played_track_rows(self) -> List[int]: + """Return rows marked as played, or None""" + + return self._meta_search(RowMeta.PLAYED, one=False) # # def _get_row_duration(self, row: int) -> int: # """Return duration associated with this row""" @@ -1349,11 +1361,6 @@ class PlaylistTab(QTableWidget): # note = Notes.get_by_id(session, note_id) # return note # -# def _get_played_track_rows(self) -> List[int]: -# """Return rows marked as played, or None""" -# -# return self._meta_search(RowMeta.PLAYED, one=False) -# # def _get_unplayed_track_rows(self) -> Optional[List[int]]: # """Return rows marked as unplayed, or None""" # @@ -1386,11 +1393,11 @@ class PlaylistTab(QTableWidget): # """Return rows marked as tracks, or None""" # # return self._meta_notset(RowMeta.NOTE) -# -# def _get_unreadable_track_rows(self) -> List[int]: -# """Return rows marked as unreadable, or None""" -# -# return self._meta_search(RowMeta.UNREADABLE, one=False) + + def _get_unreadable_track_rows(self) -> List[int]: + """Return rows marked as unreadable, or None""" + + return self._meta_search(RowMeta.UNREADABLE, one=False) # # def _info_row(self, row: int) -> None: # """Display popup with info re row""" @@ -1502,11 +1509,12 @@ class PlaylistTab(QTableWidget): # next_row: Optional[int] = self._get_next_track_row() # if next_row is not None: # self._meta_clear_attribute(next_row, RowMeta.NEXT) -# -# def _meta_get(self, row: int) -> int: -# """Return row metadata""" -# -# return self.item(row, self.COL_USERDATA).data(self.ROW_METADATA) + + def _meta_get(self, row: int) -> int: + """Return row metadata""" + + return (self.item(row, columns['userdata'].idx) + .data(self.ROW_METADATA)) # # def _meta_notset(self, metadata: int) -> List[int]: # """ @@ -1523,34 +1531,34 @@ class PlaylistTab(QTableWidget): # matches.append(row) # # return matches -# -# def _meta_search(self, metadata: int, one: bool = True) -> List[int]: -# """ -# Search rows for metadata. -# -# If one is True, check that only one row matches and return -# the row number. -# -# If one is False, return a list of matching row numbers. -# """ -# -# matches = [] -# for row in range(self.rowCount()): -# if self._meta_get(row): -# if self._meta_get(row) & (1 << metadata): -# matches.append(row) -# -# if not one: -# return matches -# -# if len(matches) <= 1: -# return matches -# else: -# log.error( -# f"Multiple matches for metadata '{metadata}' found " -# f"in rows: {', '.join([str(x) for x in matches])}" -# ) -# raise AttributeError(f"Multiple '{metadata}' metadata {matches}") + + def _meta_search(self, metadata: int, one: bool = True) -> List[int]: + """ + Search rows for metadata. + + If one is True, check that only one row matches and return + the row number. + + If one is False, return a list of matching row numbers. + """ + + matches = [] + for row in range(self.rowCount()): + if self._meta_get(row): + if self._meta_get(row) & (1 << metadata): + matches.append(row) + + if not one: + return matches + + if len(matches) <= 1: + return matches + else: + log.error( + f"Multiple matches for metadata '{metadata}' found " + f"in rows: {', '.join([str(x) for x in matches])}" + ) + raise AttributeError(f"Multiple '{metadata}' metadata {matches}") # # def _meta_set_attribute(self, row: int, attribute: int) -> None: # """Set row metadata""" @@ -1622,12 +1630,12 @@ class PlaylistTab(QTableWidget): # """Mark this row as played""" # # self._meta_set_attribute(row, RowMeta.PLAYED) -# -# def _set_unreadable_row(self, row: int) -> None: -# """Mark this row as unreadable""" -# -# self._meta_set_attribute(row, RowMeta.UNREADABLE) -# + + def _set_unreadable_row(self, row: int) -> None: + """Mark this row as unreadable""" + + self._meta_set_attribute(row, RowMeta.UNREADABLE) + # def _select_event(self) -> None: # """ # Called when item selection changes. @@ -1726,27 +1734,26 @@ class PlaylistTab(QTableWidget): # # # Notify musicmuster # self.musicmuster.this_is_the_next_track(self, track, session) -# -# def _set_row_bold(self, row: int, bold: bool = True) -> None: -# """Make row bold (bold=True) or not bold""" -# -# i: int -# j: int -# -# boldfont: QFont = QFont() -# boldfont.setBold(bold) -# for j in range(self.columnCount()): -# if self.item(row, j): -# self.item(row, j).setFont(boldfont) -# -# def _set_row_colour(self, row: int, colour: QColor) -> None: -# """Set row background colour""" -# -# j: int -# -# for j in range(2, self.columnCount()): -# if self.item(row, j): -# self.item(row, j).setBackground(colour) + + def _set_row_bold(self, row: int, bold: bool = True) -> None: + """Make row bold (bold=True) or not bold""" + + j: int + + boldfont: QFont = QFont() + boldfont.setBold(bold) + for j in range(self.columnCount()): + if self.item(row, j): + self.item(row, j).setFont(boldfont) + + def _set_row_colour(self, row: int, colour: QColor) -> None: + """Set row background colour""" + + j: int + + for j in range(2, self.columnCount()): + if self.item(row, j): + self.item(row, j).setBackground(colour) # # def _set_row_content(self, row: int, object_id: int) -> None: # """Set content associated with this row""" @@ -1756,37 +1763,37 @@ class PlaylistTab(QTableWidget): # self.item(row, self.COL_USERDATA).setData( # self.CONTENT_OBJECT, object_id) # -# def _set_row_duration(self, row: int, ms: int) -> None: -# """Set duration of this row in milliseconds""" +# def _set_row_duration(self, row: int, ms: int) -> None: +# """Set duration of this row in row metadata""" # -# assert self.item(row, self.COL_USERDATA) +# assert self.item(row, columns['userdata'].idx) # -# self.item(row, self.COL_USERDATA).setData(self.ROW_DURATION, ms) -# -# def _set_row_end_time(self, row: int, time: Optional[datetime]) -> None: -# """Set passed row end time to passed time""" -# -# try: -# time_str: str = time.strftime(Config.TRACK_TIME_FORMAT) -# except AttributeError: -# time_str = "" -# item = QTableWidgetItem(time_str) -# self.setItem(row, self.COL_END_TIME, item) -# -# def _set_row_not_bold(self, row: int) -> None: -# """Set row to not be bold""" -# -# self._set_row_bold(row, False) -# -# def _set_row_start_time(self, row: int, time: Optional[datetime]) -> None: -# """Set passed row start time to passed time""" -# -# try: -# time_str: str = time.strftime(Config.TRACK_TIME_FORMAT) -# except AttributeError: -# time_str = "" -# item: QTableWidgetItem = QTableWidgetItem(time_str) -# self.setItem(row, self.COL_START_TIME, item) +# self.item(row, columns['userdata'].idx).setData(self.ROW_DURATION, ms) + + def _set_row_end_time(self, row: int, time: Optional[datetime]) -> None: + """Set passed row end time to passed time""" + + try: + time_str: str = time.strftime(Config.TRACK_TIME_FORMAT) + except AttributeError: + time_str = "" + item = QTableWidgetItem(time_str) + self.setItem(row, columns['end_time'].idx, item) + + def _set_row_not_bold(self, row: int) -> None: + """Set row to not be bold""" + + self._set_row_bold(row, False) + + def _set_row_start_time(self, row: int, time: Optional[datetime]) -> None: + """Set passed row start time to passed time""" + + try: + time_str: str = time.strftime(Config.TRACK_TIME_FORMAT) + except AttributeError: + time_str = "" + item: QTableWidgetItem = QTableWidgetItem(time_str) + self.setItem(row, columns['start_time'].idx, item) # # def _set_timed_section(self, session, start_row, ms, no_end=False): # """Add duration to a marked section"""