Compare commits

..

No commits in common. "0c38fc2ef45cb53e4fcd518422d9ba76add13c89" and "f182f49f15df2a8dbdb85f192e8846e2b44db1c3" have entirely different histories.

7 changed files with 327 additions and 588 deletions

0
app/__init__.py Normal file
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 Iterable, List, Optional, Union, ValuesView
from typing import List, Optional
from sqlalchemy.ext.associationproxy import association_proxy
@ -30,10 +30,7 @@ from sqlalchemy.orm import (
relationship,
)
from sqlalchemy.orm.exc import (
NoResultFound,
)
from sqlalchemy.exc import (
IntegrityError,
NoResultFound
)
from config import Config
from helpers import (
@ -98,13 +95,13 @@ class NoteColours(Base):
)
@staticmethod
def get_colour(session: scoped_session, text: str) -> str:
def get_colour(session: scoped_session, text: str) -> Optional[str]:
"""
Parse text and return colour string if matched, else empty string
Parse text and return colour string if matched, else None
"""
if not text:
return ""
return None
for rec in session.execute(
select(NoteColours)
@ -126,7 +123,7 @@ class NoteColours(Base):
if rec.substring.lower() in text.lower():
return rec.colour
return ""
return None
class Playdates(Base):
@ -199,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: List["PlaylistRows"] = relationship(
rows: "PlaylistRows" = relationship(
"PlaylistRows",
back_populates="playlist",
cascade="all, delete-orphan",
@ -373,7 +370,7 @@ class PlaylistRows(Base):
def __init__(self,
session: scoped_session,
playlist_id: int,
track_id: Optional[int],
track_id: int,
row_number: int,
note: Optional[str] = None
) -> None:
@ -411,9 +408,8 @@ class PlaylistRows(Base):
plr.note)
@staticmethod
def delete_plrids_not_in_list(
session: scoped_session, playlist_id: int,
plr_ids: Union[Iterable[int], ValuesView]) -> None:
def delete_plrids_not_in_list(session: scoped_session, playlist_id: int,
plrids: List["PlaylistRows"]) -> None:
"""
Delete rows in given playlist that have a higher row number
than 'maxrow'
@ -423,7 +419,7 @@ class PlaylistRows(Base):
delete(PlaylistRows)
.where(
PlaylistRows.playlist_id == playlist_id,
PlaylistRows.id.not_in(plr_ids)
PlaylistRows.id.not_in(plrids)
)
)
# Delete won't take effect until commit()
@ -473,7 +469,7 @@ class PlaylistRows(Base):
@classmethod
def get_played_rows(cls, session: scoped_session,
playlist_id: int) -> List["PlaylistRows"]:
playlist_id: int) -> List[int]:
"""
For passed playlist, return a list of rows that
have been played.
@ -492,7 +488,7 @@ class PlaylistRows(Base):
@classmethod
def get_rows_with_tracks(cls, session: scoped_session,
playlist_id: int) -> List["PlaylistRows"]:
playlist_id: int) -> List[int]:
"""
For passed playlist, return a list of rows that
contain tracks
@ -530,8 +526,24 @@ class PlaylistRows(Base):
return plrs
@staticmethod
def indexed_by_id(session: scoped_session,
plr_ids: Union[Iterable[int], ValuesView]) -> dict:
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:
"""
Return a dictionary of playlist_rows indexed by their plr id from
the passed plr_id list.
@ -550,23 +562,6 @@ 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"""
@ -657,16 +652,8 @@ class Tracks(Base):
self.mtime = mtime
self.lastplayed = lastplayed
try:
session.add(self)
session.commit()
except IntegrityError as error:
session.rollback()
log.error(
f"Error importing track ({title=}, "
f"{title=}, {artist=}, {path=}, {error=})"
)
raise ValueError
session.add(self)
session.commit()
@classmethod
def get_all(cls, session) -> List["Tracks"]:

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python
from log import log
from os.path import basename
import argparse
import stackprinter # type: ignore
import subprocess
@ -10,31 +9,10 @@ import threading
from datetime import datetime, timedelta
from time import sleep
from typing import (
Callable,
cast,
List,
Optional,
)
from typing import Callable, List, Optional
from PyQt5.QtCore import (
pyqtSignal,
QDate,
QEvent,
QObject,
Qt,
QSize,
QThread,
QTime,
QTimer,
)
from PyQt5.QtGui import (
QColor,
QFont,
QMouseEvent,
QPalette,
QResizeEvent,
)
from PyQt5.QtCore import pyqtSignal, QDate, QEvent, Qt, QSize, QTime, QTimer
from PyQt5.QtGui import QColor, QFont, QPalette, QResizeEvent
from PyQt5.QtWidgets import (
QApplication,
QDialog,
@ -49,13 +27,10 @@ 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,
@ -80,11 +55,12 @@ class CartButton(QPushButton):
progress = pyqtSignal(int)
def __init__(self, musicmuster: "Window", cart: Carts, *args, **kwargs):
def __init__(self, parent: QMainWindow, cart: Carts):
"""Create a cart pushbutton and set it disabled"""
super().__init__(*args, **kwargs)
self.musicmuster = musicmuster
super().__init__(parent)
# Next line is redundant (check)
# self.parent = parent
self.cart_id = cart.id
if cart.path and cart.enabled and not cart.duration:
tags = helpers.get_tags(cart.path)
@ -101,8 +77,7 @@ class CartButton(QPushButton):
self.setFont(font)
self.setObjectName("cart_" + str(cart.cart_number))
self.pgb = QProgressBar(self)
self.pgb.setTextVisible(False)
self.pgb = QProgressBar(self, textVisible=False)
self.pgb.setVisible(False)
palette = self.pgb.palette()
palette.setColor(QPalette.Highlight,
@ -125,9 +100,8 @@ class CartButton(QPushButton):
"""Allow right click even when button is disabled"""
if event.type() == QEvent.MouseButtonRelease:
mouse_event = cast(QMouseEvent, event)
if mouse_event.button() == Qt.RightButton:
self.musicmuster.cart_edit(self, event)
if event.button() == Qt.RightButton:
self.parent.cart_edit(self, event)
return True
return super().event(event)
@ -162,7 +136,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[int] = None
self.silence_at: Optional[datetime] = None
self.start_gap: Optional[int] = None
self.start_time: Optional[datetime] = None
self.title: Optional[str] = None
@ -189,6 +163,7 @@ 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
@ -198,59 +173,18 @@ 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()
if self.duration:
self.end_time = (
self.start_time + timedelta(milliseconds=self.duration))
class ImportTrack(QObject):
import_error = pyqtSignal(str)
importing = pyqtSignal(str)
finished = pyqtSignal(PlaylistTab)
def __init__(self, playlist: PlaylistTab, filenames: list) -> None:
super().__init__()
self.filenames = filenames
self.playlist = playlist
def run(self):
"""
Create track objects from passed files and add to visible playlist
"""
with Session() as session:
for fname in self.filenames:
self.importing.emit(f"Importing {basename(fname)}")
try:
track = Tracks(session, fname)
except ValueError:
self.import_error.emit(basename(fname))
continue
helpers.set_track_metadata(session, track)
helpers.normalise_track(track.path)
self.playlist.insert_track(session, track)
# We're importing potentially multiple tracks in a loop.
# If there's an error adding the track to the Tracks
# table, the session will rollback, thus losing any
# previous additions in this loop. So, commit now to
# lock in what we've just done.
session.commit()
self.playlist.save_playlist(session)
self.finished.emit(self.playlist)
self.end_time = self.start_time + timedelta(milliseconds=self.duration)
class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setupUi(self)
self.timer: QTimer = QTimer()
@ -263,8 +197,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.next_track = PlaylistTrack()
self.previous_track = PlaylistTrack()
self.previous_track_position: Optional[float] = None
self.selected_plrs: Optional[List[PlaylistRows]] = None
self.previous_track_position: Optional[int] = None
self.selected_plrs = None
# Set colours that will be used by playlist row stripes
palette = QPalette()
@ -304,8 +238,7 @@ class Window(QMainWindow, Ui_MainWindow):
colour = Config.COLOUR_CART_READY
btn.path = 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:
btn.setEnabled(True)
btn.pgb.setVisible(True)
@ -315,24 +248,16 @@ class Window(QMainWindow, Ui_MainWindow):
colour = Config.COLOUR_CART_UNCONFIGURED
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:
"""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
@ -342,7 +267,7 @@ class Window(QMainWindow, Ui_MainWindow):
else:
colour = Config.COLOUR_CART_ERROR
btn.setStyleSheet("background-color: " + colour + ";\n")
btn.pgb.setMinimum(0)
btn.pgb.minimum = 0
def cart_edit(self, btn: CartButton, event: QEvent):
"""Handle context menu for cart button"""
@ -350,10 +275,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(musicmuster=self, session=session, cart=cart)
dlg = CartDialog(parent=self, session=session, cart=cart)
if dlg.exec():
name = dlg.ui.lineEditName.text()
if not name:
@ -398,9 +323,6 @@ 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:
@ -514,8 +436,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session:
playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
playlist = session.get(Playlists, playlist_id)
if playlist:
playlist.close(session)
playlist.close(session)
# Close playlist and remove tab
self.tabPlaylist.widget(tab_index).close()
@ -576,8 +497,10 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_name: Optional[str] = None) -> Playlists:
"""Create new playlist"""
while not playlist_name:
if not playlist_name:
playlist_name = self.solicit_playlist_name()
if not playlist_name:
return
playlist = Playlists(session, playlist_name)
return playlist
@ -597,8 +520,6 @@ 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)
@ -700,8 +621,7 @@ 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:
if self.previous_track.playlist_tab:
self.previous_track.playlist_tab.update_display(session)
self.previous_track.playlist_tab.update_display(session)
# Reset clocks
self.frame_fade.setStyleSheet("")
@ -717,11 +637,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_track_length.setText(
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.next_track.silence_at - self.next_track.fade_at))
else:
self.label_fade_length.setText("0:00")
self.label_fade_length.setText(helpers.ms_to_mmss(
self.next_track.silence_at - self.next_track.fade_at))
else:
self.label_track_length.setText("0:00")
self.label_fade_length.setText("0:00")
@ -744,9 +661,6 @@ 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",
@ -764,8 +678,6 @@ 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)},"
@ -794,8 +706,7 @@ class Window(QMainWindow, Ui_MainWindow):
git_tag = str(exc_info.output)
with Session() as session:
if session.bind:
dbname = session.bind.engine.url.database
dbname = session.bind.engine.url.database
QMessageBox.information(
self,
@ -809,9 +720,7 @@ class Window(QMainWindow, Ui_MainWindow):
dlg = DbDialog(self, session, get_one_track=True)
if dlg.exec():
return dlg.track
else:
return None
return dlg.ui.track
def hide_played(self):
"""Toggle hide played tracks"""
@ -845,7 +754,7 @@ class Window(QMainWindow, Ui_MainWindow):
for fname in dlg.selectedFiles():
txt = ""
tags = helpers.get_tags(fname)
new_tracks.append(fname)
new_tracks.append((fname, tags))
title = tags['title']
artist = tags['artist']
count = 0
@ -873,32 +782,22 @@ class Window(QMainWindow, Ui_MainWindow):
return
# Import in separate thread
self.import_thread = QThread()
self.worker = ImportTrack(self.visible_playlist_tab(), new_tracks)
self.worker.moveToThread(self.import_thread)
self.import_thread.started.connect(self.worker.run)
self.worker.finished.connect(self.import_thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.import_thread.finished.connect(self.import_thread.deleteLater)
self.worker.import_error.connect(
lambda msg: helpers.show_warning(
"Import error", "Error importing " + msg
)
)
self.worker.importing.connect(
lambda msg: self.statusbar.showMessage("Importing " + msg, 5000)
)
self.worker.finished.connect(self.import_complete)
self.import_thread.start()
thread = threading.Thread(target=self._import_tracks,
args=(new_tracks,))
thread.start()
def import_complete(self, playlist_tab: PlaylistTab):
def _import_tracks(self, tracks: list):
"""
Called by thread when track import complete
Create track objects from passed files and add to visible playlist
"""
self.statusbar.showMessage("Imports complete")
with Session() as session:
playlist_tab.update_display(session)
for (fname, tags) in tracks:
track = Tracks(session, fname)
helpers.set_track_metadata(session, track)
helpers.normalise_track(track.path)
self.visible_playlist_tab().insert_track(session, track)
self.visible_playlist_tab().save_playlist(session)
def insert_header(self) -> None:
"""Show dialog box to enter header text and add to playlist"""
@ -932,8 +831,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as 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
record = Settings.get_int_settings(session, "active_tab")
if record and record.f_int >= 0:
@ -955,14 +853,11 @@ 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
if plr.row_number is not None]
if not rows_to_delete:
return
rows_to_delete = [plr.row_number for plr in plrs_to_move]
# Identify destination playlist
playlists = []
@ -1016,13 +911,11 @@ 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, selected_plrs)
self.move_playlist_rows(
session,
self.visible_playlist_tab().get_selected_playlistrows(session)
)
def move_tab(self, frm: int, to: int) -> None:
"""Handle tabs being moved"""
@ -1060,8 +953,6 @@ 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)
@ -1115,10 +1006,7 @@ class Window(QMainWindow, Ui_MainWindow):
plr.row_number = row
row += 1
if not src_playlist_id:
return
session.flush()
session.commit()
# Update display
self.visible_playlist_tab().populate_display(
@ -1174,9 +1062,8 @@ 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
current_tab = self.current_track.playlist_tab
if current_tab and current_tab != self.next_track.playlist_tab:
self.set_tab_colour(current_tab,
if self.current_track.playlist_tab != self.next_track.playlist_tab:
self.set_tab_colour(self.current_track.playlist_tab,
QColor(Config.COLOUR_NORMAL_TAB))
# Move next track to current track.
@ -1185,17 +1072,9 @@ 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
if current_tab:
self.set_tab_colour(
current_tab, QColor(Config.COLOUR_CURRENT_TAB))
self.set_tab_colour(self.current_track.playlist_tab,
QColor(Config.COLOUR_CURRENT_TAB))
# Restore volume if -3dB active
if self.btnDrop3db.isChecked():
@ -1209,8 +1088,7 @@ class Window(QMainWindow, Ui_MainWindow):
Playdates(session, self.current_track.track_id)
# 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
self.playing = True
@ -1237,14 +1115,11 @@ class Window(QMainWindow, Ui_MainWindow):
)
self.label_fade_length.setText(
helpers.ms_to_mmss(self.current_track.fade_length))
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))
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))
def resume(self) -> None:
"""
@ -1281,8 +1156,6 @@ 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)
@ -1434,13 +1307,12 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.stop()
# Reset playlist_tab colour
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))
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()
@ -1493,11 +1365,10 @@ class Window(QMainWindow, Ui_MainWindow):
self.next_track = PlaylistTrack()
self.next_track.set_plr(session, plr, playlist_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))
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
@ -1610,26 +1481,21 @@ class Window(QMainWindow, Ui_MainWindow):
Update last / current / next track headers
"""
if self.previous_track.title and self.previous_track.artist:
if self.previous_track.title:
self.hdrPreviousTrack.setText(
f"{self.previous_track.title.replace('&', '&&')} - "
f"{self.previous_track.artist.replace('&', '&&')}"
)
f"{self.previous_track.title} - {self.previous_track.artist}")
else:
self.hdrPreviousTrack.setText("")
if self.current_track.title and self.current_track.artist:
if self.current_track.title:
self.hdrCurrentTrack.setText(
f"{self.current_track.title.replace('&', '&&')} - "
f"{self.current_track.artist.replace('&', '&&')}"
)
f"{self.current_track.title} - {self.current_track.artist}")
else:
self.hdrCurrentTrack.setText("")
if self.next_track.title and self.next_track.artist:
if self.next_track.title:
self.hdrNextTrack.setText(
f"{self.next_track.title.replace('&', '&&')} - "
f"{self.next_track.artist.replace('&', '&&')}"
f"{self.next_track.title} - {self.next_track.artist}"
)
else:
self.hdrNextTrack.setText("")
@ -1638,14 +1504,14 @@ class Window(QMainWindow, Ui_MainWindow):
class CartDialog(QDialog):
"""Edit cart details"""
def __init__(self, musicmuster: Window, session: scoped_session,
cart: Carts, *args, **kwargs) -> None:
def __init__(self, parent: QMainWindow, session: scoped_session,
cart: Carts) -> None:
"""
Manage carts
"""
super().__init__(*args, **kwargs)
self.musicmuster = musicmuster
super().__init__(parent)
self.parent = parent
self.session = session
self.ui = Ui_DialogCartEdit()
@ -1655,7 +1521,7 @@ class CartDialog(QDialog):
self.ui.lineEditName.setText(cart.name)
self.ui.chkEnabled.setChecked(cart.enabled)
self.setWindowTitle("Edit Cart " + str(cart.id))
self.ui.windowTitle = "Edit Cart " + str(cart.id)
self.ui.btnFile.clicked.connect(self.choose_file)
@ -1675,8 +1541,8 @@ class CartDialog(QDialog):
class DbDialog(QDialog):
"""Select track from database"""
def __init__(self, musicmuster: Window, session: scoped_session,
get_one_track: bool = False, *args, **kwargs) -> None:
def __init__(self, parent: QMainWindow, session: scoped_session,
get_one_track: bool = False) -> None:
"""
Subclassed QDialog to manage track selection
@ -1685,8 +1551,7 @@ class DbDialog(QDialog):
to be added to the playlist.
"""
super().__init__(*args, **kwargs)
self.musicmuster = musicmuster
super().__init__(parent)
self.session = session
self.get_one_track = get_one_track
self.ui = Ui_Dialog()
@ -1698,7 +1563,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
self.ui.track = None
if get_one_track:
self.ui.txtNote.hide()
@ -1732,7 +1597,7 @@ class DbDialog(QDialog):
item = self.ui.matchList.currentItem()
if item:
track = item.data(Qt.UserRole)
self.add_track(track)
self.add_track(track)
def add_selected_and_close(self) -> None:
"""Handle Add and Close button"""
@ -1744,19 +1609,19 @@ class DbDialog(QDialog):
"""Add passed track to playlist on screen"""
if self.get_one_track:
self.track = track
self.ui.track = track
self.accept()
return
if track:
self.musicmuster.visible_playlist_tab().insert_track(
self.parent().visible_playlist_tab().insert_track(
self.session, track, note=self.ui.txtNote.text())
else:
self.musicmuster.visible_playlist_tab().insert_header(
self.parent().visible_playlist_tab().insert_header(
self.session, note=self.ui.txtNote.text())
# Save to database (which will also commit changes)
self.musicmuster.visible_playlist_tab().save_playlist(self.session)
self.parent().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()
@ -1819,7 +1684,7 @@ class DbDialog(QDialog):
class DownloadCSV(QDialog):
def __init__(self, parent=None):
super().__init__(*args, **kwargs)
super().__init__(parent)
self.ui = Ui_DateSelect()
self.ui.setupUi(self)
@ -1831,7 +1696,7 @@ class DownloadCSV(QDialog):
class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None, playlists=None, session=None):
super().__init__(*args, **kwargs)
super().__init__(parent)
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, TYPE_CHECKING
from typing import List, Optional
from PyQt5.QtCore import (
pyqtSignal,
@ -21,7 +21,6 @@ from PyQt5.QtGui import (
QColor,
QFont,
QDropEvent,
QKeyEvent
)
from PyQt5.QtWidgets import (
QAbstractItemDelegate,
@ -59,9 +58,6 @@ 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
@ -115,12 +111,10 @@ class NoSelectDelegate(QStyledItemDelegate):
def eventFilter(self, editor: QObject, event: QEvent):
"""By default, QPlainTextEdit doesn't handle enter or return"""
if event.type() == QEvent.KeyPress:
key_event = cast(QKeyEvent, event)
if key_event.key() == Qt.Key_Return:
if key_event.modifiers() == Qt.ControlModifier:
self.commitData.emit(editor)
self.closeEditor.emit(editor)
if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Return:
if event.modifiers() == Qt.ControlModifier:
self.commitData.emit(editor)
self.closeEditor.emit(editor)
return super().eventFilter(editor, event)
@ -132,11 +126,10 @@ class PlaylistTab(QTableWidget):
ROW_DURATION = Qt.UserRole + 2
PLAYLISTROW_ID = Qt.UserRole + 3
def __init__(self, musicmuster: "Window",
session: scoped_session,
def __init__(self, musicmuster: QMainWindow, session: scoped_session,
playlist_id: int, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.musicmuster: Window = musicmuster
self.musicmuster = musicmuster
self.playlist_id = playlist_id
self.menu: Optional[QMenu] = None
@ -158,7 +151,7 @@ class PlaylistTab(QTableWidget):
# Header row
for idx in [a for a in range(len(columns))]:
item = QTableWidgetItem()
item: QTableWidgetItem = QTableWidgetItem()
self.setHorizontalHeaderItem(idx, item)
self.horizontalHeader().setMinimumSectionSize(0)
# Set column headings sorted by idx
@ -187,7 +180,7 @@ class PlaylistTab(QTableWidget):
self.itemSelectionChanged.connect(self._select_event)
self.search_text: str = ""
self.edit_cell_type: Optional[int]
self.edit_cell_type = None
self.selecting_in_progress = False
# Connect signals
self.horizontalHeader().sectionResized.connect(self._column_resize)
@ -216,9 +209,7 @@ class PlaylistTab(QTableWidget):
rows: List = sorted(set(item.row() for item in self.selectedItems()))
rows_to_move = [
[QTableWidgetItem(
self.item(row_index, column_index) # type: ignore
) for
[QTableWidgetItem(self.item(row_index, column_index)) for
column_index in range(self.columnCount())]
for row_index in rows
]
@ -413,14 +404,10 @@ class PlaylistTab(QTableWidget):
# change cell again (metadata)
self.cellChanged.disconnect(self._cell_changed)
cell = self.item(row, column)
if not cell:
return
new_text = cell.text().strip()
new_text = self.item(row, column).text().strip()
# Update cell with strip()'d text
cell.setText(new_text)
self.item(row, column).setText(new_text)
track_id = self._get_row_track_id(row)
@ -429,8 +416,6 @@ 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
@ -485,7 +470,7 @@ class PlaylistTab(QTableWidget):
super(PlaylistTab, self).closeEditor(editor, hint)
def edit(self, index: QModelIndex, # type: ignore # FIXME
def edit(self, index: QModelIndex,
trigger: QAbstractItemView.EditTrigger,
event: QEvent) -> bool:
"""
@ -504,6 +489,7 @@ 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:
@ -534,8 +520,6 @@ 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.
@ -576,8 +560,6 @@ 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,
@ -603,9 +585,6 @@ 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)
@ -622,46 +601,44 @@ class PlaylistTab(QTableWidget):
start_gap = plr.track.start_gap
except AttributeError:
return
start_gap_item = self._set_item_text(
row, START_GAP, str(start_gap))
start_gap_item = QTableWidgetItem(str(start_gap))
if start_gap and start_gap >= 500:
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
self.setItem(row, START_GAP, start_gap_item)
track_title = plr.track.title
if not track_title:
track_title = ""
_ = self._set_item_text(row, TITLE, track_title)
title_item = QTableWidgetItem(plr.track.title)
self.setItem(row, TITLE, title_item)
track_artist = plr.track.artist
if not track_artist:
track_artist = ""
_ = self._set_item_text(row, ARTIST, track_artist)
artist_item = QTableWidgetItem(plr.track.artist)
self.setItem(row, ARTIST, artist_item)
_ = self._set_item_text(row, DURATION,
ms_to_mmss(plr.track.duration))
if plr.track.duration:
self._set_row_duration(row, plr.track.duration)
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, START_TIME, "")
start_item = QTableWidgetItem()
self.setItem(row, START_TIME, start_item)
_ = self._set_item_text(row, END_TIME, "")
end_item = QTableWidgetItem()
self.setItem(row, END_TIME, end_item)
if plr.track.bitrate:
bitrate = str(plr.track.bitrate)
else:
bitrate = ""
_ = self._set_item_text(row, BITRATE, bitrate)
bitrate_item = QTableWidgetItem(bitrate)
self.setItem(row, BITRATE, bitrate_item)
# As we have track info, any notes should be contained in
# the notes column
plr_note = plr.note
if not plr_note:
plr_note = ""
_ = self._set_item_text(row, ROW_NOTES, plr_note)
notes_item = QTableWidgetItem(plr.note)
self.setItem(row, ROW_NOTES, notes_item)
last_playtime = Playdates.last_played(session, plr.track.id)
last_played_str = get_relative_date(last_playtime)
_ = self._set_item_text(row, LASTPLAYED, last_played_str)
last_played_item = QTableWidgetItem(last_played_str)
self.setItem(row, LASTPLAYED, last_played_item)
else:
# This is a section header so it must have note text
@ -681,7 +658,8 @@ class PlaylistTab(QTableWidget):
continue
self.setItem(row, i, QTableWidgetItem())
self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
_ = self._set_item_text(row, HEADER_NOTES_COLUMN, plr.note)
notes_item = QTableWidgetItem(plr.note)
self.setItem(row, HEADER_NOTES_COLUMN, notes_item)
# Save (no) track_id
userdata_item.setData(self.ROW_TRACK_ID, 0)
@ -792,9 +770,8 @@ class PlaylistTab(QTableWidget):
# Scroll to top
if scroll_to_top:
row0_item = self.item(0, 0)
if row0_item:
self.scrollToItem(row0_item, QAbstractItemView.PositionAtTop)
scroll_to: QTableWidgetItem = self.item(0, 0)
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop)
# Set widths
self._set_column_widths(session)
@ -838,8 +815,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,
display_plr_ids.values())
# Finally a dictionary of
# {display_row_number: plr}
@ -855,24 +832,21 @@ class PlaylistTab(QTableWidget):
# that's not in the displayed playlist need to be deleted.
# Ensure changes flushed
session.flush()
PlaylistRows.delete_plrids_not_in_list(
session, self.playlist_id,
display_plr_ids.values())
session.commit()
PlaylistRows.delete_plrids_not_in_list(session, self.playlist_id,
display_plr_ids.values())
def scroll_current_to_top(self) -> None:
"""Scroll currently-playing row to top"""
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:
"""Scroll nextly-playing row to top"""
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:
"""Set search text and find first match"""
@ -1026,15 +1000,9 @@ 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 = 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
track_id = self._get_row_track_id(row)
@ -1051,9 +1019,9 @@ class PlaylistTab(QTableWidget):
else:
note_text = f"track_id {missing_track} not found"
playlist_row.note = note_text
session.flush()
_ = self._set_item_text(row, HEADER_NOTES_COLUMN,
note_text)
session.commit()
note_item = QTableWidgetItem(note_text)
self.setItem(row, HEADER_NOTES_COLUMN, note_item)
if track:
# Reset colour in case it was current/next/unplayable
@ -1071,36 +1039,33 @@ class PlaylistTab(QTableWidget):
# Colour any note
if note_colour:
notes_item = self.item(row, ROW_NOTES)
if notes_item:
notes_item.setBackground(QColor(note_colour))
(self.item(row, ROW_NOTES)
.setBackground(QColor(note_colour)))
# Highlight low bitrates
if track.bitrate:
bitrate_str = str(track.bitrate)
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)
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)
# Render playing track
if row == current_row:
# Set last played time to "Today"
self._set_item_text(
row, LASTPLAYED, Config.LAST_PLAYED_TODAY_STRING)
self.item(row, LASTPLAYED).setText("Today")
# Calculate next_start_time
if track.duration:
next_start_time = self._calculate_end_time(
self.musicmuster.current_track.start_time,
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
@ -1125,9 +1090,8 @@ class PlaylistTab(QTableWidget):
start_time = next_start_time
self._set_row_start_time(row, start_time)
# Calculate next_start_time
if track.duration:
next_start_time = self._calculate_end_time(
start_time, 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
@ -1139,8 +1103,8 @@ class PlaylistTab(QTableWidget):
if row in played:
# Played today, so update last played column
self._set_item_text(
row, LASTPLAYED, Config.LAST_PLAYED_TODAY_STRING)
self.item(row, LASTPLAYED).setText(
Config.LAST_PLAYED_TODAY_STRING)
if self.musicmuster.hide_played_tracks:
self.hideRow(row)
else:
@ -1150,9 +1114,8 @@ 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)
if track.duration:
next_start_time = self._calculate_end_time(
start_time, track.duration)
next_start_time = self._calculate_end_time(
next_start_time, track.duration)
# Set end time
self._set_row_end_time(row, next_start_time)
else:
@ -1210,34 +1173,28 @@ 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.flush()
session.commit()
# Reset row span
for column in range(len(columns)):
self.setSpan(row, column, 1, 1)
# Update attributes of row
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._set_item_text(row, LASTPLAYED, last_played_str)
_ = self._set_item_text(row, ROW_NOTES, plr.note)
start_gap_item = self._set_item_text(row, START_GAP,
track.start_gap)
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))
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._update_row(session, row, track)
self.update_display(session)
def _calculate_end_time(self, start: Optional[datetime],
duration: int) -> Optional[datetime]:
@ -1286,7 +1243,7 @@ class PlaylistTab(QTableWidget):
with Session() as session:
track = session.get(Tracks, track_id)
if track and track.path:
if track:
# Escape single quotes and spaces in name
path = track.path
pathq = path.replace("'", "\\'")
@ -1306,8 +1263,6 @@ 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)
@ -1334,8 +1289,7 @@ class PlaylistTab(QTableWidget):
else index.row())
def _find_next_track_row(self, session: scoped_session,
starting_row: Optional[int] = None) \
-> Optional[int]:
starting_row: 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.
@ -1358,8 +1312,6 @@ 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
@ -1375,18 +1327,12 @@ 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
@ -1406,27 +1352,18 @@ class PlaylistTab(QTableWidget):
except ValueError:
return None
def _get_playlistrow_id(self, row: int) -> Optional[int]:
def _get_playlistrow_id(self, row: int) -> int:
"""Return the playlistrow_id associated with this row"""
userdata_item = self.item(row, USERDATA)
if not userdata_item:
return None
playlistrow_id = (self.item(row, USERDATA).data(self.PLAYLISTROW_ID))
return userdata_item.data(self.PLAYLISTROW_ID)
return playlistrow_id
def _get_playlistrow_object(self, session: scoped_session,
row: int) -> Optional[PlaylistRows]:
row: int) -> PlaylistRows:
"""Return the playlistrow object associated with this row"""
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
playlistrow_id = (self.item(row, USERDATA).data(self.PLAYLISTROW_ID))
return session.get(PlaylistRows, playlistrow_id)
def _get_row_artist(self, row: int) -> Optional[str]:
@ -1437,19 +1374,12 @@ 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"""
userdata_item = self.item(row, USERDATA)
if not userdata_item:
return 0
duration = userdata_item.data(self.ROW_DURATION)
duration = (self.item(row, USERDATA).data(self.ROW_DURATION))
if duration:
return duration
else:
@ -1463,21 +1393,17 @@ 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:
return datetime.strptime(start_time_item.text(),
Config.NOTE_TIME_FORMAT)
if self.item(row, START_TIME):
return datetime.strptime(self.item(
row, START_TIME).text(),
Config.NOTE_TIME_FORMAT
)
else:
return None
except ValueError:
return None
@ -1489,22 +1415,16 @@ 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 = userdata_item.data(self.ROW_TRACK_ID)
track_id = (self.item(row, USERDATA)
.data(self.ROW_TRACK_ID))
except AttributeError:
return 0
return None
return track_id
@ -1573,9 +1493,6 @@ 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
@ -1615,13 +1532,7 @@ class PlaylistTab(QTableWidget):
)
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]:
"""
@ -1645,9 +1556,6 @@ 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:
@ -1656,17 +1564,15 @@ class PlaylistTab(QTableWidget):
# Clear track text items
for i in range(2, len(columns)):
_ = self._set_item_text(row, i, "")
self.item(row, i).setText("")
# Remove row duration
self._set_row_duration(row, 0)
# Remote track_id from row
userdata_item = self.item(row, USERDATA)
if userdata_item:
userdata_item.setData(self.ROW_TRACK_ID, 0)
self.item(row, USERDATA).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._set_item_text(row, HEADER_NOTES_COLUMN, plr.note)
self.item(row, HEADER_NOTES_COLUMN).setText(plr.note)
# And refresh display
self.update_display(session)
@ -1821,19 +1727,6 @@ 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.
@ -1863,10 +1756,7 @@ class PlaylistTab(QTableWidget):
# Notify musicmuster
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
self.clear_selection()
@ -1894,9 +1784,8 @@ class PlaylistTab(QTableWidget):
for column in range(self.columnCount()):
if column == ROW_NOTES:
continue
item = self.item(row, column)
if item:
item.setFont(boldfont)
if self.item(row, column):
self.item(row, column).setFont(boldfont)
def _set_row_colour(self, row: int,
colour: Optional[QColor] = None) -> None:
@ -1915,25 +1804,21 @@ class PlaylistTab(QTableWidget):
# Don't change colour on start gap columns
if column == START_GAP:
continue
item = self.item(row, column)
if item:
item.setBackground(brush)
if self.item(row, column):
self.item(row, column).setBackground(brush)
def _set_row_duration(self, row: int, ms: int) -> None:
"""Set duration of this row in row metadata"""
item = self.item(row, USERDATA)
if item:
item.setData(self.ROW_DURATION, ms)
self.item(row, USERDATA).setData(self.ROW_DURATION, ms)
def _set_row_end_time(self, row: int, time: Optional[datetime]) -> None:
"""Set passed row end time to passed time"""
try:
time_str = time.strftime(Config.TRACK_TIME_FORMAT) # type: ignore
time_str = time.strftime(Config.TRACK_TIME_FORMAT)
except AttributeError:
time_str = ""
item = QTableWidgetItem(time_str)
self.setItem(row, END_TIME, item)
@ -1946,13 +1831,14 @@ class PlaylistTab(QTableWidget):
"""Set passed row start time to passed time"""
try:
time_str = time.strftime(Config.TRACK_TIME_FORMAT) # type: ignore
time_str = time.strftime(Config.TRACK_TIME_FORMAT)
except AttributeError:
time_str = ""
_ = self._set_item_text(row, START_TIME, time_str)
item = QTableWidgetItem(time_str)
self.setItem(row, START_TIME, item)
def _get_section_timing_string(self, ms: int,
no_end: bool = False) -> str:
no_end: bool = False) -> None:
"""Return string describing section duration"""
duration = ms_to_mmss(ms)
@ -1980,29 +1866,40 @@ class PlaylistTab(QTableWidget):
column = HEADER_NOTES_COLUMN
# Update text
if playlist_row.note:
new_text = playlist_row.note + additional_text
else:
new_text = additional_text
_ = self._set_item_text(playlist_row.row_number, column, new_text)
new_text = playlist_row.note + additional_text
# FIXME temporary workaround to issue #147
try:
self.item(playlist_row.row_number, column).setText(new_text)
except AttributeError as exc:
msg = f"Issue 147 occurred. {playlist_row=}, {additional_text=}"
msg += "\n\n"
msg += stackprinter.format(exc)
helpers.send_mail(Config.ERRORS_TO, Confit.ERRORS_FROM,
"Issue #147 from musicmuster", msg)
def _update_row(self, session, row: int, track: Tracks) -> None:
"""
Update the passed row with info from the passed track.
"""
start_gap_item = self._set_item_text(
row, START_GAP, str(track.start_gap))
if track.start_gap and track.start_gap >= 500:
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
item_startgap = self.item(row, START_GAP)
item_startgap.setText(str(track.start_gap))
if track.start_gap >= 500:
item_startgap.setBackground(QColor(Config.COLOUR_LONG_START))
else:
start_gap_item.setBackground(QColor("white"))
item_startgap.setBackground(QColor("white"))
_ = self._set_item_text(row, TITLE, track.title)
_ = self._set_item_text(row, ARTIST, track.artist)
_ = self._set_item_text(row, DURATION, track.duration)
_ = self._set_item_text(row, BITRATE, track.bitrate)
item_title = self.item(row, TITLE)
item_title.setText(track.title)
item_artist = self.item(row, ARTIST)
item_artist.setText(track.artist)
item_duration = self.item(row, DURATION)
item_duration.setText(ms_to_mmss(track.duration))
item_bitrate = self.item(row, BITRATE)
item_bitrate.setText(str(track.bitrate))
self.update_display(session)

View File

@ -36,6 +36,7 @@ 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

114
poetry.lock generated
View File

@ -1,6 +1,6 @@
[[package]]
name = "alembic"
version = "1.9.3"
version = "1.9.1"
description = "A database migration tool for SQLAlchemy."
category = "main"
optional = false
@ -67,6 +67,17 @@ category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
[[package]]
name = "commonmark"
version = "0.9.1"
description = "Python parser for the CommonMark Markdown spec"
category = "main"
optional = false
python-versions = "*"
[package.extras]
test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"]
[[package]]
name = "decorator"
version = "5.1.1"
@ -140,7 +151,7 @@ dev = ["dlint", "flake8-2020", "flake8-aaa", "flake8-absolute-import", "flake8-a
[[package]]
name = "greenlet"
version = "2.0.2"
version = "2.0.1"
description = "Lightweight in-process concurrent programming"
category = "main"
optional = false
@ -148,15 +159,15 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
[package.extras]
docs = ["sphinx", "docutils (<0.18)"]
test = ["objgraph", "psutil"]
test = ["objgraph", "psutil", "faulthandler"]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
python-versions = "*"
[[package]]
name = "ipdb"
@ -173,7 +184,7 @@ tomli = {version = "*", markers = "python_version > \"3.6\" and python_version <
[[package]]
name = "ipython"
version = "8.9.0"
version = "8.7.0"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
@ -188,7 +199,7 @@ jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
pickleshare = "*"
prompt-toolkit = ">=3.0.30,<3.1.0"
prompt-toolkit = ">=3.0.11,<3.1.0"
pygments = ">=2.4.0"
stack-data = "*"
traitlets = ">=5"
@ -256,30 +267,9 @@ babel = ["babel"]
lingua = ["lingua"]
testing = ["pytest"]
[[package]]
name = "markdown-it-py"
version = "2.1.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"]
code_style = ["pre-commit (==2.6)"]
compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"]
linkify = ["linkify-it-py (>=1.0,<2.0)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx-book-theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "markupsafe"
version = "2.1.2"
version = "2.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
@ -304,14 +294,6 @@ category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "mutagen"
version = "1.46.0"
@ -341,11 +323,11 @@ reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = ">=3.5"
python-versions = "*"
[[package]]
name = "mysqlclient"
@ -357,7 +339,7 @@ python-versions = ">=3.5"
[[package]]
name = "packaging"
version = "23.0"
version = "22.0"
description = "Core utilities for Python packages"
category = "dev"
optional = false
@ -500,7 +482,7 @@ python-versions = "*"
[[package]]
name = "pygments"
version = "2.14.0"
version = "2.13.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "main"
optional = false
@ -511,14 +493,14 @@ plugins = ["importlib-metadata"]
[[package]]
name = "pyqt5"
version = "5.15.9"
version = "5.15.7"
description = "Python bindings for the Qt cross platform application toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
PyQt5-Qt5 = ">=5.15.2"
PyQt5-Qt5 = ">=5.15.0"
PyQt5-sip = ">=12.11,<13"
[[package]]
@ -531,7 +513,7 @@ python-versions = "*"
[[package]]
name = "pyqt5-sip"
version = "12.11.1"
version = "12.11.0"
description = "The sip module support for PyQt5"
category = "main"
optional = false
@ -571,7 +553,7 @@ python-versions = "*"
[[package]]
name = "pytest"
version = "7.2.1"
version = "7.2.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
@ -636,18 +618,18 @@ python-versions = "*"
[[package]]
name = "rich"
version = "13.3.1"
version = "12.6.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false
python-versions = ">=3.7.0"
python-versions = ">=3.6.3,<4.0.0"
[package.dependencies]
markdown-it-py = ">=2.1.0,<3.0.0"
pygments = ">=2.14.0,<3.0.0"
commonmark = ">=0.9.0,<0.10.0"
pygments = ">=2.6.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[[package]]
name = "six"
@ -659,7 +641,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "sqlalchemy"
version = "1.4.46"
version = "1.4.45"
description = "Database Abstraction Library"
category = "main"
optional = false
@ -772,7 +754,7 @@ python-versions = ">=3.7"
[[package]]
name = "traitlets"
version = "5.9.0"
version = "5.8.0"
description = "Traitlets Python configuration system"
category = "dev"
optional = false
@ -784,7 +766,7 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
[[package]]
name = "types-psutil"
version = "5.9.5.6"
version = "5.9.5.5"
description = "Typing stubs for psutil"
category = "main"
optional = false
@ -800,7 +782,7 @@ python-versions = ">=3.7"
[[package]]
name = "urllib3"
version = "1.26.14"
version = "1.26.13"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
optional = false
@ -813,7 +795,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "wcwidth"
version = "0.2.6"
version = "0.2.5"
description = "Measures the displayed width of unicode strings in a terminal"
category = "dev"
optional = false
@ -834,6 +816,7 @@ backcall = [
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
colorama = []
commonmark = []
decorator = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
@ -844,20 +827,24 @@ executing = []
flake8 = []
flakehell = []
greenlet = []
iniconfig = []
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
ipdb = []
ipython = []
jedi = []
line-profiler = []
mako = []
markdown-it-py = []
markupsafe = []
matplotlib-inline = []
mccabe = []
mdurl = []
mutagen = []
mypy = []
mypy-extensions = []
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
mysqlclient = []
packaging = []
parso = [
@ -943,4 +930,7 @@ traitlets = []
types-psutil = []
typing-extensions = []
urllib3 = []
wcwidth = []
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]

View File

@ -41,8 +41,7 @@ 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/git/musicmuster/app"
mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/musicmuster-oWgGw1IG-py3.9:/home/kae/git/musicmuster/app"
plugins = "sqlalchemy.ext.mypy.plugin"
[tool.vulture]