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 datetime import datetime
from typing import List, Optional
from typing import Iterable, List, Optional
from sqlalchemy.ext.associationproxy import association_proxy
@ -95,13 +95,13 @@ class NoteColours(Base):
)
@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:
return None
return ""
for rec in session.execute(
select(NoteColours)
@ -123,7 +123,7 @@ class NoteColours(Base):
if rec.substring.lower() in text.lower():
return rec.colour
return None
return ""
class Playdates(Base):
@ -196,7 +196,7 @@ class Playlists(Base):
is_template = Column(Boolean, default=False, nullable=False)
query = Column(String(256), default=None, nullable=True, unique=False)
deleted = Column(Boolean, default=False, nullable=False)
rows: "PlaylistRows" = relationship(
rows: List["PlaylistRows"] = relationship(
"PlaylistRows",
back_populates="playlist",
cascade="all, delete-orphan",
@ -370,7 +370,7 @@ class PlaylistRows(Base):
def __init__(self,
session: scoped_session,
playlist_id: int,
track_id: int,
track_id: Optional[int],
row_number: int,
note: Optional[str] = None
) -> None:
@ -409,7 +409,7 @@ class PlaylistRows(Base):
@staticmethod
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
than 'maxrow'
@ -469,7 +469,7 @@ class PlaylistRows(Base):
@classmethod
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
have been played.
@ -488,7 +488,7 @@ class PlaylistRows(Base):
@classmethod
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
contain tracks
@ -526,24 +526,8 @@ class PlaylistRows(Base):
return plrs
@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)
)
@staticmethod
def indexed_by_id(session: scoped_session, plr_ids: List[int]) -> dict:
def indexed_by_id(session: scoped_session,
plr_ids: Iterable[int]) -> dict:
"""
Return a dictionary of playlist_rows indexed by their plr id from
the passed plr_id list.
@ -562,6 +546,23 @@ class PlaylistRows(Base):
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):
"""Manage settings"""

View File

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

View File

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

View File

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

View File

@ -41,7 +41,8 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[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"
[tool.vulture]