No mypy errors; four FIXMEs

This commit is contained in:
Keith Edmunds 2023-02-05 21:04:10 +00:00
parent e4ef0b34c8
commit 4f3fb6c1ae
6 changed files with 412 additions and 219 deletions

View File

View File

@ -7,7 +7,7 @@ import stackprinter # type: ignore
from dbconfig import Session, scoped_session from dbconfig import Session, scoped_session
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import Iterable, List, Optional
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
@ -95,13 +95,13 @@ class NoteColours(Base):
) )
@staticmethod @staticmethod
def get_colour(session: scoped_session, text: str) -> Optional[str]: def get_colour(session: scoped_session, text: str) -> str:
""" """
Parse text and return colour string if matched, else None Parse text and return colour string if matched, else empty string
""" """
if not text: if not text:
return None return ""
for rec in session.execute( for rec in session.execute(
select(NoteColours) select(NoteColours)
@ -123,7 +123,7 @@ class NoteColours(Base):
if rec.substring.lower() in text.lower(): if rec.substring.lower() in text.lower():
return rec.colour return rec.colour
return None return ""
class Playdates(Base): class Playdates(Base):
@ -196,7 +196,7 @@ class Playlists(Base):
is_template = Column(Boolean, default=False, nullable=False) is_template = Column(Boolean, default=False, nullable=False)
query = Column(String(256), default=None, nullable=True, unique=False) query = Column(String(256), default=None, nullable=True, unique=False)
deleted = Column(Boolean, default=False, nullable=False) deleted = Column(Boolean, default=False, nullable=False)
rows: "PlaylistRows" = relationship( rows: List["PlaylistRows"] = relationship(
"PlaylistRows", "PlaylistRows",
back_populates="playlist", back_populates="playlist",
cascade="all, delete-orphan", cascade="all, delete-orphan",
@ -370,7 +370,7 @@ class PlaylistRows(Base):
def __init__(self, def __init__(self,
session: scoped_session, session: scoped_session,
playlist_id: int, playlist_id: int,
track_id: int, track_id: Optional[int],
row_number: int, row_number: int,
note: Optional[str] = None note: Optional[str] = None
) -> None: ) -> None:
@ -409,7 +409,7 @@ class PlaylistRows(Base):
@staticmethod @staticmethod
def delete_plrids_not_in_list(session: scoped_session, playlist_id: int, def delete_plrids_not_in_list(session: scoped_session, playlist_id: int,
plrids: List["PlaylistRows"]) -> None: plrids: List[int]) -> None:
""" """
Delete rows in given playlist that have a higher row number Delete rows in given playlist that have a higher row number
than 'maxrow' than 'maxrow'
@ -469,7 +469,7 @@ class PlaylistRows(Base):
@classmethod @classmethod
def get_played_rows(cls, session: scoped_session, def get_played_rows(cls, session: scoped_session,
playlist_id: int) -> List[int]: playlist_id: int) -> List["PlaylistRows"]:
""" """
For passed playlist, return a list of rows that For passed playlist, return a list of rows that
have been played. have been played.
@ -488,7 +488,7 @@ class PlaylistRows(Base):
@classmethod @classmethod
def get_rows_with_tracks(cls, session: scoped_session, def get_rows_with_tracks(cls, session: scoped_session,
playlist_id: int) -> List[int]: playlist_id: int) -> List["PlaylistRows"]:
""" """
For passed playlist, return a list of rows that For passed playlist, return a list of rows that
contain tracks contain tracks
@ -526,24 +526,8 @@ class PlaylistRows(Base):
return plrs return plrs
@staticmethod @staticmethod
def move_rows_down(session: scoped_session, playlist_id: int, def indexed_by_id(session: scoped_session,
starting_row: int, move_by: int) -> None: plr_ids: Iterable[int]) -> dict:
"""
Create space to insert move_by additional rows by incremented row
number from starting_row to end of playlist
"""
session.execute(
update(PlaylistRows)
.where(
(PlaylistRows.playlist_id == playlist_id),
(PlaylistRows.row_number >= starting_row)
)
.values(row_number=PlaylistRows.row_number + move_by)
)
@staticmethod
def indexed_by_id(session: scoped_session, plr_ids: List[int]) -> dict:
""" """
Return a dictionary of playlist_rows indexed by their plr id from Return a dictionary of playlist_rows indexed by their plr id from
the passed plr_id list. the passed plr_id list.
@ -562,6 +546,23 @@ class PlaylistRows(Base):
return result return result
@staticmethod
def move_rows_down(session: scoped_session, playlist_id: int,
starting_row: int, move_by: int) -> None:
"""
Create space to insert move_by additional rows by incremented row
number from starting_row to end of playlist
"""
session.execute(
update(PlaylistRows)
.where(
(PlaylistRows.playlist_id == playlist_id),
(PlaylistRows.row_number >= starting_row)
)
.values(row_number=PlaylistRows.row_number + move_by)
)
class Settings(Base): class Settings(Base):
"""Manage settings""" """Manage settings"""

View File

@ -9,10 +9,29 @@ import threading
from datetime import datetime, timedelta from datetime import datetime, timedelta
from time import sleep from time import sleep
from typing import Callable, List, Optional from typing import (
Callable,
cast,
List,
Optional,
)
from PyQt5.QtCore import pyqtSignal, QDate, QEvent, Qt, QSize, QTime, QTimer from PyQt5.QtCore import (
from PyQt5.QtGui import QColor, QFont, QPalette, QResizeEvent pyqtSignal,
QDate,
QEvent,
Qt,
QSize,
QTime,
QTimer,
)
from PyQt5.QtGui import (
QColor,
QFont,
QMouseEvent,
QPalette,
QResizeEvent,
)
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QApplication,
QDialog, QDialog,
@ -27,10 +46,13 @@ from PyQt5.QtWidgets import (
QProgressBar, QProgressBar,
) )
from dbconfig import engine, Session, scoped_session from dbconfig import (
engine,
Session,
scoped_session,
)
import helpers import helpers
import music import music
from models import ( from models import (
Base, Base,
Carts, Carts,
@ -55,12 +77,11 @@ class CartButton(QPushButton):
progress = pyqtSignal(int) progress = pyqtSignal(int)
def __init__(self, parent: QMainWindow, cart: Carts): def __init__(self, musicmuster: "Window", cart: Carts, *args, **kwargs):
"""Create a cart pushbutton and set it disabled""" """Create a cart pushbutton and set it disabled"""
super().__init__(parent) super().__init__(*args, **kwargs)
# Next line is redundant (check) self.musicmuster = musicmuster
# self.parent = parent
self.cart_id = cart.id self.cart_id = cart.id
if cart.path and cart.enabled and not cart.duration: if cart.path and cart.enabled and not cart.duration:
tags = helpers.get_tags(cart.path) tags = helpers.get_tags(cart.path)
@ -101,8 +122,9 @@ class CartButton(QPushButton):
"""Allow right click even when button is disabled""" """Allow right click even when button is disabled"""
if event.type() == QEvent.MouseButtonRelease: if event.type() == QEvent.MouseButtonRelease:
if event.button() == Qt.RightButton: mouse_event = cast(QMouseEvent, event)
self.parent.cart_edit(self, event) if mouse_event.button() == Qt.RightButton:
self.musicmuster.cart_edit(self, event) # type: ignore # FIXME
return True return True
return super().event(event) return super().event(event)
@ -137,7 +159,7 @@ class PlaylistTrack:
self.playlist_id: Optional[int] = None self.playlist_id: Optional[int] = None
self.playlist_tab: Optional[PlaylistTab] = None self.playlist_tab: Optional[PlaylistTab] = None
self.plr_id: Optional[int] = None self.plr_id: Optional[int] = None
self.silence_at: Optional[datetime] = None self.silence_at: Optional[int] = None
self.start_gap: Optional[int] = None self.start_gap: Optional[int] = None
self.start_time: Optional[datetime] = None self.start_time: Optional[datetime] = None
self.title: Optional[str] = None self.title: Optional[str] = None
@ -164,7 +186,6 @@ class PlaylistTrack:
self.duration = track.duration self.duration = track.duration
self.end_time = None self.end_time = None
self.fade_at = track.fade_at self.fade_at = track.fade_at
self.fade_length = track.silence_at - track.fade_at
self.path = track.path self.path = track.path
self.playlist_id = plr.playlist_id self.playlist_id = plr.playlist_id
self.plr_id = plr.id self.plr_id = plr.id
@ -174,18 +195,23 @@ class PlaylistTrack:
self.title = track.title self.title = track.title
self.track_id = track.id self.track_id = track.id
if track.silence_at and track.fade_at:
self.fade_length = track.silence_at - track.fade_at
def start(self) -> None: def start(self) -> None:
""" """
Called when track starts playing Called when track starts playing
""" """
self.start_time = datetime.now() self.start_time = datetime.now()
self.end_time = self.start_time + timedelta(milliseconds=self.duration) if self.duration:
self.end_time = (
self.start_time + timedelta(milliseconds=self.duration))
class Window(QMainWindow, Ui_MainWindow): class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None) -> None: def __init__(self, parent=None, *args, **kwargs) -> None:
super().__init__(parent) super().__init__(*args, **kwargs)
self.setupUi(self) self.setupUi(self)
self.timer: QTimer = QTimer() self.timer: QTimer = QTimer()
@ -198,8 +224,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.next_track = PlaylistTrack() self.next_track = PlaylistTrack()
self.previous_track = PlaylistTrack() self.previous_track = PlaylistTrack()
self.previous_track_position: Optional[int] = None self.previous_track_position: Optional[float] = None
self.selected_plrs = None self.selected_plrs: Optional[List[PlaylistRows]] = None
# Set colours that will be used by playlist row stripes # Set colours that will be used by playlist row stripes
palette = QPalette() palette = QPalette()
@ -239,6 +265,7 @@ class Window(QMainWindow, Ui_MainWindow):
colour = Config.COLOUR_CART_READY colour = Config.COLOUR_CART_READY
btn.path = cart.path btn.path = cart.path
btn.player = self.music.VLC.media_player_new(cart.path) btn.player = self.music.VLC.media_player_new(cart.path)
if btn.player:
btn.player.audio_set_volume(Config.VOLUME_VLC_DEFAULT) btn.player.audio_set_volume(Config.VOLUME_VLC_DEFAULT)
if cart.enabled: if cart.enabled:
btn.setEnabled(True) btn.setEnabled(True)
@ -249,16 +276,24 @@ class Window(QMainWindow, Ui_MainWindow):
colour = Config.COLOUR_CART_UNCONFIGURED colour = Config.COLOUR_CART_UNCONFIGURED
btn.setStyleSheet("background-color: " + colour + ";\n") btn.setStyleSheet("background-color: " + colour + ";\n")
if cart.name is not None:
btn.setText(cart.name) btn.setText(cart.name)
def cart_click(self) -> None: def cart_click(self) -> None:
"""Handle cart click""" """Handle cart click"""
btn = self.sender() btn = self.sender()
if not isinstance(btn, CartButton):
return
if helpers.file_is_readable(btn.path): if helpers.file_is_readable(btn.path):
# Don't allow clicks while we're playing # Don't allow clicks while we're playing
btn.setEnabled(False) btn.setEnabled(False)
if not btn.player:
log.debug(
f"musicmuster.cart_click(): no player assigned ({btn=})")
return
btn.player.play() btn.player.play()
btn.is_playing = True btn.is_playing = True
colour = Config.COLOUR_CART_PLAYING colour = Config.COLOUR_CART_PLAYING
@ -268,7 +303,7 @@ class Window(QMainWindow, Ui_MainWindow):
else: else:
colour = Config.COLOUR_CART_ERROR colour = Config.COLOUR_CART_ERROR
btn.setStyleSheet("background-color: " + colour + ";\n") btn.setStyleSheet("background-color: " + colour + ";\n")
btn.pgb.minimum = 0 btn.pgb.setMinimum(0)
def cart_edit(self, btn: CartButton, event: QEvent): def cart_edit(self, btn: CartButton, event: QEvent):
"""Handle context menu for cart button""" """Handle context menu for cart button"""
@ -276,10 +311,10 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
cart = session.query(Carts).get(btn.cart_id) cart = session.query(Carts).get(btn.cart_id)
if cart is None: if cart is None:
log.ERROR("cart_edit: cart not found") log.error("cart_edit: cart not found")
return return
dlg = CartDialog(parent=self, session=session, cart=cart) dlg = CartDialog(musicmuster=self, session=session, cart=cart)
if dlg.exec(): if dlg.exec():
name = dlg.ui.lineEditName.text() name = dlg.ui.lineEditName.text()
if not name: if not name:
@ -324,6 +359,9 @@ class Window(QMainWindow, Ui_MainWindow):
def cart_progressbar(self, btn: CartButton) -> None: def cart_progressbar(self, btn: CartButton) -> None:
"""Manage progress bar""" """Manage progress bar"""
if not btn.duration:
return
ms = 0 ms = 0
btn.pgb.setMaximum(btn.duration) btn.pgb.setMaximum(btn.duration)
while ms <= btn.duration: while ms <= btn.duration:
@ -437,6 +475,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
playlist_id = self.tabPlaylist.widget(tab_index).playlist_id playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
if playlist:
playlist.close(session) playlist.close(session)
# Close playlist and remove tab # Close playlist and remove tab
@ -498,10 +537,8 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_name: Optional[str] = None) -> Playlists: playlist_name: Optional[str] = None) -> Playlists:
"""Create new playlist""" """Create new playlist"""
if not playlist_name: while not playlist_name:
playlist_name = self.solicit_playlist_name() playlist_name = self.solicit_playlist_name()
if not playlist_name:
return
playlist = Playlists(session, playlist_name) playlist = Playlists(session, playlist_name)
return playlist return playlist
@ -521,6 +558,8 @@ class Window(QMainWindow, Ui_MainWindow):
add tab to display. Return index number of tab. add tab to display. Return index number of tab.
""" """
assert playlist.id
playlist_tab = PlaylistTab( playlist_tab = PlaylistTab(
musicmuster=self, session=session, playlist_id=playlist.id) musicmuster=self, session=session, playlist_id=playlist.id)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
@ -622,6 +661,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Repaint playlist to remove currently playing track colour # Repaint playlist to remove currently playing track colour
# What was current track is now previous track # What was current track is now previous track
with Session() as session: with Session() as session:
if self.previous_track.playlist_tab:
self.previous_track.playlist_tab.update_display(session) self.previous_track.playlist_tab.update_display(session)
# Reset clocks # Reset clocks
@ -638,8 +678,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_track_length.setText( self.label_track_length.setText(
helpers.ms_to_mmss(self.next_track.duration) helpers.ms_to_mmss(self.next_track.duration)
) )
if self.next_track.silence_at and self.next_track.fade_at:
self.label_fade_length.setText(helpers.ms_to_mmss( self.label_fade_length.setText(helpers.ms_to_mmss(
self.next_track.silence_at - self.next_track.fade_at)) self.next_track.silence_at - self.next_track.fade_at))
else:
self.label_fade_length.setText("0:00")
else: else:
self.label_track_length.setText("0:00") self.label_track_length.setText("0:00")
self.label_fade_length.setText("0:00") self.label_fade_length.setText("0:00")
@ -662,6 +705,9 @@ class Window(QMainWindow, Ui_MainWindow):
# Get output filename # Get output filename
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
if not playlist:
return
pathspec = QFileDialog.getSaveFileName( pathspec = QFileDialog.getSaveFileName(
self, 'Save Playlist', self, 'Save Playlist',
directory=f"{playlist.name}.m3u", directory=f"{playlist.name}.m3u",
@ -679,6 +725,8 @@ class Window(QMainWindow, Ui_MainWindow):
# Required directive on first line # Required directive on first line
f.write("#EXTM3U\n") f.write("#EXTM3U\n")
for track in [a.track for a in plrs]: for track in [a.track for a in plrs]:
if track.duration is None:
track.duration = 0
f.write( f.write(
"#EXTINF:" "#EXTINF:"
f"{int(track.duration / 1000)}," f"{int(track.duration / 1000)},"
@ -707,6 +755,7 @@ class Window(QMainWindow, Ui_MainWindow):
git_tag = str(exc_info.output) git_tag = str(exc_info.output)
with Session() as session: with Session() as session:
if session.bind:
dbname = session.bind.engine.url.database dbname = session.bind.engine.url.database
QMessageBox.information( QMessageBox.information(
@ -721,7 +770,9 @@ class Window(QMainWindow, Ui_MainWindow):
dlg = DbDialog(self, session, get_one_track=True) dlg = DbDialog(self, session, get_one_track=True)
if dlg.exec(): if dlg.exec():
return dlg.ui.track return dlg.track
else:
return None
def hide_played(self): def hide_played(self):
"""Toggle hide played tracks""" """Toggle hide played tracks"""
@ -832,6 +883,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
for playlist in Playlists.get_open(session): for playlist in Playlists.get_open(session):
if playlist:
_ = self.create_playlist_tab(session, playlist) _ = self.create_playlist_tab(session, playlist)
# Set active tab # Set active tab
record = Settings.get_int_settings(session, "active_tab") record = Settings.get_int_settings(session, "active_tab")
@ -858,7 +910,10 @@ class Window(QMainWindow, Ui_MainWindow):
self.next_track.plr_id] self.next_track.plr_id]
] ]
rows_to_delete = [plr.row_number for plr in plrs_to_move] rows_to_delete = [plr.row_number for plr in plrs_to_move
if plr.row_number is not None]
if not rows_to_delete:
return
# Identify destination playlist # Identify destination playlist
playlists = [] playlists = []
@ -912,11 +967,13 @@ class Window(QMainWindow, Ui_MainWindow):
Move selected rows to another playlist Move selected rows to another playlist
""" """
selected_plrs = self.visible_playlist_tab().get_selected_playlistrows(
session)
if not selected_plrs:
return
with Session() as session: with Session() as session:
self.move_playlist_rows( self.move_playlist_rows(session, selected_plrs)
session,
self.visible_playlist_tab().get_selected_playlistrows(session)
)
def move_tab(self, frm: int, to: int) -> None: def move_tab(self, frm: int, to: int) -> None:
"""Handle tabs being moved""" """Handle tabs being moved"""
@ -954,6 +1011,8 @@ class Window(QMainWindow, Ui_MainWindow):
return return
playlist = Playlists.create_playlist_from_template( playlist = Playlists.create_playlist_from_template(
session, template, playlist_name) session, template, playlist_name)
if not playlist:
return
tab_index = self.create_playlist_tab(session, playlist) tab_index = self.create_playlist_tab(session, playlist)
playlist.mark_open(session, tab_index) playlist.mark_open(session, tab_index)
@ -1007,7 +1066,10 @@ class Window(QMainWindow, Ui_MainWindow):
plr.row_number = row plr.row_number = row
row += 1 row += 1
session.commit() if not src_playlist_id:
return
session.flush()
# Update display # Update display
self.visible_playlist_tab().populate_display( self.visible_playlist_tab().populate_display(
@ -1063,8 +1125,9 @@ class Window(QMainWindow, Ui_MainWindow):
# Ensure playlist tabs are the correct colour # Ensure playlist tabs are the correct colour
# If next track is on a different playlist_tab to the # If next track is on a different playlist_tab to the
# current track, reset the current track playlist_tab colour # current track, reset the current track playlist_tab colour
if self.current_track.playlist_tab != self.next_track.playlist_tab: current_tab = self.current_track.playlist_tab
self.set_tab_colour(self.current_track.playlist_tab, if current_tab and current_tab != self.next_track.playlist_tab:
self.set_tab_colour(current_tab,
QColor(Config.COLOUR_NORMAL_TAB)) QColor(Config.COLOUR_NORMAL_TAB))
# Move next track to current track. # Move next track to current track.
@ -1073,9 +1136,17 @@ class Window(QMainWindow, Ui_MainWindow):
self.current_track = self.next_track self.current_track = self.next_track
self.next_track = PlaylistTrack() self.next_track = PlaylistTrack()
if not self.current_track.track_id:
log.debug("musicmuster.play_next(): no id for next track")
return
if not self.current_track.path:
log.debug("musicmuster.play_next(): no path for next track")
return
# Set current track playlist_tab colour # Set current track playlist_tab colour
self.set_tab_colour(self.current_track.playlist_tab, if current_tab:
QColor(Config.COLOUR_CURRENT_TAB)) self.set_tab_colour(
current_tab, QColor(Config.COLOUR_CURRENT_TAB))
# Restore volume if -3dB active # Restore volume if -3dB active
if self.btnDrop3db.isChecked(): if self.btnDrop3db.isChecked():
@ -1089,6 +1160,7 @@ class Window(QMainWindow, Ui_MainWindow):
Playdates(session, self.current_track.track_id) Playdates(session, self.current_track.track_id)
# Tell playlist track is now playing # Tell playlist track is now playing
if self.current_track.playlist_tab:
self.current_track.playlist_tab.play_started(session) self.current_track.playlist_tab.play_started(session)
# Note that track is now playing # Note that track is now playing
@ -1116,11 +1188,14 @@ class Window(QMainWindow, Ui_MainWindow):
) )
self.label_fade_length.setText( self.label_fade_length.setText(
helpers.ms_to_mmss(self.current_track.fade_length)) helpers.ms_to_mmss(self.current_track.fade_length))
if self.current_track.start_time:
self.label_start_time.setText( self.label_start_time.setText(
self.current_track.start_time.strftime( self.current_track.start_time.strftime(
Config.TRACK_TIME_FORMAT)) Config.TRACK_TIME_FORMAT))
if self.current_track.end_time:
self.label_end_time.setText( self.label_end_time.setText(
self.current_track.end_time.strftime(Config.TRACK_TIME_FORMAT)) self.current_track.end_time.strftime(
Config.TRACK_TIME_FORMAT))
def resume(self) -> None: def resume(self) -> None:
""" """
@ -1157,6 +1232,8 @@ class Window(QMainWindow, Ui_MainWindow):
# Reset next track if there was one # Reset next track if there was one
if original_next_plr_id: if original_next_plr_id:
next_plr = session.get(PlaylistRows, original_next_plr_id) next_plr = session.get(PlaylistRows, original_next_plr_id)
if not next_plr or not original_next_plr_playlist_tab:
return
self.this_is_the_next_playlist_row( self.this_is_the_next_playlist_row(
session, next_plr, original_next_plr_playlist_tab) session, next_plr, original_next_plr_playlist_tab)
@ -1308,6 +1385,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.stop() self.music.stop()
# Reset playlist_tab colour # Reset playlist_tab colour
if self.current_track.playlist_tab:
if self.current_track.playlist_tab == self.next_track.playlist_tab: if self.current_track.playlist_tab == self.next_track.playlist_tab:
self.set_tab_colour(self.current_track.playlist_tab, self.set_tab_colour(self.current_track.playlist_tab,
QColor(Config.COLOUR_NEXT_TAB)) QColor(Config.COLOUR_NEXT_TAB))
@ -1366,6 +1444,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.next_track = PlaylistTrack() self.next_track = PlaylistTrack()
self.next_track.set_plr(session, plr, playlist_tab) self.next_track.set_plr(session, plr, playlist_tab)
if self.next_track.playlist_tab:
self.next_track.playlist_tab.update_display(session) self.next_track.playlist_tab.update_display(session)
if self.current_track.playlist_tab != self.next_track.playlist_tab: if self.current_track.playlist_tab != self.next_track.playlist_tab:
self.set_tab_colour(self.next_track.playlist_tab, self.set_tab_colour(self.next_track.playlist_tab,
@ -1505,14 +1584,14 @@ class Window(QMainWindow, Ui_MainWindow):
class CartDialog(QDialog): class CartDialog(QDialog):
"""Edit cart details""" """Edit cart details"""
def __init__(self, parent: QMainWindow, session: scoped_session, def __init__(self, musicmuster: Window, session: scoped_session,
cart: Carts) -> None: cart: Carts, *args, **kwargs) -> None:
""" """
Manage carts Manage carts
""" """
super().__init__(parent) super().__init__(*args, **kwargs)
self.parent = parent self.musicmuster = musicmuster
self.session = session self.session = session
self.ui = Ui_DialogCartEdit() self.ui = Ui_DialogCartEdit()
@ -1522,7 +1601,7 @@ class CartDialog(QDialog):
self.ui.lineEditName.setText(cart.name) self.ui.lineEditName.setText(cart.name)
self.ui.chkEnabled.setChecked(cart.enabled) self.ui.chkEnabled.setChecked(cart.enabled)
self.ui.windowTitle = "Edit Cart " + str(cart.id) self.setWindowTitle("Edit Cart " + str(cart.id))
self.ui.btnFile.clicked.connect(self.choose_file) self.ui.btnFile.clicked.connect(self.choose_file)
@ -1542,8 +1621,8 @@ class CartDialog(QDialog):
class DbDialog(QDialog): class DbDialog(QDialog):
"""Select track from database""" """Select track from database"""
def __init__(self, parent: Window, session: scoped_session, def __init__(self, musicmuster: Window, session: scoped_session,
get_one_track: bool = False) -> None: get_one_track: bool = False, *args, **kwargs) -> None:
""" """
Subclassed QDialog to manage track selection Subclassed QDialog to manage track selection
@ -1552,7 +1631,8 @@ class DbDialog(QDialog):
to be added to the playlist. to be added to the playlist.
""" """
super().__init__(parent) super().__init__(*args, **kwargs)
self.musicmuster = musicmuster
self.session = session self.session = session
self.get_one_track = get_one_track self.get_one_track = get_one_track
self.ui = Ui_Dialog() self.ui = Ui_Dialog()
@ -1564,6 +1644,7 @@ class DbDialog(QDialog):
self.ui.matchList.itemSelectionChanged.connect(self.selection_changed) self.ui.matchList.itemSelectionChanged.connect(self.selection_changed)
self.ui.radioTitle.toggled.connect(self.title_artist_toggle) self.ui.radioTitle.toggled.connect(self.title_artist_toggle)
self.ui.searchString.textEdited.connect(self.chars_typed) self.ui.searchString.textEdited.connect(self.chars_typed)
self.track: Optional[Tracks] = None
if get_one_track: if get_one_track:
self.ui.txtNote.hide() self.ui.txtNote.hide()
@ -1609,19 +1690,19 @@ class DbDialog(QDialog):
"""Add passed track to playlist on screen""" """Add passed track to playlist on screen"""
if self.get_one_track: if self.get_one_track:
self.ui.track = track self.track = track
self.accept() self.accept()
return return
if track: if track:
self.parent().visible_playlist_tab().insert_track( self.musicmuster.visible_playlist_tab().insert_track(
self.session, track, note=self.ui.txtNote.text()) self.session, track, note=self.ui.txtNote.text())
else: else:
self.parent().visible_playlist_tab().insert_header( self.musicmuster.visible_playlist_tab().insert_header(
self.session, note=self.ui.txtNote.text()) self.session, note=self.ui.txtNote.text())
# Save to database (which will also commit changes) # Save to database (which will also commit changes)
self.parent().visible_playlist_tab().save_playlist(self.session) self.musicmuster.visible_playlist_tab().save_playlist(self.session)
# Clear note field and select search text to make it easier for # Clear note field and select search text to make it easier for
# next search # next search
self.ui.txtNote.clear() self.ui.txtNote.clear()
@ -1684,7 +1765,7 @@ class DbDialog(QDialog):
class DownloadCSV(QDialog): class DownloadCSV(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(*args, **kwargs)
self.ui = Ui_DateSelect() self.ui = Ui_DateSelect()
self.ui.setupUi(self) self.ui.setupUi(self)
@ -1696,7 +1777,7 @@ class DownloadCSV(QDialog):
class SelectPlaylistDialog(QDialog): class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None, playlists=None, session=None): def __init__(self, parent=None, playlists=None, session=None):
super().__init__(parent) super().__init__(*args, **kwargs)
if playlists is None: if playlists is None:
return return

View File

@ -5,7 +5,7 @@ import threading
from collections import namedtuple from collections import namedtuple
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import cast, List, Optional from typing import cast, List, Optional, TYPE_CHECKING
from PyQt5.QtCore import ( from PyQt5.QtCore import (
pyqtSignal, pyqtSignal,
@ -59,6 +59,9 @@ from models import (
NoteColours NoteColours
) )
if TYPE_CHECKING:
from musicmuster import Window
start_time_re = re.compile(r"@\d\d:\d\d:\d\d") start_time_re = re.compile(r"@\d\d:\d\d:\d\d")
HEADER_NOTES_COLUMN = 2 HEADER_NOTES_COLUMN = 2
MINIMUM_ROW_HEIGHT = 30 MINIMUM_ROW_HEIGHT = 30
@ -129,10 +132,11 @@ class PlaylistTab(QTableWidget):
ROW_DURATION = Qt.UserRole + 2 ROW_DURATION = Qt.UserRole + 2
PLAYLISTROW_ID = Qt.UserRole + 3 PLAYLISTROW_ID = Qt.UserRole + 3
def __init__(self, musicmuster: QMainWindow, session: scoped_session, def __init__(self, musicmuster: Window,
session: scoped_session,
playlist_id: int, *args, **kwargs) -> None: playlist_id: int, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.musicmuster = musicmuster self.musicmuster: Window = musicmuster
self.playlist_id = playlist_id self.playlist_id = playlist_id
self.menu: Optional[QMenu] = None self.menu: Optional[QMenu] = None
@ -154,7 +158,7 @@ class PlaylistTab(QTableWidget):
# Header row # Header row
for idx in [a for a in range(len(columns))]: for idx in [a for a in range(len(columns))]:
item: QTableWidgetItem = QTableWidgetItem() item = QTableWidgetItem()
self.setHorizontalHeaderItem(idx, item) self.setHorizontalHeaderItem(idx, item)
self.horizontalHeader().setMinimumSectionSize(0) self.horizontalHeader().setMinimumSectionSize(0)
# Set column headings sorted by idx # Set column headings sorted by idx
@ -183,7 +187,7 @@ class PlaylistTab(QTableWidget):
self.itemSelectionChanged.connect(self._select_event) self.itemSelectionChanged.connect(self._select_event)
self.search_text: str = "" self.search_text: str = ""
self.edit_cell_type = None self.edit_cell_type: Optional[int]
self.selecting_in_progress = False self.selecting_in_progress = False
# Connect signals # Connect signals
self.horizontalHeader().sectionResized.connect(self._column_resize) self.horizontalHeader().sectionResized.connect(self._column_resize)
@ -212,7 +216,9 @@ class PlaylistTab(QTableWidget):
rows: List = sorted(set(item.row() for item in self.selectedItems())) rows: List = sorted(set(item.row() for item in self.selectedItems()))
rows_to_move = [ rows_to_move = [
[QTableWidgetItem(self.item(row_index, column_index)) for [QTableWidgetItem(
self.item(row_index, column_index) # type: ignore
) for
column_index in range(self.columnCount())] column_index in range(self.columnCount())]
for row_index in rows for row_index in rows
] ]
@ -407,10 +413,14 @@ class PlaylistTab(QTableWidget):
# change cell again (metadata) # change cell again (metadata)
self.cellChanged.disconnect(self._cell_changed) self.cellChanged.disconnect(self._cell_changed)
new_text = self.item(row, column).text().strip() cell = self.item(row, column)
if not cell:
return
new_text = cell.text().strip()
# Update cell with strip()'d text # Update cell with strip()'d text
self.item(row, column).setText(new_text) cell.setText(new_text)
track_id = self._get_row_track_id(row) track_id = self._get_row_track_id(row)
@ -419,6 +429,8 @@ class PlaylistTab(QTableWidget):
# Get playlistrow object # Get playlistrow object
plr_id = self._get_playlistrow_id(row) plr_id = self._get_playlistrow_id(row)
plr_item = session.get(PlaylistRows, plr_id) plr_item = session.get(PlaylistRows, plr_id)
if not plr_item:
return
# Note any updates needed to PlaylistTrack objects # Note any updates needed to PlaylistTrack objects
update_current = self.musicmuster.current_track.plr_id == plr_id update_current = self.musicmuster.current_track.plr_id == plr_id
@ -473,7 +485,7 @@ class PlaylistTab(QTableWidget):
super(PlaylistTab, self).closeEditor(editor, hint) super(PlaylistTab, self).closeEditor(editor, hint)
def edit(self, index: QModelIndex, def edit(self, index: QModelIndex, # type: ignore # FIXME
trigger: QAbstractItemView.EditTrigger, trigger: QAbstractItemView.EditTrigger,
event: QEvent) -> bool: event: QEvent) -> bool:
""" """
@ -492,7 +504,6 @@ class PlaylistTab(QTableWidget):
if track_row: if track_row:
# If a track row, we only allow editing of title, artist and # If a track row, we only allow editing of title, artist and
# note. Check that this column is one of those. # note. Check that this column is one of those.
self.edit_cell_type = None
if column in [TITLE, ARTIST, ROW_NOTES]: if column in [TITLE, ARTIST, ROW_NOTES]:
self.edit_cell_type = column self.edit_cell_type = column
else: else:
@ -523,6 +534,8 @@ class PlaylistTab(QTableWidget):
plr_id = self._get_playlistrow_id(row) plr_id = self._get_playlistrow_id(row)
plr_item = session.get(PlaylistRows, plr_id) plr_item = session.get(PlaylistRows, plr_id)
item = self.item(row, note_column) item = self.item(row, note_column)
if not plr_item or not plr_item.note or not item:
return False
item.setText(plr_item.note) item.setText(plr_item.note)
# Connect signal so we know when cell has changed. # Connect signal so we know when cell has changed.
@ -563,6 +576,8 @@ class PlaylistTab(QTableWidget):
""" """
plr_ids = self.get_selected_playlistrow_ids() plr_ids = self.get_selected_playlistrow_ids()
if not plr_ids:
return None
return [session.get(PlaylistRows, a) for a in plr_ids] return [session.get(PlaylistRows, a) for a in plr_ids]
def insert_header(self, session: scoped_session, note: str, def insert_header(self, session: scoped_session, note: str,
@ -588,6 +603,9 @@ class PlaylistTab(QTableWidget):
Insert passed playlist row (plr) into playlist tab. Insert passed playlist row (plr) into playlist tab.
""" """
if plr.row_number is None:
return
row = plr.row_number row = plr.row_number
self.insertRow(row) self.insertRow(row)
@ -604,44 +622,46 @@ class PlaylistTab(QTableWidget):
start_gap = plr.track.start_gap start_gap = plr.track.start_gap
except AttributeError: except AttributeError:
return return
start_gap_item = QTableWidgetItem(str(start_gap)) start_gap_item = self._set_item_text(
row, START_GAP, str(start_gap))
if start_gap and start_gap >= 500: if start_gap and start_gap >= 500:
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START)) start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
self.setItem(row, START_GAP, start_gap_item)
title_item = QTableWidgetItem(plr.track.title) track_title = plr.track.title
self.setItem(row, TITLE, title_item) if not track_title:
track_title = ""
_ = self._set_item_text(row, TITLE, track_title)
artist_item = QTableWidgetItem(plr.track.artist) track_artist = plr.track.artist
self.setItem(row, ARTIST, artist_item) if not track_artist:
track_artist = ""
_ = self._set_item_text(row, ARTIST, track_artist)
duration_item = QTableWidgetItem( _ = self._set_item_text(row, DURATION,
ms_to_mmss(plr.track.duration)) ms_to_mmss(plr.track.duration))
self.setItem(row, DURATION, duration_item) if plr.track.duration:
self._set_row_duration(row, plr.track.duration) self._set_row_duration(row, plr.track.duration)
start_item = QTableWidgetItem() _ = self._set_item_text(row, START_TIME, "")
self.setItem(row, START_TIME, start_item)
end_item = QTableWidgetItem() _ = self._set_item_text(row, END_TIME, "")
self.setItem(row, END_TIME, end_item)
if plr.track.bitrate: if plr.track.bitrate:
bitrate = str(plr.track.bitrate) bitrate = str(plr.track.bitrate)
else: else:
bitrate = "" bitrate = ""
bitrate_item = QTableWidgetItem(bitrate) _ = self._set_item_text(row, BITRATE, bitrate)
self.setItem(row, BITRATE, bitrate_item)
# As we have track info, any notes should be contained in # As we have track info, any notes should be contained in
# the notes column # the notes column
notes_item = QTableWidgetItem(plr.note) plr_note = plr.note
self.setItem(row, ROW_NOTES, notes_item) if not plr_note:
plr_note = ""
_ = self._set_item_text(row, ROW_NOTES, plr_note)
last_playtime = Playdates.last_played(session, plr.track.id) last_playtime = Playdates.last_played(session, plr.track.id)
last_played_str = get_relative_date(last_playtime) last_played_str = get_relative_date(last_playtime)
last_played_item = QTableWidgetItem(last_played_str) _ = self._set_item_text(row, LASTPLAYED, last_played_str)
self.setItem(row, LASTPLAYED, last_played_item)
else: else:
# This is a section header so it must have note text # This is a section header so it must have note text
@ -661,8 +681,7 @@ class PlaylistTab(QTableWidget):
continue continue
self.setItem(row, i, QTableWidgetItem()) self.setItem(row, i, QTableWidgetItem())
self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1) self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
notes_item = QTableWidgetItem(plr.note) _ = self._set_item_text(row, HEADER_NOTES_COLUMN, plr.note)
self.setItem(row, HEADER_NOTES_COLUMN, notes_item)
# Save (no) track_id # Save (no) track_id
userdata_item.setData(self.ROW_TRACK_ID, 0) userdata_item.setData(self.ROW_TRACK_ID, 0)
@ -773,8 +792,9 @@ class PlaylistTab(QTableWidget):
# Scroll to top # Scroll to top
if scroll_to_top: if scroll_to_top:
scroll_to: QTableWidgetItem = self.item(0, 0) row0_item = self.item(0, 0)
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop) if row0_item:
self.scrollToItem(row0_item, QAbstractItemView.PositionAtTop)
# Set widths # Set widths
self._set_column_widths(session) self._set_column_widths(session)
@ -818,8 +838,8 @@ class PlaylistTab(QTableWidget):
# Now build a dictionary of # Now build a dictionary of
# {display_row_number: display_row_plr} # {display_row_number: display_row_plr}
plr_dict_by_id = PlaylistRows.indexed_by_id(session, plr_dict_by_id = PlaylistRows.indexed_by_id(
display_plr_ids.values()) session, iter(display_plr_ids.values())) # type: ignore # FIXME
# Finally a dictionary of # Finally a dictionary of
# {display_row_number: plr} # {display_row_number: plr}
@ -835,20 +855,23 @@ class PlaylistTab(QTableWidget):
# that's not in the displayed playlist need to be deleted. # that's not in the displayed playlist need to be deleted.
# Ensure changes flushed # Ensure changes flushed
session.commit() session.flush()
PlaylistRows.delete_plrids_not_in_list(session, self.playlist_id, PlaylistRows.delete_plrids_not_in_list(
display_plr_ids.values()) session, self.playlist_id,
iter(display_plr_ids.values())) # type: ignore # FIXME
def scroll_current_to_top(self) -> None: def scroll_current_to_top(self) -> None:
"""Scroll currently-playing row to top""" """Scroll currently-playing row to top"""
current_row = self._get_current_track_row_number() current_row = self._get_current_track_row_number()
if current_row is not None:
self._scroll_to_top(current_row) self._scroll_to_top(current_row)
def scroll_next_to_top(self) -> None: def scroll_next_to_top(self) -> None:
"""Scroll nextly-playing row to top""" """Scroll nextly-playing row to top"""
next_row = self._get_next_track_row_number() next_row = self._get_next_track_row_number()
if next_row is not None:
self._scroll_to_top(next_row) self._scroll_to_top(next_row)
def set_search(self, text: str) -> None: def set_search(self, text: str) -> None:
@ -1003,8 +1026,14 @@ class PlaylistTab(QTableWidget):
# Extract note text from database to ignore section timings # Extract note text from database to ignore section timings
playlist_row = session.get(PlaylistRows, playlist_row = session.get(PlaylistRows,
self._get_playlistrow_id(row)) self._get_playlistrow_id(row))
if not playlist_row:
continue
note_text = playlist_row.note note_text = playlist_row.note
if not note_text:
note_text = ""
# Get note colour # Get note colour
note_colour = None
if note_text:
note_colour = NoteColours.get_colour(session, note_text) note_colour = NoteColours.get_colour(session, note_text)
# Get track if there is one # Get track if there is one
@ -1022,9 +1051,9 @@ class PlaylistTab(QTableWidget):
else: else:
note_text = f"track_id {missing_track} not found" note_text = f"track_id {missing_track} not found"
playlist_row.note = note_text playlist_row.note = note_text
session.commit() session.flush()
note_item = QTableWidgetItem(note_text) _ = self._set_item_text(row, HEADER_NOTES_COLUMN,
self.setItem(row, HEADER_NOTES_COLUMN, note_item) note_text)
if track: if track:
# Reset colour in case it was current/next/unplayable # Reset colour in case it was current/next/unplayable
@ -1042,15 +1071,16 @@ class PlaylistTab(QTableWidget):
# Colour any note # Colour any note
if note_colour: if note_colour:
(self.item(row, ROW_NOTES) notes_item = self.item(row, ROW_NOTES)
.setBackground(QColor(note_colour))) if notes_item:
notes_item.setBackground(QColor(note_colour))
# Highlight low bitrates # Highlight low bitrates
if track.bitrate: if track.bitrate:
bitrate_str = str(track.bitrate) bitrate_str = str(track.bitrate)
bitrate_item = self.item(row, BITRATE) bitrate_item = self._set_item_text(
if bitrate_item.text() != bitrate_str: row, BITRATE, str(track.bitrate))
bitrate_item.setText(bitrate_str) if bitrate_item:
if track.bitrate < Config.BITRATE_LOW_THRESHOLD: if track.bitrate < Config.BITRATE_LOW_THRESHOLD:
cell_colour = Config.COLOUR_BITRATE_LOW cell_colour = Config.COLOUR_BITRATE_LOW
elif track.bitrate < Config.BITRATE_OK_THRESHOLD: elif track.bitrate < Config.BITRATE_OK_THRESHOLD:
@ -1058,13 +1088,15 @@ class PlaylistTab(QTableWidget):
else: else:
cell_colour = Config.COLOUR_BITRATE_OK cell_colour = Config.COLOUR_BITRATE_OK
brush = QBrush(QColor(cell_colour)) brush = QBrush(QColor(cell_colour))
self.item(row, BITRATE).setBackground(brush) bitrate_item.setBackground(brush)
# Render playing track # Render playing track
if row == current_row: if row == current_row:
# Set last played time to "Today" # Set last played time to "Today"
self.item(row, LASTPLAYED).setText("Today") self._set_item_text(
row, LASTPLAYED, Config.LAST_PLAYED_TODAY_STRING)
# Calculate next_start_time # Calculate next_start_time
if track.duration:
next_start_time = self._calculate_end_time( next_start_time = self._calculate_end_time(
self.musicmuster.current_track.start_time, self.musicmuster.current_track.start_time,
track.duration track.duration
@ -1093,8 +1125,9 @@ class PlaylistTab(QTableWidget):
start_time = next_start_time start_time = next_start_time
self._set_row_start_time(row, start_time) self._set_row_start_time(row, start_time)
# Calculate next_start_time # Calculate next_start_time
next_start_time = self._calculate_end_time(start_time, if track.duration:
track.duration) next_start_time = self._calculate_end_time(
start_time, track.duration)
# Set end time # Set end time
self._set_row_end_time(row, next_start_time) self._set_row_end_time(row, next_start_time)
# Set colour # Set colour
@ -1106,8 +1139,8 @@ class PlaylistTab(QTableWidget):
if row in played: if row in played:
# Played today, so update last played column # Played today, so update last played column
self.item(row, LASTPLAYED).setText( self._set_item_text(
Config.LAST_PLAYED_TODAY_STRING) row, LASTPLAYED, Config.LAST_PLAYED_TODAY_STRING)
if self.musicmuster.hide_played_tracks: if self.musicmuster.hide_played_tracks:
self.hideRow(row) self.hideRow(row)
else: else:
@ -1117,8 +1150,9 @@ class PlaylistTab(QTableWidget):
# Set start/end times as we haven't played it yet # Set start/end times as we haven't played it yet
if next_start_time: if next_start_time:
self._set_row_start_time(row, next_start_time) self._set_row_start_time(row, next_start_time)
if track.duration:
next_start_time = self._calculate_end_time( next_start_time = self._calculate_end_time(
next_start_time, track.duration) start_time, track.duration)
# Set end time # Set end time
self._set_row_end_time(row, next_start_time) self._set_row_end_time(row, next_start_time)
else: else:
@ -1176,28 +1210,34 @@ class PlaylistTab(QTableWidget):
# Add track to playlist row # Add track to playlist row
plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) plr = session.get(PlaylistRows, self._get_playlistrow_id(row))
if not plr:
return
plr.track_id = track.id plr.track_id = track.id
session.commit() session.flush()
# Reset row span # Reset row span
for column in range(len(columns)): for column in range(len(columns)):
self.setSpan(row, column, 1, 1) self.setSpan(row, column, 1, 1)
# Update attributes of row # Update attributes of row
self.item(row, USERDATA).setData(self.ROW_TRACK_ID, track.id) userdata_item = self.item(row, USERDATA)
start_gap_item = self.item(row, START_GAP) if not userdata_item:
start_gap_item.setText(str(track.start_gap)) userdata_item = QTableWidgetItem()
if track.start_gap and track.start_gap >= 500: userdata_item.setData(self.ROW_TRACK_ID, track.id)
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
self.item(row, TITLE).setText(str(track.title))
self.item(row, ARTIST).setText(str(track.artist))
self.item(row, DURATION).setText(ms_to_mmss(track.duration))
last_playtime = Playdates.last_played(session, track.id) last_playtime = Playdates.last_played(session, track.id)
last_played_str = get_relative_date(last_playtime) last_played_str = get_relative_date(last_playtime)
self.item(row, LASTPLAYED).setText(last_played_str) _ = self._set_item_text(row, LASTPLAYED, last_played_str)
self.item(row, ROW_NOTES).setText(plr.note)
self.update_display(session) _ = self._set_item_text(row, ROW_NOTES, plr.note)
start_gap_item = self._set_item_text(row, START_GAP,
track.start_gap)
if track.start_gap and track.start_gap >= 500:
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
self._update_row(session, row, track)
def _calculate_end_time(self, start: Optional[datetime], def _calculate_end_time(self, start: Optional[datetime],
duration: int) -> Optional[datetime]: duration: int) -> Optional[datetime]:
@ -1246,7 +1286,7 @@ class PlaylistTab(QTableWidget):
with Session() as session: with Session() as session:
track = session.get(Tracks, track_id) track = session.get(Tracks, track_id)
if track: if track and track.path:
# Escape single quotes and spaces in name # Escape single quotes and spaces in name
path = track.path path = track.path
pathq = path.replace("'", "\\'") pathq = path.replace("'", "\\'")
@ -1266,6 +1306,8 @@ class PlaylistTab(QTableWidget):
# Delete rows from database # Delete rows from database
plr_ids = self.get_selected_playlistrow_ids() plr_ids = self.get_selected_playlistrow_ids()
if not plr_ids:
return
# Get confirmation # Get confirmation
row_count = len(plr_ids) row_count = len(plr_ids)
@ -1292,7 +1334,8 @@ class PlaylistTab(QTableWidget):
else index.row()) else index.row())
def _find_next_track_row(self, session: scoped_session, def _find_next_track_row(self, session: scoped_session,
starting_row: int = None) -> Optional[int]: starting_row: Optional[int] = None) \
-> Optional[int]:
""" """
Find next track to play. If a starting row is given, start there; Find next track to play. If a starting row is given, start there;
otherwise, start from top. Skip rows already played. otherwise, start from top. Skip rows already played.
@ -1315,6 +1358,8 @@ class PlaylistTab(QTableWidget):
] ]
for row in range(starting_row, self.rowCount()): for row in range(starting_row, self.rowCount()):
plr = self._get_playlistrow_object(session, row) plr = self._get_playlistrow_object(session, row)
if not plr:
continue
if ( if (
row not in track_rows or row not in track_rows or
row in played_rows or row in played_rows or
@ -1330,12 +1375,18 @@ class PlaylistTab(QTableWidget):
"""Return current track row or None""" """Return current track row or None"""
current_track = self.musicmuster.current_track current_track = self.musicmuster.current_track
if not current_track or not current_track.plr_id:
return None
return self._plrid_to_row_number(current_track.plr_id) return self._plrid_to_row_number(current_track.plr_id)
def _get_next_track_row_number(self) -> Optional[int]: def _get_next_track_row_number(self) -> Optional[int]:
"""Return next track row or None""" """Return next track row or None"""
next_track = self.musicmuster.next_track next_track = self.musicmuster.next_track
if not next_track or not next_track.plr_id:
return None
return self._plrid_to_row_number(next_track.plr_id) return self._plrid_to_row_number(next_track.plr_id)
@staticmethod @staticmethod
@ -1355,18 +1406,27 @@ class PlaylistTab(QTableWidget):
except ValueError: except ValueError:
return None return None
def _get_playlistrow_id(self, row: int) -> int: def _get_playlistrow_id(self, row: int) -> Optional[int]:
"""Return the playlistrow_id associated with this row""" """Return the playlistrow_id associated with this row"""
playlistrow_id = (self.item(row, USERDATA).data(self.PLAYLISTROW_ID)) userdata_item = self.item(row, USERDATA)
if not userdata_item:
return None
return playlistrow_id return userdata_item.data(self.PLAYLISTROW_ID)
def _get_playlistrow_object(self, session: scoped_session, def _get_playlistrow_object(self, session: scoped_session,
row: int) -> PlaylistRows: row: int) -> Optional[PlaylistRows]:
"""Return the playlistrow object associated with this row""" """Return the playlistrow object associated with this row"""
playlistrow_id = (self.item(row, USERDATA).data(self.PLAYLISTROW_ID)) userdata_item = self.item(row, USERDATA)
if not userdata_item:
return None
playlistrow_id = userdata_item.data(self.PLAYLISTROW_ID)
if not playlistrow_id:
return None
return session.get(PlaylistRows, playlistrow_id) return session.get(PlaylistRows, playlistrow_id)
def _get_row_artist(self, row: int) -> Optional[str]: def _get_row_artist(self, row: int) -> Optional[str]:
@ -1377,12 +1437,19 @@ class PlaylistTab(QTableWidget):
return None return None
item_artist = self.item(row, ARTIST) item_artist = self.item(row, ARTIST)
if not item_artist:
return None
return item_artist.text() return item_artist.text()
def _get_row_duration(self, row: int) -> int: def _get_row_duration(self, row: int) -> int:
"""Return duration associated with this row""" """Return duration associated with this row"""
duration = (self.item(row, USERDATA).data(self.ROW_DURATION)) userdata_item = self.item(row, USERDATA)
if not userdata_item:
return 0
duration = userdata_item.data(self.ROW_DURATION)
if duration: if duration:
return duration return duration
else: else:
@ -1396,17 +1463,21 @@ class PlaylistTab(QTableWidget):
item_note = self.item(row, ROW_NOTES) item_note = self.item(row, ROW_NOTES)
else: else:
item_note = self.item(row, HEADER_NOTES_COLUMN) item_note = self.item(row, HEADER_NOTES_COLUMN)
if not item_note:
return None
return item_note.text() return item_note.text()
def _get_row_start_time(self, row: int) -> Optional[datetime]: def _get_row_start_time(self, row: int) -> Optional[datetime]:
try: """Return row start time as string or None"""
if self.item(row, START_TIME):
return datetime.strptime(self.item( start_time_item = self.item(row, START_TIME)
row, START_TIME).text(), if not start_time_item:
Config.NOTE_TIME_FORMAT
)
else:
return None return None
try:
return datetime.strptime(start_time_item.text(),
Config.NOTE_TIME_FORMAT)
except ValueError: except ValueError:
return None return None
@ -1418,16 +1489,22 @@ class PlaylistTab(QTableWidget):
return None return None
item_title = self.item(row, TITLE) item_title = self.item(row, TITLE)
if not item_title:
return None
return item_title.text() return item_title.text()
def _get_row_track_id(self, row: int) -> int: def _get_row_track_id(self, row: int) -> int:
"""Return the track_id associated with this row or None""" """Return the track_id associated with this row or None"""
userdata_item = self.item(row, USERDATA)
if not userdata_item:
return 0
try: try:
track_id = (self.item(row, USERDATA) track_id = userdata_item.data(self.ROW_TRACK_ID)
.data(self.ROW_TRACK_ID))
except AttributeError: except AttributeError:
return None return 0
return track_id return track_id
@ -1496,6 +1573,9 @@ class PlaylistTab(QTableWidget):
new_row_number: int) -> None: new_row_number: int) -> None:
"""Move playlist row to new_row_number using parent copy/paste""" """Move playlist row to new_row_number using parent copy/paste"""
if plr.row_number is None:
return
# Remove source row # Remove source row
self.removeRow(plr.row_number) self.removeRow(plr.row_number)
# Fixup plr row number # Fixup plr row number
@ -1535,6 +1615,12 @@ class PlaylistTab(QTableWidget):
) )
return return
if track.path is None:
log.error(
f"playlists._open_in_audacity({track_id=}): "
"Track has no path"
)
else:
open_in_audacity(track.path) open_in_audacity(track.path)
def _plrid_to_row_number(self, plrid: int) -> Optional[int]: def _plrid_to_row_number(self, plrid: int) -> Optional[int]:
@ -1559,6 +1645,9 @@ class PlaylistTab(QTableWidget):
# Update playlist_rows record # Update playlist_rows record
with Session() as session: with Session() as session:
plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) plr = session.get(PlaylistRows, self._get_playlistrow_id(row))
if not plr:
return
plr.track_id = None plr.track_id = None
# We can't have null text # We can't have null text
if not plr.note: if not plr.note:
@ -1567,15 +1656,17 @@ class PlaylistTab(QTableWidget):
# Clear track text items # Clear track text items
for i in range(2, len(columns)): for i in range(2, len(columns)):
self.item(row, i).setText("") _ = self._set_item_text(row, i, "")
# Remove row duration # Remove row duration
self._set_row_duration(row, 0) self._set_row_duration(row, 0)
# Remote track_id from row # Remote track_id from row
self.item(row, USERDATA).setData(self.ROW_TRACK_ID, 0) userdata_item = self.item(row, USERDATA)
if userdata_item:
userdata_item.setData(self.ROW_TRACK_ID, 0)
# Span the rows # Span the rows
self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1) self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
# Set note text in correct column for section head # Set note text in correct column for section head
self.item(row, HEADER_NOTES_COLUMN).setText(plr.note) _ = self._set_item_text(row, HEADER_NOTES_COLUMN, plr.note)
# And refresh display # And refresh display
self.update_display(session) self.update_display(session)
@ -1730,6 +1821,19 @@ class PlaylistTab(QTableWidget):
else: else:
self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH) self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH)
def _set_item_text(self, row, column, text) -> QTableWidgetItem:
"""
Set text for item if it exists, else create it, and return item
"""
item = self.item(row, column)
if not item:
item = QTableWidgetItem(text)
self.setItem(row, column, item)
else:
item.setText(text)
return item
def _set_next(self, session: scoped_session, row_number: int) -> None: def _set_next(self, session: scoped_session, row_number: int) -> None:
""" """
Set passed row as next playlist row to play. Set passed row as next playlist row to play.
@ -1759,6 +1863,9 @@ class PlaylistTab(QTableWidget):
# Notify musicmuster # Notify musicmuster
plr = session.get(PlaylistRows, self._get_playlistrow_id(row_number)) plr = session.get(PlaylistRows, self._get_playlistrow_id(row_number))
if not plr:
log.debug(f"playists._set_next({row_number=}) can't retrieve plr")
else:
self.musicmuster.this_is_the_next_playlist_row(session, plr, self) self.musicmuster.this_is_the_next_playlist_row(session, plr, self)
# Update display # Update display
@ -1787,8 +1894,9 @@ class PlaylistTab(QTableWidget):
for column in range(self.columnCount()): for column in range(self.columnCount()):
if column == ROW_NOTES: if column == ROW_NOTES:
continue continue
if self.item(row, column): item = self.item(row, column)
self.item(row, column).setFont(boldfont) if item:
item.setFont(boldfont)
def _set_row_colour(self, row: int, def _set_row_colour(self, row: int,
colour: Optional[QColor] = None) -> None: colour: Optional[QColor] = None) -> None:
@ -1807,21 +1915,25 @@ class PlaylistTab(QTableWidget):
# Don't change colour on start gap columns # Don't change colour on start gap columns
if column == START_GAP: if column == START_GAP:
continue continue
if self.item(row, column): item = self.item(row, column)
self.item(row, column).setBackground(brush) if item:
item.setBackground(brush)
def _set_row_duration(self, row: int, ms: int) -> None: def _set_row_duration(self, row: int, ms: int) -> None:
"""Set duration of this row in row metadata""" """Set duration of this row in row metadata"""
self.item(row, USERDATA).setData(self.ROW_DURATION, ms) item = self.item(row, USERDATA)
if item:
item.setData(self.ROW_DURATION, ms)
def _set_row_end_time(self, row: int, time: Optional[datetime]) -> None: def _set_row_end_time(self, row: int, time: Optional[datetime]) -> None:
"""Set passed row end time to passed time""" """Set passed row end time to passed time"""
try: try:
time_str = time.strftime(Config.TRACK_TIME_FORMAT) time_str = time.strftime(Config.TRACK_TIME_FORMAT) # type: ignore
except AttributeError: except AttributeError:
time_str = "" time_str = ""
item = QTableWidgetItem(time_str) item = QTableWidgetItem(time_str)
self.setItem(row, END_TIME, item) self.setItem(row, END_TIME, item)
@ -1834,14 +1946,13 @@ class PlaylistTab(QTableWidget):
"""Set passed row start time to passed time""" """Set passed row start time to passed time"""
try: try:
time_str = time.strftime(Config.TRACK_TIME_FORMAT) time_str = time.strftime(Config.TRACK_TIME_FORMAT) # type: ignore
except AttributeError: except AttributeError:
time_str = "" time_str = ""
item = QTableWidgetItem(time_str) _ = self._set_item_text(row, START_TIME, time_str)
self.setItem(row, START_TIME, item)
def _get_section_timing_string(self, ms: int, def _get_section_timing_string(self, ms: int,
no_end: bool = False) -> None: no_end: bool = False) -> str:
"""Return string describing section duration""" """Return string describing section duration"""
duration = ms_to_mmss(ms) duration = ms_to_mmss(ms)

View File

@ -36,7 +36,6 @@ parent_dir = os.path.dirname(source_dir)
name_and_tags: List[str] = [] name_and_tags: List[str] = []
tags_not_name: List[str] = [] tags_not_name: List[str] = []
# multiple_similar: List[str] = [] # multiple_similar: List[str] = []
no_match: List[str] = []
# possibles: List[str] = [] # possibles: List[str] = []
no_match: int = 0 no_match: int = 0

View File

@ -41,7 +41,8 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.mypy] [tool.mypy]
mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/musicmuster-oWgGw1IG-py3.9:/home/kae/git/musicmuster/app" # mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/musicmuster-oWgGw1IG-py3.9:/home/kae/git/musicmuster/app"
mypy_path = "/home/kae/git/musicmuster/app"
plugins = "sqlalchemy.ext.mypy.plugin" plugins = "sqlalchemy.ext.mypy.plugin"
[tool.vulture] [tool.vulture]