diff --git a/app/config.py b/app/config.py index 4825275..ab31ec5 100644 --- a/app/config.py +++ b/app/config.py @@ -11,14 +11,12 @@ class Config(object): COLOUR_CURRENT_PLAYLIST = "#7eca8f" COLOUR_CURRENT_TAB = "#248f24" COLOUR_ENDING_TIMER = "#dc3545" - COLOUR_EVEN_PLAYLIST = "#d9d9d9" COLOUR_LONG_START = "#dc3545" COLOUR_NEXT_HEADER = "#fff3cd" COLOUR_NEXT_PLAYLIST = "#ffc107" COLOUR_NEXT_TAB = "#b38600" COLOUR_NORMAL_TAB = "#000000" COLOUR_NOTES_PLAYLIST = "#b8daff" - COLOUR_ODD_PLAYLIST = "#f2f2f2" COLOUR_PREVIOUS_HEADER = "#f8d7da" COLOUR_UNREADABLE = "#dc3545" COLOUR_WARNING_TIMER = "#ffc107" diff --git a/app/dbconfig.py b/app/dbconfig.py index a19b623..c769170 100644 --- a/app/dbconfig.py +++ b/app/dbconfig.py @@ -2,9 +2,10 @@ import inspect import logging import os from config import Config -from sqlalchemy import create_engine from contextlib import contextmanager +from sqlalchemy import create_engine from sqlalchemy.orm import (sessionmaker, scoped_session) +from typing import Generator from log import log @@ -38,7 +39,7 @@ engine = create_engine( @contextmanager -def Session(): +def Session() -> Generator[scoped_session, None, None]: frame = inspect.stack()[2] file = frame.filename function = frame.function diff --git a/app/log.py b/app/log.py index 101987d..ab61170 100644 --- a/app/log.py +++ b/app/log.py @@ -11,7 +11,7 @@ from config import Config class LevelTagFilter(logging.Filter): """Add leveltag""" - def filter(self, record): + def filter(self, record: logging.LogRecord): # Extract the first character of the level name record.leveltag = record.levelname[0] @@ -23,7 +23,7 @@ class LevelTagFilter(logging.Filter): class DebugStdoutFilter(logging.Filter): """Filter debug messages sent to stdout""" - def filter(self, record): + def filter(self, record: logging.LogRecord): if record.levelno != logging.DEBUG: return True if record.module in Config.DEBUG_MODULES: diff --git a/app/models.py b/app/models.py index 1eb3a9c..4f6065c 100644 --- a/app/models.py +++ b/app/models.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # # import os.path -# import re +import re # from dbconfig import Session # @@ -46,8 +46,8 @@ from sqlalchemy.orm.exc import ( # from log import log.debug, log.error # Base = declarative_base() -# -# + + # Database classes class NoteColours(Base): __tablename__ = 'notecolours' @@ -93,38 +93,40 @@ class NoteColours(Base): # # return session.query(NoteColours).filter( # NoteColours.id == note_id).first() -# -# @staticmethod -# def get_colour(session: Session, text: str) -> Optional[str]: -# """ -# Parse text and return colour string if matched, else None -# """ -# -# for rec in ( -# session.query(NoteColours) -# .filter(NoteColours.enabled.is_(True)) -# .order_by(NoteColours.order) -# .all() -# ): -# if rec.is_regex: -# flags = re.UNICODE -# if not rec.is_casesensitive: -# flags |= re.IGNORECASE -# p = re.compile(rec.substring, flags) -# if p.match(text): -# return rec.colour -# else: -# if rec.is_casesensitive: -# if rec.substring in text: -# return rec.colour -# else: -# if rec.substring.lower() in text.lower(): -# return rec.colour -# -# return None -# -# -#class Notes(Base): + + @staticmethod + def get_colour(session: Session, text: str) -> Optional[str]: + """ + Parse text and return colour string if matched, else None + """ + + if not text: + return None + + for rec in session.execute( + select(NoteColours) + .filter(NoteColours.enabled.is_(True)) + .order_by(NoteColours.order) + ).scalars().all(): + if rec.is_regex: + flags = re.UNICODE + if not rec.is_casesensitive: + flags |= re.IGNORECASE + p = re.compile(rec.substring, flags) + if p.match(text): + return rec.colour + else: + if rec.is_casesensitive: + if rec.substring in text: + return rec.colour + else: + if rec.substring.lower() in text.lower(): + return rec.colour + + return None + + +# class Notes(Base): # __tablename__ = 'notes' # # id: int = Column(Integer, primary_key=True, autoincrement=True) @@ -514,7 +516,7 @@ class Settings(Base): return int_setting - def update(self, session: Session, data): + def update(self, session: Session, data: "Settings"): for key, value in data.items(): assert hasattr(self, key) setattr(self, key, value) diff --git a/app/musicmuster.py b/app/musicmuster.py index f02f392..3ba2235 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -17,14 +17,14 @@ import sys # from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView from PyQt5.QtWidgets import ( QApplication, -# QDialog, -# QFileDialog, -# QInputDialog, -# QLabel, -# QLineEdit, -# QListWidgetItem, + # QDialog, + # QFileDialog, + # QInputDialog, + # QLabel, + # QLineEdit, + # QListWidgetItem, QMainWindow, -# QMessageBox, + # QMessageBox, ) # from dbconfig import engine, Session @@ -494,7 +494,7 @@ class Window(QMainWindow, Ui_MainWindow): # # also be saved to database # self.visible_playlist_tab().insert_track(session, track) - def _load_last_playlists(self): + def _load_last_playlists(self) -> None: """Load the playlists that were open when the last session closed""" with Session() as session: diff --git a/app/playlists.py b/app/playlists.py index ccbee19..7b15c9c 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -6,9 +6,9 @@ from typing import List, Optional # # from PyQt5 import QtCore from PyQt5.QtCore import Qt -from PyQt5.Qt import QFont from PyQt5.QtGui import ( QColor, + QFont, # QDropEvent ) # from PyQt5 import QtWidgets @@ -27,12 +27,12 @@ from PyQt5.QtWidgets import ( # import helpers # import os -# import re +import re # import subprocess # import threading # from config import Config -from datetime import datetime #, timedelta +from datetime import datetime # , timedelta from helpers import ( get_relative_date, # open_in_audacity @@ -45,10 +45,10 @@ from models import ( PlaylistRows, Settings, Tracks, - # NoteColours + NoteColours ) from dbconfig import Session -# start_time_re = re.compile(r"@\d\d:\d\d:\d\d") +start_time_re = re.compile(r"@\d\d:\d\d:\d\d") class RowMeta: @@ -101,7 +101,7 @@ class PlaylistTab(QTableWidget): PLAYLISTROW_ID = Qt.UserRole + 2 def __init__(self, musicmuster: QMainWindow, session: Session, - playlist_id: int, *args, **kwargs): + playlist_id: int, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.musicmuster: QMainWindow = musicmuster self.playlist_id: int = playlist_id @@ -166,7 +166,7 @@ class PlaylistTab(QTableWidget): # Now load our tracks and notes self.populate(session, self.playlist_id) - def _column_resize(self, idx, old, new): + def _column_resize(self, idx: int, old: int, new: int) -> None: """ Called when column widths are changed. @@ -380,14 +380,10 @@ class PlaylistTab(QTableWidget): self.insertRow(row) # Add row metadata to userdata column - item: QTableWidgetItem = QTableWidgetItem() - item.setData(self.ROW_METADATA, 0) - self.setItem(row, columns['userdata'].idx, item) - - # Prepare notes, start and end items for later - notes_item = QTableWidgetItem(row_data.note) - start_item = QTableWidgetItem() - end_item = QTableWidgetItem() + userdata_item = QTableWidgetItem() + userdata_item.setData(self.ROW_METADATA, 0) + userdata_item.setData(self.PLAYLISTROW_ID, row_data.id) + self.setItem(row, columns['userdata'].idx, userdata_item) if row_data.track_id: # Add track details to items @@ -395,13 +391,28 @@ class PlaylistTab(QTableWidget): start_gap_item = QTableWidgetItem(str(start_gap)) if start_gap and start_gap >= 500: start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START)) + self.setItem(row, columns['start_gap'].idx, start_gap_item) title_item = QTableWidgetItem(row_data.track.title) + self.setItem(row, columns['title'].idx, title_item) artist_item = QTableWidgetItem(row_data.track.artist) + self.setItem(row, columns['artist'].idx, artist_item) duration_item = QTableWidgetItem( helpers.ms_to_mmss(row_data.track.duration)) + self.setItem(row, columns['duration'].idx, duration_item) + + start_item = QTableWidgetItem() + self.setItem(row, columns['start_time'].idx, start_item) + + end_item = QTableWidgetItem() + self.setItem(row, columns['end_time'].idx, end_item) + + # As we have track info, any notes should be contained in + # the notes column + notes_item = QTableWidgetItem(row_data.note) + self.setItem(row, columns['row_notes'].idx, notes_item) last_playtime = Playdates.last_played(session, row_data.track.id) last_played_str = get_relative_date(last_playtime) @@ -412,39 +423,21 @@ class PlaylistTab(QTableWidget): if not helpers.file_is_readable(row_data.track.path): self._set_unreadable_row(row) - else: - # This is a note row so make empty items (row background - # won't be coloured without items present) - start_gap_item = QTableWidgetItem() - title_item = QTableWidgetItem() - artist_item = QTableWidgetItem() - duration_item = QTableWidgetItem() - last_played_item = QTableWidgetItem() - - # Add items to table - self.setItem(row, columns['start_gap'].idx, start_gap_item) - self.setItem(row, columns['title'].idx, title_item) - self.setItem(row, columns['artist'].idx, artist_item) - self.setItem(row, columns['duration'].idx, duration_item) - self.setItem(row, columns['start_time'].idx, start_item) - 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: + # Save track_id userdata_item.setData(self.ROW_TRACK_ID, row_data.track_id) + else: + # This is a section header so make empty items (row + # background won't be coloured without items present). Any + # notes should displayed starting in column 0 + for i in range(2, len(columns) - 1): + self.setItem(row, i, QTableWidgetItem()) + notes_item = QTableWidgetItem(row_data.note) + self.setItem(row, 1, notes_item) + self.setSpan(row, 1, 1, len(columns)) + + # Save (no) track_id 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: - self.setSpan(row, columns['row_notes'].idx, len(columns), 1) - - # Scroll to new row - self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter) if repaint: self.save_playlist(session) @@ -870,6 +863,15 @@ class PlaylistTab(QTableWidget): # Cycle through all rows for row in range(self.rowCount()): + # Extract note text from database to ignore section timings + playlist_row = session.get(PlaylistRows, + self._get_playlistrow_id(row)) + note_text = playlist_row.note + # Get note colour + note_colour = NoteColours.get_colour(session, note_text) + if not note_colour: + note_colour = Config.COLOUR_NOTES_PLAYLIST + # Get track if there is one track_id = self._get_row_track_id(row) track = None @@ -903,6 +905,11 @@ class PlaylistTab(QTableWidget): else: self.showRow(row) + # Colour any note + if note_text: + (self.item(row, columns['row_notes'].idx) + .setBackground(QColor(note_colour))) + # Render playing track if row == current_row: # Set start time @@ -973,21 +980,10 @@ class PlaylistTab(QTableWidget): 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) @@ -1010,10 +1006,6 @@ class PlaylistTab(QTableWidget): 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) ) @@ -1249,7 +1241,7 @@ class PlaylistTab(QTableWidget): # return self._meta_search(RowMeta.NOTE, one=False) # - def _get_playlistrow_id(self, row): + def _get_playlistrow_id(self, row: int) -> int: """Return the playlistrow_id associated with this row""" playlistrow_id = (self.item(row, columns['userdata'].idx) @@ -1257,7 +1249,7 @@ class PlaylistTab(QTableWidget): return playlistrow_id - def _get_row_track_id(self, row): + def _get_row_track_id(self, row: int) -> int: """Return the track_id associated with this row or None""" track_id = (self.item(row, columns['userdata'].idx) @@ -1309,20 +1301,20 @@ class PlaylistTab(QTableWidget): return row[0] else: return None -# -# @staticmethod -# def _get_note_text_time(text: str) -> Optional[datetime]: -# """Return time specified as @hh:mm:ss in text""" -# -# match = start_time_re.search(text) -# if not match: -# return None -# -# try: -# return datetime.strptime(match.group(0)[1:], -# Config.NOTE_TIME_FORMAT) -# except ValueError: -# return None + + @staticmethod + def _get_note_text_time(text: str) -> Optional[datetime]: + """Return time specified as @hh:mm:ss in text""" + + match = start_time_re.search(text) + if not match: + return None + + try: + return datetime.strptime(match.group(0)[1:], + Config.NOTE_TIME_FORMAT) + except ValueError: + return None def _get_played_track_rows(self) -> List[int]: """Return rows marked as played, or None""" @@ -1751,7 +1743,7 @@ class PlaylistTab(QTableWidget): j: int - for j in range(2, self.columnCount()): + for j in range(1, self.columnCount()): if self.item(row, j): self.item(row, j).setBackground(colour) #