Compare commits
7 Commits
f182f49f15
...
0c38fc2ef4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c38fc2ef4 | ||
|
|
df2652e6cc | ||
|
|
1cc1f1a185 | ||
|
|
4a6d6fa208 | ||
|
|
4f3fb6c1ae | ||
|
|
e4ef0b34c8 | ||
|
|
9e6c700644 |
@ -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, Union, ValuesView
|
||||
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
@ -30,7 +30,10 @@ from sqlalchemy.orm import (
|
||||
relationship,
|
||||
)
|
||||
from sqlalchemy.orm.exc import (
|
||||
NoResultFound
|
||||
NoResultFound,
|
||||
)
|
||||
from sqlalchemy.exc import (
|
||||
IntegrityError,
|
||||
)
|
||||
from config import Config
|
||||
from helpers import (
|
||||
@ -95,13 +98,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 +126,7 @@ class NoteColours(Base):
|
||||
if rec.substring.lower() in text.lower():
|
||||
return rec.colour
|
||||
|
||||
return None
|
||||
return ""
|
||||
|
||||
|
||||
class Playdates(Base):
|
||||
@ -196,7 +199,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 +373,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:
|
||||
@ -408,8 +411,9 @@ class PlaylistRows(Base):
|
||||
plr.note)
|
||||
|
||||
@staticmethod
|
||||
def delete_plrids_not_in_list(session: scoped_session, playlist_id: int,
|
||||
plrids: List["PlaylistRows"]) -> None:
|
||||
def delete_plrids_not_in_list(
|
||||
session: scoped_session, playlist_id: int,
|
||||
plr_ids: Union[Iterable[int], ValuesView]) -> None:
|
||||
"""
|
||||
Delete rows in given playlist that have a higher row number
|
||||
than 'maxrow'
|
||||
@ -419,7 +423,7 @@ class PlaylistRows(Base):
|
||||
delete(PlaylistRows)
|
||||
.where(
|
||||
PlaylistRows.playlist_id == playlist_id,
|
||||
PlaylistRows.id.not_in(plrids)
|
||||
PlaylistRows.id.not_in(plr_ids)
|
||||
)
|
||||
)
|
||||
# Delete won't take effect until commit()
|
||||
@ -469,7 +473,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 +492,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 +530,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: Union[Iterable[int], ValuesView]) -> dict:
|
||||
"""
|
||||
Return a dictionary of playlist_rows indexed by their plr id from
|
||||
the passed plr_id list.
|
||||
@ -562,6 +550,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"""
|
||||
@ -652,8 +657,16 @@ 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
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, session) -> List["Tracks"]:
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from log import log
|
||||
from os.path import basename
|
||||
import argparse
|
||||
import stackprinter # type: ignore
|
||||
import subprocess
|
||||
@ -9,10 +10,31 @@ 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,
|
||||
QObject,
|
||||
Qt,
|
||||
QSize,
|
||||
QThread,
|
||||
QTime,
|
||||
QTimer,
|
||||
)
|
||||
from PyQt5.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
QMouseEvent,
|
||||
QPalette,
|
||||
QResizeEvent,
|
||||
)
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
@ -27,10 +49,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 +80,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)
|
||||
@ -77,7 +101,8 @@ class CartButton(QPushButton):
|
||||
self.setFont(font)
|
||||
self.setObjectName("cart_" + str(cart.cart_number))
|
||||
|
||||
self.pgb = QProgressBar(self, textVisible=False)
|
||||
self.pgb = QProgressBar(self)
|
||||
self.pgb.setTextVisible(False)
|
||||
self.pgb.setVisible(False)
|
||||
palette = self.pgb.palette()
|
||||
palette.setColor(QPalette.Highlight,
|
||||
@ -100,8 +125,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)
|
||||
return True
|
||||
|
||||
return super().event(event)
|
||||
@ -136,7 +162,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
|
||||
@ -163,7 +189,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
|
||||
@ -173,18 +198,59 @@ 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 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)
|
||||
|
||||
|
||||
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()
|
||||
@ -197,8 +263,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()
|
||||
@ -238,6 +304,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)
|
||||
if cart.enabled:
|
||||
btn.setEnabled(True)
|
||||
@ -248,16 +315,24 @@ 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)
|
||||
|
||||
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
|
||||
@ -267,7 +342,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"""
|
||||
@ -275,10 +350,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:
|
||||
@ -323,6 +398,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:
|
||||
@ -436,6 +514,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)
|
||||
|
||||
# Close playlist and remove tab
|
||||
@ -497,10 +576,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
|
||||
@ -520,6 +597,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)
|
||||
@ -621,6 +700,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)
|
||||
|
||||
# Reset clocks
|
||||
@ -637,8 +717,11 @@ 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")
|
||||
else:
|
||||
self.label_track_length.setText("0:00")
|
||||
self.label_fade_length.setText("0:00")
|
||||
@ -661,6 +744,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",
|
||||
@ -678,6 +764,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)},"
|
||||
@ -706,6 +794,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
|
||||
|
||||
QMessageBox.information(
|
||||
@ -720,7 +809,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"""
|
||||
@ -754,7 +845,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
for fname in dlg.selectedFiles():
|
||||
txt = ""
|
||||
tags = helpers.get_tags(fname)
|
||||
new_tracks.append((fname, tags))
|
||||
new_tracks.append(fname)
|
||||
title = tags['title']
|
||||
artist = tags['artist']
|
||||
count = 0
|
||||
@ -782,22 +873,32 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
return
|
||||
|
||||
# Import in separate thread
|
||||
thread = threading.Thread(target=self._import_tracks,
|
||||
args=(new_tracks,))
|
||||
thread.start()
|
||||
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()
|
||||
|
||||
def _import_tracks(self, tracks: list):
|
||||
def import_complete(self, playlist_tab: PlaylistTab):
|
||||
"""
|
||||
Create track objects from passed files and add to visible playlist
|
||||
Called by thread when track import complete
|
||||
"""
|
||||
|
||||
self.statusbar.showMessage("Imports complete")
|
||||
with Session() as 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)
|
||||
playlist_tab.update_display(session)
|
||||
|
||||
def insert_header(self) -> None:
|
||||
"""Show dialog box to enter header text and add to playlist"""
|
||||
@ -831,6 +932,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)
|
||||
# Set active tab
|
||||
record = Settings.get_int_settings(session, "active_tab")
|
||||
@ -857,7 +959,10 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
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 = []
|
||||
@ -911,11 +1016,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"""
|
||||
@ -953,6 +1060,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)
|
||||
|
||||
@ -1006,7 +1115,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(
|
||||
@ -1062,8 +1174,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.
|
||||
@ -1072,9 +1185,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():
|
||||
@ -1088,6 +1209,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)
|
||||
|
||||
# Note that track is now playing
|
||||
@ -1115,11 +1237,14 @@ 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.current_track.end_time.strftime(
|
||||
Config.TRACK_TIME_FORMAT))
|
||||
|
||||
def resume(self) -> None:
|
||||
"""
|
||||
@ -1156,6 +1281,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)
|
||||
|
||||
@ -1307,6 +1434,7 @@ 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))
|
||||
@ -1365,6 +1493,7 @@ 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,
|
||||
@ -1481,21 +1610,26 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
Update last / current / next track headers
|
||||
"""
|
||||
|
||||
if self.previous_track.title:
|
||||
if self.previous_track.title and self.previous_track.artist:
|
||||
self.hdrPreviousTrack.setText(
|
||||
f"{self.previous_track.title} - {self.previous_track.artist}")
|
||||
f"{self.previous_track.title.replace('&', '&&')} - "
|
||||
f"{self.previous_track.artist.replace('&', '&&')}"
|
||||
)
|
||||
else:
|
||||
self.hdrPreviousTrack.setText("")
|
||||
|
||||
if self.current_track.title:
|
||||
if self.current_track.title and self.current_track.artist:
|
||||
self.hdrCurrentTrack.setText(
|
||||
f"{self.current_track.title} - {self.current_track.artist}")
|
||||
f"{self.current_track.title.replace('&', '&&')} - "
|
||||
f"{self.current_track.artist.replace('&', '&&')}"
|
||||
)
|
||||
else:
|
||||
self.hdrCurrentTrack.setText("")
|
||||
|
||||
if self.next_track.title:
|
||||
if self.next_track.title and self.next_track.artist:
|
||||
self.hdrNextTrack.setText(
|
||||
f"{self.next_track.title} - {self.next_track.artist}"
|
||||
f"{self.next_track.title.replace('&', '&&')} - "
|
||||
f"{self.next_track.artist.replace('&', '&&')}"
|
||||
)
|
||||
else:
|
||||
self.hdrNextTrack.setText("")
|
||||
@ -1504,14 +1638,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()
|
||||
@ -1521,7 +1655,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)
|
||||
|
||||
@ -1541,8 +1675,8 @@ class CartDialog(QDialog):
|
||||
class DbDialog(QDialog):
|
||||
"""Select track from database"""
|
||||
|
||||
def __init__(self, parent: QMainWindow, 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
|
||||
|
||||
@ -1551,7 +1685,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()
|
||||
@ -1563,7 +1698,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.ui.track = None
|
||||
self.track: Optional[Tracks] = None
|
||||
|
||||
if get_one_track:
|
||||
self.ui.txtNote.hide()
|
||||
@ -1609,19 +1744,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 +1819,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 +1831,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
|
||||
|
||||
345
app/playlists.py
345
app/playlists.py
@ -5,7 +5,7 @@ import threading
|
||||
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
from typing import cast, List, Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import (
|
||||
pyqtSignal,
|
||||
@ -21,6 +21,7 @@ from PyQt5.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
QDropEvent,
|
||||
QKeyEvent
|
||||
)
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemDelegate,
|
||||
@ -58,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
|
||||
@ -111,8 +115,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 and event.key() == Qt.Key_Return:
|
||||
if event.modifiers() == Qt.ControlModifier:
|
||||
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)
|
||||
|
||||
@ -126,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
|
||||
@ -151,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
|
||||
@ -180,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)
|
||||
@ -209,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
|
||||
]
|
||||
@ -404,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)
|
||||
|
||||
@ -416,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
|
||||
@ -470,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:
|
||||
"""
|
||||
@ -489,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:
|
||||
@ -520,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.
|
||||
@ -560,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,
|
||||
@ -585,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)
|
||||
|
||||
@ -601,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(
|
||||
_ = self._set_item_text(row, DURATION,
|
||||
ms_to_mmss(plr.track.duration))
|
||||
self.setItem(row, DURATION, duration_item)
|
||||
if plr.track.duration:
|
||||
self._set_row_duration(row, plr.track.duration)
|
||||
|
||||
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
|
||||
@ -658,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)
|
||||
@ -770,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)
|
||||
@ -815,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, display_plr_ids.values())
|
||||
|
||||
# Finally a dictionary of
|
||||
# {display_row_number: plr}
|
||||
@ -832,20 +855,23 @@ 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,
|
||||
session.flush()
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def set_search(self, text: str) -> None:
|
||||
@ -1000,8 +1026,14 @@ 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)
|
||||
|
||||
# Get track if there is one
|
||||
@ -1019,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
|
||||
@ -1039,15 +1071,16 @@ 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)
|
||||
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:
|
||||
@ -1055,13 +1088,15 @@ class PlaylistTab(QTableWidget):
|
||||
else:
|
||||
cell_colour = Config.COLOUR_BITRATE_OK
|
||||
brush = QBrush(QColor(cell_colour))
|
||||
self.item(row, BITRATE).setBackground(brush)
|
||||
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
|
||||
if track.duration:
|
||||
next_start_time = self._calculate_end_time(
|
||||
self.musicmuster.current_track.start_time,
|
||||
track.duration
|
||||
@ -1090,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
|
||||
@ -1103,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:
|
||||
@ -1114,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)
|
||||
if track.duration:
|
||||
next_start_time = self._calculate_end_time(
|
||||
next_start_time, track.duration)
|
||||
start_time, track.duration)
|
||||
# Set end time
|
||||
self._set_row_end_time(row, next_start_time)
|
||||
else:
|
||||
@ -1173,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]:
|
||||
@ -1243,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("'", "\\'")
|
||||
@ -1263,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)
|
||||
@ -1289,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.
|
||||
@ -1312,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
|
||||
@ -1327,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
|
||||
@ -1352,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]:
|
||||
@ -1374,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:
|
||||
@ -1393,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]:
|
||||
try:
|
||||
if self.item(row, START_TIME):
|
||||
return datetime.strptime(self.item(
|
||||
row, START_TIME).text(),
|
||||
Config.NOTE_TIME_FORMAT
|
||||
)
|
||||
else:
|
||||
"""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)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
@ -1415,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
|
||||
|
||||
@ -1493,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
|
||||
@ -1532,6 +1615,12 @@ 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)
|
||||
|
||||
def _plrid_to_row_number(self, plrid: int) -> Optional[int]:
|
||||
@ -1556,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:
|
||||
@ -1564,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)
|
||||
|
||||
@ -1727,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.
|
||||
@ -1756,6 +1863,9 @@ 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)
|
||||
|
||||
# Update display
|
||||
@ -1784,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:
|
||||
@ -1804,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)
|
||||
|
||||
@ -1831,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)
|
||||
@ -1866,40 +1980,29 @@ class PlaylistTab(QTableWidget):
|
||||
column = HEADER_NOTES_COLUMN
|
||||
|
||||
# Update text
|
||||
if playlist_row.note:
|
||||
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)
|
||||
else:
|
||||
new_text = additional_text
|
||||
|
||||
_ = self._set_item_text(playlist_row.row_number, column, new_text)
|
||||
|
||||
def _update_row(self, session, row: int, track: Tracks) -> None:
|
||||
"""
|
||||
Update the passed row with info from the passed track.
|
||||
"""
|
||||
|
||||
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))
|
||||
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))
|
||||
else:
|
||||
item_startgap.setBackground(QColor("white"))
|
||||
start_gap_item.setBackground(QColor("white"))
|
||||
|
||||
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._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)
|
||||
|
||||
self.update_display(session)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
114
poetry.lock
generated
114
poetry.lock
generated
@ -1,6 +1,6 @@
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.9.1"
|
||||
version = "1.9.3"
|
||||
description = "A database migration tool for SQLAlchemy."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -67,17 +67,6 @@ 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"
|
||||
@ -151,7 +140,7 @@ dev = ["dlint", "flake8-2020", "flake8-aaa", "flake8-absolute-import", "flake8-a
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -159,15 +148,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", "faulthandler"]
|
||||
test = ["objgraph", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "ipdb"
|
||||
@ -184,7 +173,7 @@ tomli = {version = "*", markers = "python_version > \"3.6\" and python_version <
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "8.7.0"
|
||||
version = "8.9.0"
|
||||
description = "IPython: Productive Interactive Computing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -199,7 +188,7 @@ jedi = ">=0.16"
|
||||
matplotlib-inline = "*"
|
||||
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
|
||||
pickleshare = "*"
|
||||
prompt-toolkit = ">=3.0.11,<3.1.0"
|
||||
prompt-toolkit = ">=3.0.30,<3.1.0"
|
||||
pygments = ">=2.4.0"
|
||||
stack-data = "*"
|
||||
traitlets = ">=5"
|
||||
@ -267,9 +256,30 @@ 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.1"
|
||||
version = "2.1.2"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -294,6 +304,14 @@ 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"
|
||||
@ -323,11 +341,11 @@ reports = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "0.4.3"
|
||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "mysqlclient"
|
||||
@ -339,7 +357,7 @@ python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "22.0"
|
||||
version = "23.0"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -482,7 +500,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.13.0"
|
||||
version = "2.14.0"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -493,14 +511,14 @@ plugins = ["importlib-metadata"]
|
||||
|
||||
[[package]]
|
||||
name = "pyqt5"
|
||||
version = "5.15.7"
|
||||
version = "5.15.9"
|
||||
description = "Python bindings for the Qt cross platform application toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
PyQt5-Qt5 = ">=5.15.0"
|
||||
PyQt5-Qt5 = ">=5.15.2"
|
||||
PyQt5-sip = ">=12.11,<13"
|
||||
|
||||
[[package]]
|
||||
@ -513,7 +531,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyqt5-sip"
|
||||
version = "12.11.0"
|
||||
version = "12.11.1"
|
||||
description = "The sip module support for PyQt5"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -553,7 +571,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.2.0"
|
||||
version = "7.2.1"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -618,18 +636,18 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "12.6.0"
|
||||
version = "13.3.1"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.3,<4.0.0"
|
||||
python-versions = ">=3.7.0"
|
||||
|
||||
[package.dependencies]
|
||||
commonmark = ">=0.9.0,<0.10.0"
|
||||
pygments = ">=2.6.0,<3.0.0"
|
||||
markdown-it-py = ">=2.1.0,<3.0.0"
|
||||
pygments = ">=2.14.0,<3.0.0"
|
||||
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
@ -641,7 +659,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "1.4.45"
|
||||
version = "1.4.46"
|
||||
description = "Database Abstraction Library"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -754,7 +772,7 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "traitlets"
|
||||
version = "5.8.0"
|
||||
version = "5.9.0"
|
||||
description = "Traitlets Python configuration system"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -766,7 +784,7 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
|
||||
|
||||
[[package]]
|
||||
name = "types-psutil"
|
||||
version = "5.9.5.5"
|
||||
version = "5.9.5.6"
|
||||
description = "Typing stubs for psutil"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -782,7 +800,7 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.13"
|
||||
version = "1.26.14"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -795,7 +813,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
description = "Measures the displayed width of unicode strings in a terminal"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -816,7 +834,6 @@ 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"},
|
||||
@ -827,24 +844,20 @@ executing = []
|
||||
flake8 = []
|
||||
flakehell = []
|
||||
greenlet = []
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
iniconfig = []
|
||||
ipdb = []
|
||||
ipython = []
|
||||
jedi = []
|
||||
line-profiler = []
|
||||
mako = []
|
||||
markdown-it-py = []
|
||||
markupsafe = []
|
||||
matplotlib-inline = []
|
||||
mccabe = []
|
||||
mdurl = []
|
||||
mutagen = []
|
||||
mypy = []
|
||||
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"},
|
||||
]
|
||||
mypy-extensions = []
|
||||
mysqlclient = []
|
||||
packaging = []
|
||||
parso = [
|
||||
@ -930,7 +943,4 @@ traitlets = []
|
||||
types-psutil = []
|
||||
typing-extensions = []
|
||||
urllib3 = []
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
||||
]
|
||||
wcwidth = []
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user