From 986257bef6df92c827e6b50a49641013e64b85b3 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 9 Jul 2023 16:12:21 +0100 Subject: [PATCH] Flake8 and Black run on all files --- .flake8 | 13 + InterceptEscapeWhenEditingTableCell.py | 11 +- analyse_tracks.py | 2 +- app/dbconfig.py | 14 +- app/helpers.py | 125 ++- app/infotabs.py | 16 +- app/models.py | 318 ++++--- app/music.py | 13 +- app/musicmuster.py | 510 +++++------ app/playlists.py | 572 +++++++------ app/replace_files.py | 54 +- app/utilities.py | 12 +- conftest.py | 11 +- devnotes.txt | 1 + docs/build/doctrees/environment.pickle | Bin 50839 -> 50188 bytes docs/build/doctrees/introduction.doctree | Bin 17521 -> 17104 bytes docs/build/html/_sources/introduction.rst.txt | 24 +- docs/build/html/introduction.html | 24 +- docs/build/html/searchindex.js | 2 +- docs/source/introduction.rst | 23 +- poetry.lock | 802 ++++++++++-------- pyproject.toml | 2 + test.py | 7 +- test_helpers.py | 26 +- test_models.py | 25 +- test_playlists.py | 18 +- tree.py | 24 +- web.py | 1 - 28 files changed, 1359 insertions(+), 1291 deletions(-) create mode 100644 .flake8 create mode 100644 devnotes.txt diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..2e5812e --- /dev/null +++ b/.flake8 @@ -0,0 +1,13 @@ +[flake8] +max-line-length = 88 +select = C,E,F,W,B,B950 +extend-ignore = E203, E501 +exclude = + .git + app/ui, + __pycache__, + archive, + migrations, + prof, + docs, + app/icons_rc.py diff --git a/InterceptEscapeWhenEditingTableCell.py b/InterceptEscapeWhenEditingTableCell.py index d0e3eda..e9bf50e 100755 --- a/InterceptEscapeWhenEditingTableCell.py +++ b/InterceptEscapeWhenEditingTableCell.py @@ -2,14 +2,12 @@ from PyQt6.QtCore import Qt, QEvent, QObject from PyQt6.QtWidgets import ( - QAbstractItemDelegate, QAbstractItemView, QApplication, QMainWindow, QMessageBox, QPlainTextEdit, QStyledItemDelegate, - QStyleOptionViewItem, QTableWidget, QTableWidgetItem, ) @@ -33,16 +31,15 @@ class EscapeDelegate(QStyledItemDelegate): if event.type() == QEvent.Type.KeyPress: key_event = cast(QKeyEvent, event) if key_event.key() == Qt.Key.Key_Return: - if key_event.modifiers() == ( - Qt.KeyboardModifier.ControlModifier - ): + if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier): print("save data") self.commitData.emit(editor) self.closeEditor.emit(editor) return True elif key_event.key() == Qt.Key.Key_Escape: discard_edits = QMessageBox.question( - self.parent(), "Abandon edit", "Discard changes?") + self.parent(), "Abandon edit", "Discard changes?" + ) if discard_edits == QMessageBox.StandardButton.Yes: print("abandon edit") self.closeEditor.emit(editor) @@ -74,7 +71,7 @@ class MainWindow(QMainWindow): self.table_widget.resizeRowsToContents() -if __name__ == '__main__': +if __name__ == "__main__": app = QApplication([]) window = MainWindow() window.show() diff --git a/analyse_tracks.py b/analyse_tracks.py index 4a8eb5b..a60516f 100755 --- a/analyse_tracks.py +++ b/analyse_tracks.py @@ -2,7 +2,7 @@ import os -from pydub import AudioSegment, effects +from pydub import AudioSegment # DIR = "/home/kae/git/musicmuster/archive" DIR = "/home/kae/git/musicmuster" diff --git a/app/dbconfig.py b/app/dbconfig.py index bfb5db7..8e794fe 100644 --- a/app/dbconfig.py +++ b/app/dbconfig.py @@ -1,19 +1,18 @@ import inspect -import logging import os from config import Config from contextlib import contextmanager from sqlalchemy import create_engine -from sqlalchemy.orm import (sessionmaker, scoped_session) +from sqlalchemy.orm import sessionmaker, scoped_session from typing import Generator from log import log -MYSQL_CONNECT = os.environ.get('MM_DB') +MYSQL_CONNECT = os.environ.get("MM_DB") if MYSQL_CONNECT is None: raise ValueError("MYSQL_CONNECT is undefined") else: - dbname = MYSQL_CONNECT.split('/')[-1] + dbname = MYSQL_CONNECT.split("/")[-1] log.debug(f"Database: {dbname}") # MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION') @@ -31,10 +30,10 @@ else: engine = create_engine( MYSQL_CONNECT, - encoding='utf-8', + encoding="utf-8", echo=Config.DISPLAY_SQL, pool_pre_ping=True, - future=True + future=True, ) @@ -47,8 +46,7 @@ def Session() -> Generator[scoped_session, None, None]: Session = scoped_session(sessionmaker(bind=engine, future=True)) log.debug(f"SqlA: session acquired [{hex(id(Session))}]") log.debug( - f"Session acquisition: {file}:{function}:{lineno} " - f"[{hex(id(Session))}]" + f"Session acquisition: {file}:{function}:{lineno} " f"[{hex(id(Session))}]" ) yield Session log.debug(f" SqlA: session released [{hex(id(Session))}]") diff --git a/app/helpers.py b/app/helpers.py index a63e49f..b01d2dc 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -1,4 +1,3 @@ -import numpy as np import os import psutil import shutil @@ -10,13 +9,13 @@ from config import Config from datetime import datetime from email.message import EmailMessage from log import log -from mutagen.flac import FLAC # type: ignore -from mutagen.mp3 import MP3 # type: ignore +from mutagen.flac import FLAC # type: ignore +from mutagen.mp3 import MP3 # type: ignore from pydub import AudioSegment, effects from pydub.utils import mediainfo -from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore -from tinytag import TinyTag # type: ignore -from typing import Any, Dict, Optional, Union +from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore +from tinytag import TinyTag # type: ignore +from typing import Any, Dict, Optional def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool: @@ -26,7 +25,8 @@ def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool: dlg.setWindowTitle(title) dlg.setText(question) dlg.setStandardButtons( - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) dlg.setIcon(QMessageBox.Icon.Question) if default_yes: dlg.setDefaultButton(QMessageBox.StandardButton.Yes) @@ -36,8 +36,10 @@ def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool: def fade_point( - audio_segment: AudioSegment, fade_threshold: float = 0.0, - chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: + audio_segment: AudioSegment, + fade_threshold: float = 0.0, + chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE, +) -> int: """ Returns the millisecond/index of the point where the volume drops below the maximum and doesn't get louder again. @@ -55,8 +57,9 @@ def fade_point( fade_threshold = max_vol while ( - audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold - and trim_ms > 0): # noqa W503 + audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold + and trim_ms > 0 + ): # noqa W503 trim_ms -= chunk_size # if there is no trailing silence, return lenght of track (it's less @@ -77,10 +80,10 @@ def file_is_unreadable(path: Optional[str]) -> bool: def get_audio_segment(path: str) -> Optional[AudioSegment]: try: - if path.endswith('.mp3'): + if path.endswith(".mp3"): return AudioSegment.from_mp3(path) - elif path.endswith('.flac'): - return AudioSegment.from_file(path, "flac") # type: ignore + elif path.endswith(".flac"): + return AudioSegment.from_file(path, "flac") # type: ignore except AttributeError: return None @@ -99,12 +102,13 @@ def get_tags(path: str) -> Dict[str, Any]: artist=tag.artist, bitrate=round(tag.bitrate), duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000), - path=path + path=path, ) -def get_relative_date(past_date: Optional[datetime], - reference_date: Optional[datetime] = None) -> str: +def get_relative_date( + past_date: Optional[datetime], reference_date: Optional[datetime] = None +) -> str: """ Return how long before reference_date past_date is as string. @@ -145,9 +149,10 @@ def get_relative_date(past_date: Optional[datetime], def leading_silence( - audio_segment: AudioSegment, - silence_threshold: int = Config.DBFS_SILENCE, - chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: + audio_segment: AudioSegment, + silence_threshold: int = Config.DBFS_SILENCE, + chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE, +) -> int: """ Returns the millisecond/index that the leading silence ends. audio_segment - the segment to find silence in @@ -159,9 +164,11 @@ def leading_silence( trim_ms: int = 0 # ms assert chunk_size > 0 # to avoid infinite loop - while ( - audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504 - silence_threshold and trim_ms < len(audio_segment)): + while audio_segment[ + trim_ms : trim_ms + chunk_size + ].dBFS < silence_threshold and trim_ms < len( # noqa W504 + audio_segment + ): trim_ms += chunk_size # if there is no end it should return the length of the segment @@ -175,9 +182,9 @@ def send_mail(to_addr, from_addr, subj, body): msg = EmailMessage() msg.set_content(body) - msg['Subject'] = subj - msg['From'] = from_addr - msg['To'] = to_addr + msg["Subject"] = subj + msg["From"] = from_addr + msg["To"] = to_addr # Send the message via SMTP server. context = ssl.create_default_context() @@ -194,8 +201,7 @@ def send_mail(to_addr, from_addr, subj, body): s.quit() -def ms_to_mmss(ms: Optional[int], decimals: int = 0, - negative: bool = False) -> str: +def ms_to_mmss(ms: Optional[int], decimals: int = 0, negative: bool = False) -> str: """Convert milliseconds to mm:ss""" minutes: int @@ -227,13 +233,12 @@ def normalise_track(path): # Check type ftype = os.path.splitext(path)[1][1:] - if ftype not in ['mp3', 'flac']: + if ftype not in ["mp3", "flac"]: log.info( - f"helpers.normalise_track({path}): " - f"File type {ftype} not implemented" + f"helpers.normalise_track({path}): " f"File type {ftype} not implemented" ) - bitrate = mediainfo(path)['bit_rate'] + bitrate = mediainfo(path)["bit_rate"] audio = get_audio_segment(path) if not audio: return @@ -245,23 +250,20 @@ def normalise_track(path): _, temp_path = tempfile.mkstemp() shutil.copyfile(path, temp_path) except Exception as err: - log.debug( - f"helpers.normalise_track({path}): err1: {repr(err)}" - ) + log.debug(f"helpers.normalise_track({path}): err1: {repr(err)}") return # Overwrite original file with normalised output normalised = effects.normalize(audio) try: - normalised.export(path, format=os.path.splitext(path)[1][1:], - bitrate=bitrate) + normalised.export(path, format=os.path.splitext(path)[1][1:], bitrate=bitrate) # Fix up permssions and ownership os.chown(path, stats.st_uid, stats.st_gid) os.chmod(path, stats.st_mode) # Copy tags - if ftype == 'flac': + if ftype == "flac": tag_handler = FLAC - elif ftype == 'mp3': + elif ftype == "mp3": tag_handler = MP3 else: return @@ -271,9 +273,7 @@ def normalise_track(path): dst[tag] = src[tag] dst.save() except Exception as err: - log.debug( - f"helpers.normalise_track({path}): err2: {repr(err)}" - ) + log.debug(f"helpers.normalise_track({path}): err2: {repr(err)}") # Restore original file shutil.copyfile(path, temp_path) finally: @@ -296,9 +296,9 @@ def open_in_audacity(path: str) -> bool: if not path: return False - to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid()) - from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid()) - eol: str = '\n' + to_pipe: str = "/tmp/audacity_script_pipe.to." + str(os.getuid()) + from_pipe: str = "/tmp/audacity_script_pipe.from." + str(os.getuid()) + eol: str = "\n" def send_command(command: str) -> None: """Send a single command.""" @@ -308,13 +308,13 @@ def open_in_audacity(path: str) -> bool: def get_response() -> str: """Return the command response.""" - result: str = '' - line: str = '' + result: str = "" + line: str = "" while True: result += line line = from_audacity.readline() - if line == '\n' and len(result) > 0: + if line == "\n" and len(result) > 0: break return result @@ -325,8 +325,7 @@ def open_in_audacity(path: str) -> bool: response = get_response() return response - with open(to_pipe, 'w') as to_audacity, open( - from_pipe, 'rt') as from_audacity: + with open(to_pipe, "w") as to_audacity, open(from_pipe, "rt") as from_audacity: do_command(f'Import2: Filename="{path}"') return True @@ -338,18 +337,18 @@ def set_track_metadata(session, track): t = get_tags(track.path) audio = get_audio_segment(track.path) - track.title = t['title'] - track.artist = t['artist'] - track.bitrate = t['bitrate'] + track.title = t["title"] + track.artist = t["artist"] + track.bitrate = t["bitrate"] if not audio: return track.duration = len(audio) track.start_gap = leading_silence(audio) - track.fade_at = round(fade_point(audio) / 1000, - Config.MILLISECOND_SIGFIGS) * 1000 - track.silence_at = round(trailing_silence(audio) / 1000, - Config.MILLISECOND_SIGFIGS) * 1000 + track.fade_at = round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000 + track.silence_at = ( + round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000 + ) track.mtime = os.path.getmtime(track.path) session.commit() @@ -358,20 +357,20 @@ def set_track_metadata(session, track): def show_OK(parent: QMainWindow, title: str, msg: str) -> None: """Display a message to user""" - QMessageBox.information(parent, title, msg, - buttons=QMessageBox.StandardButton.Ok) + QMessageBox.information(parent, title, msg, buttons=QMessageBox.StandardButton.Ok) def show_warning(parent: QMainWindow, title: str, msg: str) -> None: """Display a warning to user""" - QMessageBox.warning(parent, title, msg, - buttons=QMessageBox.StandardButton.Cancel) + QMessageBox.warning(parent, title, msg, buttons=QMessageBox.StandardButton.Cancel) def trailing_silence( - audio_segment: AudioSegment, silence_threshold: int = -50, - chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: + audio_segment: AudioSegment, + silence_threshold: int = -50, + chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE, +) -> int: """Return fade point from start in milliseconds""" return fade_point(audio_segment, silence_threshold, chunk_size) diff --git a/app/infotabs.py b/app/infotabs.py index 79aae45..1f14f95 100644 --- a/app/infotabs.py +++ b/app/infotabs.py @@ -1,9 +1,9 @@ import urllib.parse from datetime import datetime -from slugify import slugify # type: ignore -from typing import Dict, Optional -from PyQt6.QtCore import QUrl # type: ignore +from slugify import slugify # type: ignore +from typing import Dict +from PyQt6.QtCore import QUrl # type: ignore from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWidgets import QTabWidget from config import Config @@ -47,13 +47,11 @@ class InfoTabs(QTabWidget): if url in self.tabtitles.values(): self.setCurrentIndex( - list(self.tabtitles.keys())[ - list(self.tabtitles.values()).index(url) - ] + list(self.tabtitles.keys())[list(self.tabtitles.values()).index(url)] ) return - short_title = title[:Config.INFO_TAB_TITLE_LENGTH] + short_title = title[: Config.INFO_TAB_TITLE_LENGTH] if self.count() < Config.MAX_INFO_TABS: # Create a new tab @@ -63,9 +61,7 @@ class InfoTabs(QTabWidget): else: # Reuse oldest widget - widget = min( - self.last_update, key=self.last_update.get # type: ignore - ) + widget = min(self.last_update, key=self.last_update.get) # type: ignore tab_index = self.indexOf(widget) self.setTabText(tab_index, short_title) diff --git a/app/models.py b/app/models.py index 7d232d5..60a1878 100644 --- a/app/models.py +++ b/app/models.py @@ -1,13 +1,11 @@ #!/usr/bin/python3 -import os.path import re -import stackprinter # type: ignore -from dbconfig import Session, scoped_session +from dbconfig import 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 @@ -35,21 +33,14 @@ from sqlalchemy.orm.exc import ( from sqlalchemy.exc import ( IntegrityError, ) -from config import Config -from helpers import ( - fade_point, - get_audio_segment, - get_tags, - leading_silence, - trailing_silence, -) from log import log + Base = declarative_base() # Database classes class Carts(Base): - __tablename__ = 'carts' + __tablename__ = "carts" id: int = Column(Integer, primary_key=True, autoincrement=True) cart_number: int = Column(Integer, nullable=False, unique=True) @@ -64,10 +55,15 @@ class Carts(Base): f"name={self.name}, path={self.path}>" ) - def __init__(self, session: scoped_session, cart_number: int, - name: Optional[str] = None, - duration: Optional[int] = None, path: Optional[str] = None, - enabled: bool = True) -> None: + def __init__( + self, + session: scoped_session, + cart_number: int, + name: Optional[str] = None, + duration: Optional[int] = None, + path: Optional[str] = None, + enabled: bool = True, + ) -> None: """Create new cart""" self.cart_number = cart_number @@ -81,7 +77,7 @@ class Carts(Base): class NoteColours(Base): - __tablename__ = 'notecolours' + __tablename__ = "notecolours" id = Column(Integer, primary_key=True, autoincrement=True) substring = Column(String(256), index=False) @@ -106,11 +102,15 @@ class NoteColours(Base): if not text: return None - for rec in session.execute( + for rec in ( + session.execute( select(NoteColours) .filter(NoteColours.enabled.is_(True)) .order_by(NoteColours.order) - ).scalars().all(): + ) + .scalars() + .all() + ): if rec.is_regex: flags = re.UNICODE if not rec.is_casesensitive: @@ -130,11 +130,11 @@ class NoteColours(Base): class Playdates(Base): - __tablename__ = 'playdates' + __tablename__ = "playdates" id: int = Column(Integer, primary_key=True, autoincrement=True) lastplayed = Column(DateTime, index=True, default=None) - track_id = Column(Integer, ForeignKey('tracks.id')) + track_id = Column(Integer, ForeignKey("tracks.id")) track: "Tracks" = relationship("Tracks", back_populates="playdates") def __repr__(self) -> str: @@ -152,8 +152,7 @@ class Playdates(Base): session.commit() @staticmethod - def last_played(session: scoped_session, - track_id: int) -> Optional[datetime]: + def last_played(session: scoped_session, track_id: int) -> Optional[datetime]: """Return datetime track last played or None""" last_played = session.execute( @@ -169,8 +168,7 @@ class Playdates(Base): return None @staticmethod - def played_after(session: scoped_session, - since: datetime) -> List["Playdates"]: + def played_after(session: scoped_session, since: datetime) -> List["Playdates"]: """Return a list of Playdates objects since passed time""" return ( @@ -203,7 +201,7 @@ class Playlists(Base): "PlaylistRows", back_populates="playlist", cascade="all, delete-orphan", - order_by="PlaylistRows.plr_rownum" + order_by="PlaylistRows.plr_rownum", ) def __repr__(self) -> str: @@ -232,11 +230,9 @@ class Playlists(Base): ) @classmethod - def create_playlist_from_template(cls, - session: scoped_session, - template: "Playlists", - playlist_name: str) \ - -> Optional["Playlists"]: + def create_playlist_from_template( + cls, session: scoped_session, template: "Playlists", playlist_name: str + ) -> Optional["Playlists"]: """Create a new playlist from template""" playlist = cls(session, playlist_name) @@ -277,9 +273,7 @@ class Playlists(Base): return ( session.execute( - select(cls) - .filter(cls.is_template.is_(True)) - .order_by(cls.name) + select(cls).filter(cls.is_template.is_(True)).order_by(cls.name) ) .scalars() .all() @@ -295,7 +289,7 @@ class Playlists(Base): .filter( cls.tab.is_(None), cls.is_template.is_(False), - cls.deleted.is_(False) + cls.deleted.is_(False), ) .order_by(cls.last_used.desc()) ) @@ -310,11 +304,7 @@ class Playlists(Base): """ return ( - session.execute( - select(cls) - .where(cls.tab.is_not(None)) - .order_by(cls.tab) - ) + session.execute(select(cls).where(cls.tab.is_not(None)).order_by(cls.tab)) .scalars() .all() ) @@ -329,15 +319,9 @@ class Playlists(Base): def move_tab(session: scoped_session, frm: int, to: int) -> None: """Move tabs""" - row_frm = session.execute( - select(Playlists) - .filter_by(tab=frm) - ).scalar_one() + row_frm = session.execute(select(Playlists).filter_by(tab=frm)).scalar_one() - row_to = session.execute( - select(Playlists) - .filter_by(tab=to) - ).scalar_one() + row_to = session.execute(select(Playlists).filter_by(tab=to)).scalar_one() row_frm.tab = None row_to.tab = None @@ -354,8 +338,9 @@ class Playlists(Base): session.flush() @staticmethod - def save_as_template(session: scoped_session, - playlist_id: int, template_name: str) -> None: + def save_as_template( + session: scoped_session, playlist_id: int, template_name: str + ) -> None: """Save passed playlist as new template""" template = Playlists(session, template_name) @@ -369,15 +354,14 @@ class Playlists(Base): class PlaylistRows(Base): - __tablename__ = 'playlist_rows' + __tablename__ = "playlist_rows" id: int = Column(Integer, primary_key=True, autoincrement=True) plr_rownum: int = Column(Integer, nullable=False) note: str = Column(String(2048), index=False, default="", nullable=False) - playlist_id: int = Column(Integer, ForeignKey('playlists.id'), - nullable=False) + playlist_id: int = Column(Integer, ForeignKey("playlists.id"), nullable=False) playlist: Playlists = relationship(Playlists, back_populates="rows") - track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True) + track_id = Column(Integer, ForeignKey("tracks.id"), nullable=True) track: "Tracks" = relationship("Tracks", back_populates="playlistrows") played: bool = Column(Boolean, nullable=False, index=False, default=False) @@ -388,13 +372,14 @@ class PlaylistRows(Base): f"note={self.note}, plr_rownum={self.plr_rownum}>" ) - def __init__(self, - session: scoped_session, - playlist_id: int, - track_id: Optional[int], - row_number: int, - note: str = "" - ) -> None: + def __init__( + self, + session: scoped_session, + playlist_id: int, + track_id: Optional[int], + row_number: int, + note: str = "", + ) -> None: """Create PlaylistRows object""" self.playlist_id = playlist_id @@ -409,38 +394,38 @@ class PlaylistRows(Base): current_note = self.note if current_note: - self.note = current_note + '\n' + extra_note + self.note = current_note + "\n" + extra_note else: self.note = extra_note @staticmethod - def copy_playlist(session: scoped_session, - src_id: int, - dst_id: int) -> None: + def copy_playlist(session: scoped_session, src_id: int, dst_id: int) -> None: """Copy playlist entries""" - src_rows = session.execute( - select(PlaylistRows) - .filter(PlaylistRows.playlist_id == src_id) - ).scalars().all() + src_rows = ( + session.execute( + select(PlaylistRows).filter(PlaylistRows.playlist_id == src_id) + ) + .scalars() + .all() + ) for plr in src_rows: - PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum, - plr.note) + PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum, plr.note) @staticmethod def delete_higher_rows( - session: scoped_session, playlist_id: int, maxrow: int) -> None: + session: scoped_session, playlist_id: int, maxrow: int + ) -> None: """ Delete rows in given playlist that have a higher row number than 'maxrow' """ session.execute( - delete(PlaylistRows) - .where( + delete(PlaylistRows).where( PlaylistRows.playlist_id == playlist_id, - PlaylistRows.plr_rownum > maxrow + PlaylistRows.plr_rownum > maxrow, ) ) session.flush() @@ -451,11 +436,15 @@ class PlaylistRows(Base): Ensure the row numbers for passed playlist have no gaps """ - plrs = session.execute( - select(PlaylistRows) - .where(PlaylistRows.playlist_id == playlist_id) - .order_by(PlaylistRows.plr_rownum) - ).scalars().all() + plrs = ( + session.execute( + select(PlaylistRows) + .where(PlaylistRows.playlist_id == playlist_id) + .order_by(PlaylistRows.plr_rownum) + ) + .scalars() + .all() + ) for i, plr in enumerate(plrs): plr.plr_rownum = i @@ -464,113 +453,126 @@ class PlaylistRows(Base): session.commit() @classmethod - def get_from_id_list(cls, session: scoped_session, playlist_id: int, - plr_ids: List[int]) -> List["PlaylistRows"]: + def get_from_id_list( + cls, session: scoped_session, playlist_id: int, plr_ids: List[int] + ) -> List["PlaylistRows"]: """ Take a list of PlaylistRows ids and return a list of corresponding PlaylistRows objects """ - plrs = session.execute( - select(cls) - .where( - cls.playlist_id == playlist_id, - cls.id.in_(plr_ids) - ).order_by(cls.plr_rownum)).scalars().all() + plrs = ( + session.execute( + select(cls) + .where(cls.playlist_id == playlist_id, cls.id.in_(plr_ids)) + .order_by(cls.plr_rownum) + ) + .scalars() + .all() + ) return plrs @staticmethod - def get_last_used_row(session: scoped_session, - playlist_id: int) -> Optional[int]: + def get_last_used_row(session: scoped_session, playlist_id: int) -> Optional[int]: """Return the last used row for playlist, or None if no rows""" return session.execute( - select(func.max(PlaylistRows.plr_rownum)) - .where(PlaylistRows.playlist_id == playlist_id) + select(func.max(PlaylistRows.plr_rownum)).where( + PlaylistRows.playlist_id == playlist_id + ) ).scalar_one() @staticmethod - def get_track_plr(session: scoped_session, track_id: int, - playlist_id: int) -> Optional["PlaylistRows"]: + def get_track_plr( + session: scoped_session, track_id: int, playlist_id: int + ) -> Optional["PlaylistRows"]: """Return first matching PlaylistRows object or None""" return session.scalars( select(PlaylistRows) .where( PlaylistRows.track_id == track_id, - PlaylistRows.playlist_id == playlist_id + PlaylistRows.playlist_id == playlist_id, ) .limit(1) ).first() @classmethod - def get_played_rows(cls, session: scoped_session, - playlist_id: int) -> List["PlaylistRows"]: + def get_played_rows( + cls, session: scoped_session, playlist_id: int + ) -> List["PlaylistRows"]: """ For passed playlist, return a list of rows that have been played. """ - plrs = session.execute( - select(cls) - .where( - cls.playlist_id == playlist_id, - cls.played.is_(True) + plrs = ( + session.execute( + select(cls) + .where(cls.playlist_id == playlist_id, cls.played.is_(True)) + .order_by(cls.plr_rownum) ) - .order_by(cls.plr_rownum) - ).scalars().all() + .scalars() + .all() + ) return plrs @classmethod def get_rows_with_tracks( - cls, session: scoped_session, playlist_id: int, + cls, + session: scoped_session, + playlist_id: int, from_row: Optional[int] = None, - to_row: Optional[int] = None) -> List["PlaylistRows"]: + to_row: Optional[int] = None, + ) -> List["PlaylistRows"]: """ For passed playlist, return a list of rows that contain tracks """ query = select(cls).where( - cls.playlist_id == playlist_id, - cls.track_id.is_not(None) + cls.playlist_id == playlist_id, cls.track_id.is_not(None) ) if from_row is not None: query = query.where(cls.plr_rownum >= from_row) if to_row is not None: query = query.where(cls.plr_rownum <= to_row) - plrs = ( - session.execute((query).order_by(cls.plr_rownum)).scalars().all() - ) + plrs = session.execute((query).order_by(cls.plr_rownum)).scalars().all() return plrs @classmethod - def get_unplayed_rows(cls, session: scoped_session, - playlist_id: int) -> List["PlaylistRows"]: + def get_unplayed_rows( + cls, session: scoped_session, playlist_id: int + ) -> List["PlaylistRows"]: """ For passed playlist, return a list of playlist rows that have not been played. """ - plrs = session.execute( - select(cls) - .where( - cls.playlist_id == playlist_id, - cls.track_id.is_not(None), - cls.played.is_(False) + plrs = ( + session.execute( + select(cls) + .where( + cls.playlist_id == playlist_id, + cls.track_id.is_not(None), + cls.played.is_(False), + ) + .order_by(cls.plr_rownum) ) - .order_by(cls.plr_rownum) - ).scalars().all() + .scalars() + .all() + ) return plrs @staticmethod - def move_rows_down(session: scoped_session, playlist_id: int, - starting_row: int, move_by: int) -> None: + 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 @@ -580,7 +582,7 @@ class PlaylistRows(Base): update(PlaylistRows) .where( (PlaylistRows.playlist_id == playlist_id), - (PlaylistRows.plr_rownum >= starting_row) + (PlaylistRows.plr_rownum >= starting_row), ) .values(plr_rownum=PlaylistRows.plr_rownum + move_by) ) @@ -589,7 +591,7 @@ class PlaylistRows(Base): class Settings(Base): """Manage settings""" - __tablename__ = 'settings' + __tablename__ = "settings" id: int = Column(Integer, primary_key=True, autoincrement=True) name: str = Column(String(64), nullable=False, unique=True) @@ -602,21 +604,16 @@ class Settings(Base): return f"" def __init__(self, session: scoped_session, name: str): - self.name = name session.add(self) session.flush() @classmethod - def get_int_settings(cls, session: scoped_session, - name: str) -> "Settings": + def get_int_settings(cls, session: scoped_session, name: str) -> "Settings": """Get setting for an integer or return new setting record""" try: - return session.execute( - select(cls) - .where(cls.name == name) - ).scalar_one() + return session.execute(select(cls).where(cls.name == name)).scalar_one() except NoResultFound: return Settings(session, name) @@ -629,7 +626,7 @@ class Settings(Base): class Tracks(Base): - __tablename__ = 'tracks' + __tablename__ = "tracks" id: int = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(256), index=True) @@ -641,8 +638,7 @@ class Tracks(Base): path: str = Column(String(2048), index=False, nullable=False, unique=True) mtime = Column(Float, index=True) bitrate = Column(Integer, nullable=True, default=None) - playlistrows: PlaylistRows = relationship("PlaylistRows", - back_populates="track") + playlistrows: PlaylistRows = relationship("PlaylistRows", back_populates="track") playlists = association_proxy("playlistrows", "playlist") playdates: Playdates = relationship("Playdates", back_populates="track") @@ -653,18 +649,18 @@ class Tracks(Base): ) def __init__( - self, - session: scoped_session, - path: str, - title: Optional[str] = None, - artist: Optional[str] = None, - duration: int = 0, - start_gap: int = 0, - fade_at: Optional[int] = None, - silence_at: Optional[int] = None, - mtime: Optional[float] = None, - lastplayed: Optional[datetime] = None, - ) -> None: + self, + session: scoped_session, + path: str, + title: Optional[str] = None, + artist: Optional[str] = None, + duration: int = 0, + start_gap: int = 0, + fade_at: Optional[int] = None, + silence_at: Optional[int] = None, + mtime: Optional[float] = None, + lastplayed: Optional[datetime] = None, + ) -> None: self.path = path self.title = title self.artist = artist @@ -693,46 +689,36 @@ class Tracks(Base): return session.execute(select(cls)).scalars().all() @classmethod - def get_by_path(cls, session: scoped_session, - path: str) -> Optional["Tracks"]: + def get_by_path(cls, session: scoped_session, path: str) -> Optional["Tracks"]: """ Return track with passed path, or None. """ try: - return ( - session.execute( - select(Tracks) - .where(Tracks.path == path) - ).scalar_one() - ) + return session.execute( + select(Tracks).where(Tracks.path == path) + ).scalar_one() except NoResultFound: return None @classmethod - def search_artists(cls, session: scoped_session, - text: str) -> List["Tracks"]: + def search_artists(cls, session: scoped_session, text: str) -> List["Tracks"]: """Search case-insenstively for artists containing str""" return ( session.execute( - select(cls) - .where(cls.artist.ilike(f"%{text}%")) - .order_by(cls.title) + select(cls).where(cls.artist.ilike(f"%{text}%")).order_by(cls.title) ) .scalars() .all() ) @classmethod - def search_titles(cls, session: scoped_session, - text: str) -> List["Tracks"]: + def search_titles(cls, session: scoped_session, text: str) -> List["Tracks"]: """Search case-insenstively for titles containing str""" return ( session.execute( - select(cls) - .where(cls.title.like(f"{text}%")) - .order_by(cls.title) + select(cls).where(cls.title.like(f"{text}%")).order_by(cls.title) ) .scalars() .all() diff --git a/app/music.py b/app/music.py index bd3df29..f413b53 100644 --- a/app/music.py +++ b/app/music.py @@ -1,16 +1,16 @@ # import os import threading -import vlc # type: ignore +import vlc # type: ignore + # from config import Config -from datetime import datetime from helpers import file_is_unreadable from typing import Optional from time import sleep from log import log -from PyQt6.QtCore import ( # type: ignore +from PyQt6.QtCore import ( # type: ignore QRunnable, QThreadPool, ) @@ -19,7 +19,6 @@ lock = threading.Lock() class FadeTrack(QRunnable): - def __init__(self, player: vlc.MediaPlayer) -> None: super().__init__() self.player = player @@ -47,8 +46,7 @@ class FadeTrack(QRunnable): for i in range(1, steps + 1): measures_to_reduce_by += i - volume_factor = 1 - ( - measures_to_reduce_by / total_measures_count) + volume_factor = 1 - (measures_to_reduce_by / total_measures_count) self.player.audio_set_volume(int(original_volume * volume_factor)) sleep(sleep_time) @@ -98,8 +96,7 @@ class Music: return None return self.player.get_position() - def play(self, path: str, - position: Optional[float] = None) -> Optional[int]: + def play(self, path: str, position: Optional[float] = None) -> Optional[int]: """ Start playing the track at path. diff --git a/app/musicmuster.py b/app/musicmuster.py index 0c2e116..3386825 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -5,13 +5,12 @@ from os.path import basename import argparse import os import numpy as np -import pyqtgraph as pg # type: ignore -import stackprinter # type: ignore +import pyqtgraph as pg # type: ignore +import stackprinter # type: ignore import subprocess import sys import threading -import icons_rc from datetime import datetime, timedelta from pygame import mixer from time import sleep @@ -61,22 +60,14 @@ from dbconfig import ( ) import helpers import music -from models import ( - Base, - Carts, - Playdates, - PlaylistRows, - Playlists, - Settings, - Tracks -) +from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks from config import Config from playlists import PlaylistTab -from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore -from ui.dlg_search_database_ui import Ui_Dialog # type: ignore -from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore -from ui.downloadcsv_ui import Ui_DateSelect # type: ignore -from ui.main_window_ui import Ui_MainWindow # type: ignore +from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore +from ui.dlg_search_database_ui import Ui_Dialog # type: ignore +from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore +from ui.downloadcsv_ui import Ui_DateSelect # type: ignore +from ui.main_window_ui import Ui_MainWindow # type: ignore from utilities import check_db, update_bitrates @@ -93,7 +84,7 @@ class CartButton(QPushButton): self.cart_id = cart.id if cart.path and cart.enabled and not cart.duration: tags = helpers.get_tags(cart.path) - cart.duration = tags['duration'] + cart.duration = tags["duration"] self.duration = cart.duration self.path = cart.path self.player = None @@ -110,8 +101,9 @@ class CartButton(QPushButton): self.pgb.setTextVisible(False) self.pgb.setVisible(False) palette = self.pgb.palette() - palette.setColor(QPalette.ColorRole.Highlight, - QColor(Config.COLOUR_CART_PROGRESSBAR)) + palette.setColor( + QPalette.ColorRole.Highlight, QColor(Config.COLOUR_CART_PROGRESSBAR) + ) self.pgb.setPalette(palette) self.pgb.setGeometry(0, 0, self.width(), 10) self.pgb.setMinimum(0) @@ -157,16 +149,13 @@ class FadeCurve: # Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE # milliseconds before fade starts to silence - self.start_ms = max( - 0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1) + self.start_ms = max(0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1) self.end_ms = track.silence_at - self.audio_segment = audio[self.start_ms:self.end_ms] + self.audio_segment = audio[self.start_ms : self.end_ms] self.graph_array = np.array(self.audio_segment.get_array_of_samples()) # Calculate the factor to map milliseconds of track to array - self.ms_to_array_factor = ( - len(self.graph_array) / (self.end_ms - self.start_ms) - ) + self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms) self.region = None @@ -192,10 +181,7 @@ class FadeCurve: if self.region is None: # Create the region now that we're into fade - self.region = pg.LinearRegionItem( - [0, 0], - bounds=[0, len(self.graph_array)] - ) + self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)]) self.GraphWidget.addItem(self.region) # Update region position @@ -286,8 +272,9 @@ class PlaylistTrack: f"playlist_id={self.playlist_id}>" ) - def set_plr(self, session: scoped_session, plr: PlaylistRows, - tab: PlaylistTab) -> None: + def set_plr( + self, session: scoped_session, plr: PlaylistRows, tab: PlaylistTab + ) -> None: """ Update with new plr information """ @@ -323,8 +310,7 @@ class PlaylistTrack: self.start_time = datetime.now() if self.duration: - self.end_time = ( - self.start_time + timedelta(milliseconds=self.duration)) + self.end_time = self.start_time + timedelta(milliseconds=self.duration) class Window(QMainWindow, Ui_MainWindow): @@ -355,14 +341,15 @@ class Window(QMainWindow, Ui_MainWindow): self.txtSearch.setHidden(True) self.hide_played_tracks = False mixer.init() - self.widgetFadeVolume.hideAxis('bottom') - self.widgetFadeVolume.hideAxis('left') + self.widgetFadeVolume.hideAxis("bottom") + self.widgetFadeVolume.hideAxis("left") self.widgetFadeVolume.setDefaultPadding(0) self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND) FadeCurve.GraphWidget = self.widgetFadeVolume - self.visible_playlist_tab: Callable[[], PlaylistTab] = \ - self.tabPlaylist.currentWidget + self.visible_playlist_tab: Callable[ + [], PlaylistTab + ] = self.tabPlaylist.currentWidget self.load_last_playlists() if Config.CARTS_HIDE: @@ -380,10 +367,8 @@ class Window(QMainWindow, Ui_MainWindow): try: git_tag = str( - subprocess.check_output( - ['git', 'describe'], stderr=subprocess.STDOUT - ) - ).strip('\'b\\n') + subprocess.check_output(["git", "describe"], stderr=subprocess.STDOUT) + ).strip("'b\\n") except subprocess.CalledProcessError as exc_info: git_tag = str(exc_info.output) @@ -395,7 +380,7 @@ class Window(QMainWindow, Ui_MainWindow): self, "About", f"MusicMuster {git_tag}\n\nDatabase: {dbname}", - QMessageBox.StandardButton.Ok + QMessageBox.StandardButton.Ok, ) def cart_configure(self, cart: Carts, btn: CartButton) -> None: @@ -433,15 +418,13 @@ class Window(QMainWindow, Ui_MainWindow): # 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=})") + log.debug(f"musicmuster.cart_click(): no player assigned ({btn=})") return btn.player.play() btn.is_playing = True colour = Config.COLOUR_CART_PLAYING - thread = threading.Thread(target=self.cart_progressbar, - args=(btn,)) + thread = threading.Thread(target=self.cart_progressbar, args=(btn,)) thread.start() else: colour = Config.COLOUR_CART_ERROR @@ -469,7 +452,7 @@ class Window(QMainWindow, Ui_MainWindow): return if cart.path and not helpers.file_is_unreadable(cart.path): tags = helpers.get_tags(cart.path) - cart.duration = tags['duration'] + cart.duration = tags["duration"] cart.enabled = dlg.ui.chkEnabled.isChecked() cart.name = name @@ -488,8 +471,7 @@ class Window(QMainWindow, Ui_MainWindow): for cart_number in range(1, Config.CARTS_COUNT + 1): cart = session.query(Carts).get(cart_number) if cart is None: - cart = Carts(session, cart_number, - name=f"Cart #{cart_number}") + cart = Carts(session, cart_number, name=f"Cart #{cart_number}") btn = CartButton(self, cart) btn.clicked.connect(self.cart_click) @@ -540,7 +522,7 @@ class Window(QMainWindow, Ui_MainWindow): self.update_headers() def clear_selection(self) -> None: - """ Clear selected row""" + """Clear selected row""" # Unselect any selected rows if self.visible_playlist_tab(): @@ -555,27 +537,25 @@ class Window(QMainWindow, Ui_MainWindow): if self.playing: event.ignore() helpers.show_warning( - self, - "Track playing", - "Can't close application while track is playing") + self, "Track playing", "Can't close application while track is playing" + ) else: with Session() as session: - record = Settings.get_int_settings( - session, "mainwindow_height") + record = Settings.get_int_settings(session, "mainwindow_height") if record.f_int != self.height(): - record.update(session, {'f_int': self.height()}) + record.update(session, {"f_int": self.height()}) record = Settings.get_int_settings(session, "mainwindow_width") if record.f_int != self.width(): - record.update(session, {'f_int': self.width()}) + record.update(session, {"f_int": self.width()}) record = Settings.get_int_settings(session, "mainwindow_x") if record.f_int != self.x(): - record.update(session, {'f_int': self.x()}) + record.update(session, {"f_int": self.x()}) record = Settings.get_int_settings(session, "mainwindow_y") if record.f_int != self.y(): - record.update(session, {'f_int': self.y()}) + record.update(session, {"f_int": self.y()}) # Save splitter settings splitter_sizes = self.splitter.sizes() @@ -584,16 +564,15 @@ class Window(QMainWindow, Ui_MainWindow): record = Settings.get_int_settings(session, "splitter_top") if record.f_int != splitter_top: - record.update(session, {'f_int': splitter_top}) + record.update(session, {"f_int": splitter_top}) record = Settings.get_int_settings(session, "splitter_bottom") if record.f_int != splitter_bottom: - record.update(session, {'f_int': splitter_bottom}) + record.update(session, {"f_int": splitter_bottom}) # Save current tab record = Settings.get_int_settings(session, "active_tab") - record.update(session, - {'f_int': self.tabPlaylist.currentIndex()}) + record.update(session, {"f_int": self.tabPlaylist.currentIndex()}) event.accept() @@ -613,10 +592,8 @@ class Window(QMainWindow, Ui_MainWindow): """ # Don't close current track playlist - if self.tabPlaylist.widget(tab_index) == ( - self.current_track.playlist_tab): - self.statusbar.showMessage( - "Can't close current track playlist", 5000) + if self.tabPlaylist.widget(tab_index) == (self.current_track.playlist_tab): + self.statusbar.showMessage("Can't close current track playlist", 5000) return False # Attempt to close next track playlist @@ -643,15 +620,17 @@ class Window(QMainWindow, Ui_MainWindow): self.actionClosePlaylist.triggered.connect(self.close_playlist_tab) self.actionDeletePlaylist.triggered.connect(self.delete_playlist) self.actionDownload_CSV_of_played_tracks.triggered.connect( - self.download_played_tracks) - self.actionEnable_controls.triggered.connect( - self.enable_play_next_controls) + self.download_played_tracks + ) + self.actionEnable_controls.triggered.connect(self.enable_play_next_controls) self.actionExport_playlist.triggered.connect(self.export_playlist_tab) self.actionFade.triggered.connect(self.fade) self.actionFind_next.triggered.connect( - lambda: self.tabPlaylist.currentWidget().search_next()) + lambda: self.tabPlaylist.currentWidget().search_next() + ) self.actionFind_previous.triggered.connect( - lambda: self.tabPlaylist.currentWidget().search_previous()) + lambda: self.tabPlaylist.currentWidget().search_previous() + ) self.actionImport.triggered.connect(self.import_track) self.actionInsertSectionHeader.triggered.connect(self.insert_header) self.actionInsertTrack.triggered.connect(self.insert_track) @@ -666,13 +645,14 @@ class Window(QMainWindow, Ui_MainWindow): self.actionResume.triggered.connect(self.resume) self.actionSave_as_template.triggered.connect(self.save_as_template) self.actionSearch_title_in_Songfacts.triggered.connect( - lambda: self.tabPlaylist.currentWidget().lookup_row_in_songfacts()) + lambda: self.tabPlaylist.currentWidget().lookup_row_in_songfacts() + ) self.actionSearch_title_in_Wikipedia.triggered.connect( - lambda: self.tabPlaylist.currentWidget().lookup_row_in_wikipedia()) + lambda: self.tabPlaylist.currentWidget().lookup_row_in_wikipedia() + ) self.actionSearch.triggered.connect(self.search_playlist) self.actionSelect_next_track.triggered.connect(self.select_next_row) - self.actionSelect_previous_track.triggered.connect( - self.select_previous_row) + self.actionSelect_previous_track.triggered.connect(self.select_previous_row) self.actionMoveUnplayed.triggered.connect(self.move_unplayed) self.actionSetNext.triggered.connect(self.set_selected_track_next) self.actionSkipToNext.triggered.connect(self.play_next) @@ -691,10 +671,9 @@ class Window(QMainWindow, Ui_MainWindow): self.timer.timeout.connect(self.tick) - def create_playlist(self, - session: scoped_session, - playlist_name: Optional[str] = None) \ - -> Optional[Playlists]: + def create_playlist( + self, session: scoped_session, playlist_name: Optional[str] = None + ) -> Optional[Playlists]: """Create new playlist""" playlist_name = self.solicit_playlist_name() @@ -712,8 +691,7 @@ class Window(QMainWindow, Ui_MainWindow): if playlist: self.create_playlist_tab(session, playlist) - def create_playlist_tab(self, session: scoped_session, - playlist: Playlists) -> int: + def create_playlist_tab(self, session: scoped_session, playlist: Playlists) -> int: """ Take the passed playlist database object, create a playlist tab and add tab to display. Return index number of tab. @@ -722,8 +700,11 @@ class Window(QMainWindow, Ui_MainWindow): assert playlist.id playlist_tab = PlaylistTab( - musicmuster=self, session=session, playlist_id=playlist.id, - signals=self.signals) + musicmuster=self, + session=session, + playlist_id=playlist.id, + signals=self.signals, + ) idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) self.tabPlaylist.setCurrentIndex(idx) @@ -737,15 +718,17 @@ class Window(QMainWindow, Ui_MainWindow): with Session() as session: # Save the selected PlaylistRows items ready for a later # paste - self.selected_plrs = ( - self.visible_playlist_tab().get_selected_playlistrows(session)) + self.selected_plrs = self.visible_playlist_tab().get_selected_playlistrows( + session + ) def debug(self): """Invoke debugger""" visible_playlist_id = self.visible_playlist_tab().playlist_id print(f"Active playlist id={visible_playlist_id}") - import ipdb # type: ignore + import ipdb # type: ignore + ipdb.set_trace() def delete_playlist(self) -> None: @@ -757,10 +740,10 @@ class Window(QMainWindow, Ui_MainWindow): playlist_id = self.visible_playlist_tab().playlist_id playlist = session.get(Playlists, playlist_id) if playlist: - if helpers.ask_yes_no("Delete playlist", - f"Delete playlist '{playlist.name}': " - "Are you sure?" - ): + if helpers.ask_yes_no( + "Delete playlist", + f"Delete playlist '{playlist.name}': " "Are you sure?", + ): if self.close_playlist_tab(): playlist.delete(session) @@ -780,9 +763,10 @@ class Window(QMainWindow, Ui_MainWindow): start_dt = dlg.ui.dateTimeEdit.dateTime().toPyDateTime() # Get output filename pathspec = QFileDialog.getSaveFileName( - self, 'Save CSV of tracks played', + self, + "Save CSV of tracks played", directory="/tmp/playlist.csv", - filter="CSV files (*.csv)" + filter="CSV files (*.csv)", ) if not pathspec: return @@ -794,9 +778,7 @@ class Window(QMainWindow, Ui_MainWindow): with open(path, "w") as f: with Session() as session: for playdate in Playdates.played_after(session, start_dt): - f.write( - f"{playdate.track.artist},{playdate.track.title}\n" - ) + f.write(f"{playdate.track.artist},{playdate.track.title}\n") def drop3db(self) -> None: """Drop music level by 3db if button checked""" @@ -867,16 +849,16 @@ class Window(QMainWindow, Ui_MainWindow): playlist_id = self.visible_playlist_tab().playlist_id with Session() as session: - # Get output filename playlist = session.get(Playlists, playlist_id) if not playlist: return pathspec = QFileDialog.getSaveFileName( - self, 'Save Playlist', + self, + "Save Playlist", directory=f"{playlist.name}.m3u", - filter="M3U files (*.m3u);;All files (*.*)" + filter="M3U files (*.m3u);;All files (*.*)", ) if not pathspec: return @@ -923,10 +905,7 @@ class Window(QMainWindow, Ui_MainWindow): times a second; this function has much better resolution. """ - if ( - self.current_track.track_id is None or - self.current_track.start_time is None - ): + if self.current_track.track_id is None or self.current_track.start_time is None: return 0 now = datetime.now() @@ -965,16 +944,16 @@ class Window(QMainWindow, Ui_MainWindow): txt = "" tags = helpers.get_tags(fname) new_tracks.append(fname) - title = tags['title'] - artist = tags['artist'] + title = tags["title"] + artist = tags["artist"] count = 0 possible_matches = Tracks.search_titles(session, title) if possible_matches: - txt += 'Similar to new track ' + txt += "Similar to new track " txt += f'"{title}" by "{artist} ({fname})":\n\n' for track in possible_matches: txt += f' "{track.title}" by {track.artist}' - txt += f' ({track.path})\n\n' + txt += f" ({track.path})\n\n" count += 1 if count >= Config.MAX_IMPORT_MATCHES: txt += "\nThere are more similar-looking tracks" @@ -987,7 +966,7 @@ class Window(QMainWindow, Ui_MainWindow): "Possible duplicates", txt, QMessageBox.StandardButton.Ok, - QMessageBox.StandardButton.Cancel + QMessageBox.StandardButton.Cancel, ) if result == QMessageBox.StandardButton.Cancel: return @@ -1005,9 +984,7 @@ class Window(QMainWindow, Ui_MainWindow): self, "Import error", "Error importing " + msg ) ) - self.worker.importing.connect( - lambda msg: self.statusbar.showMessage(msg, 5000) - ) + self.worker.importing.connect(lambda msg: self.statusbar.showMessage(msg, 5000)) self.worker.finished.connect(self.import_complete) self.import_thread.start() @@ -1057,8 +1034,9 @@ class Window(QMainWindow, Ui_MainWindow): if record and record.f_int >= 0: self.tabPlaylist.setCurrentIndex(record.f_int) - def move_playlist_rows(self, session: scoped_session, - playlistrows: List[PlaylistRows]) -> None: + def move_playlist_rows( + self, session: scoped_session, playlistrows: List[PlaylistRows] + ) -> None: """ Move passed playlist rows to another playlist @@ -1071,14 +1049,15 @@ 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] - ] + plrs_to_move = [ + plr + for plr in playlistrows + if plr.id not in [self.current_track.plr_id, self.next_track.plr_id] + ] - rows_to_delete = [plr.plr_rownum for plr in plrs_to_move - if plr.plr_rownum is not None] + rows_to_delete = [ + plr.plr_rownum for plr in plrs_to_move if plr.plr_rownum is not None + ] if not rows_to_delete: return @@ -1100,8 +1079,7 @@ class Window(QMainWindow, Ui_MainWindow): destination_playlist_id = dlg.playlist.id # Update destination playlist in the database - last_row = PlaylistRows.get_last_used_row(session, - destination_playlist_id) + last_row = PlaylistRows.get_last_used_row(session, destination_playlist_id) if last_row is not None: next_row = last_row + 1 else: @@ -1135,8 +1113,9 @@ class Window(QMainWindow, Ui_MainWindow): """ with Session() as session: - selected_plrs = ( - self.visible_playlist_tab().get_selected_playlistrows(session)) + selected_plrs = self.visible_playlist_tab().get_selected_playlistrows( + session + ) if not selected_plrs: return @@ -1155,12 +1134,10 @@ class Window(QMainWindow, Ui_MainWindow): playlist_id = self.visible_playlist_tab().playlist_id with Session() as session: - unplayed_plrs = PlaylistRows.get_unplayed_rows( - session, playlist_id) - if helpers.ask_yes_no("Move tracks", - f"Move {len(unplayed_plrs)} tracks:" - " Are you sure?" - ): + unplayed_plrs = PlaylistRows.get_unplayed_rows(session, playlist_id) + if helpers.ask_yes_no( + "Move tracks", f"Move {len(unplayed_plrs)} tracks:" " Are you sure?" + ): self.move_playlist_rows(session, unplayed_plrs) def new_from_template(self) -> None: @@ -1168,8 +1145,7 @@ class Window(QMainWindow, Ui_MainWindow): with Session() as session: templates = Playlists.get_all_templates(session) - dlg = SelectPlaylistDialog(self, playlists=templates, - session=session) + dlg = SelectPlaylistDialog(self, playlists=templates, session=session) dlg.exec() template = dlg.playlist if template: @@ -1177,7 +1153,8 @@ class Window(QMainWindow, Ui_MainWindow): if not playlist_name: return playlist = Playlists.create_playlist_from_template( - session, template, playlist_name) + session, template, playlist_name + ) if not playlist: return tab_index = self.create_playlist_tab(session, playlist) @@ -1188,8 +1165,7 @@ class Window(QMainWindow, Ui_MainWindow): with Session() as session: playlists = Playlists.get_closed(session) - dlg = SelectPlaylistDialog(self, playlists=playlists, - session=session) + dlg = SelectPlaylistDialog(self, playlists=playlists, session=session) dlg.exec() playlist = dlg.playlist if playlist: @@ -1217,8 +1193,9 @@ class Window(QMainWindow, Ui_MainWindow): with Session() as session: # Create space in destination playlist - PlaylistRows.move_rows_down(session, dst_playlist_id, - dst_row, len(self.selected_plrs)) + PlaylistRows.move_rows_down( + session, dst_playlist_id, dst_row, len(self.selected_plrs) + ) session.commit() # Update plrs @@ -1240,7 +1217,8 @@ class Window(QMainWindow, Ui_MainWindow): # Update display self.visible_playlist_tab().populate_display( - session, dst_playlist_id, scroll_to_top=False) + session, dst_playlist_id, scroll_to_top=False + ) # If source playlist is not destination playlist, fixup row # numbers and update display @@ -1250,13 +1228,13 @@ class Window(QMainWindow, Ui_MainWindow): # will be re-populated when it is opened) source_playlist_tab = None for tab in range(self.tabPlaylist.count()): - if self.tabPlaylist.widget(tab).playlist_id == \ - src_playlist_id: + if self.tabPlaylist.widget(tab).playlist_id == src_playlist_id: source_playlist_tab = self.tabPlaylist.widget(tab) break if source_playlist_tab: source_playlist_tab.populate_display( - session, src_playlist_id, scroll_to_top=False) + session, src_playlist_id, scroll_to_top=False + ) # Reset so rows can't be repasted self.selected_plrs = None @@ -1305,8 +1283,7 @@ class Window(QMainWindow, Ui_MainWindow): # Set current track playlist_tab colour current_tab = self.current_track.playlist_tab if current_tab: - self.set_tab_colour( - current_tab, QColor(Config.COLOUR_CURRENT_TAB)) + self.set_tab_colour(current_tab, QColor(Config.COLOUR_CURRENT_TAB)) # Restore volume if -3dB active if self.btnDrop3db.isChecked(): @@ -1356,8 +1333,7 @@ class Window(QMainWindow, Ui_MainWindow): if self.btnPreview.isChecked(): # Get track path for first selected track if there is one - track_path = ( - self.visible_playlist_tab().get_selected_row_track_path()) + track_path = self.visible_playlist_tab().get_selected_row_track_path() if not track_path: # Otherwise get path to next track to play track_path = self.next_track.path @@ -1405,35 +1381,32 @@ class Window(QMainWindow, Ui_MainWindow): if self.current_track.track_id: playing_track = self.current_track - with Session() as session: - # Set next plr to be track to resume - if not self.previous_track.plr_id: - return - if not self.previous_track.playlist_tab: - return + # Set next plr to be track to resume + if not self.previous_track.plr_id: + return + if not self.previous_track.playlist_tab: + return - # Resume last track - self.set_next_plr_id(self.previous_track.plr_id, - self.previous_track.playlist_tab) - self.play_next(self.previous_track_position) + # Resume last track + self.set_next_plr_id( + self.previous_track.plr_id, self.previous_track.playlist_tab + ) + self.play_next(self.previous_track_position) - # If a track was playing when we were called, get details to - # set it as the next track - if playing_track: - if not playing_track.plr_id: - return - if not playing_track.playlist_tab: - return - self.set_next_plr_id(playing_track.plr_id, - playing_track.playlist_tab) + # If a track was playing when we were called, get details to + # set it as the next track + if playing_track: + if not playing_track.plr_id: + return + if not playing_track.playlist_tab: + return + self.set_next_plr_id(playing_track.plr_id, playing_track.playlist_tab) def save_as_template(self) -> None: """Save current playlist as template""" with Session() as session: - template_names = [ - a.name for a in Playlists.get_all_templates(session) - ] + template_names = [a.name for a in Playlists.get_all_templates(session)] while True: # Get name for new template @@ -1448,13 +1421,12 @@ class Window(QMainWindow, Ui_MainWindow): template_name = dlg.textValue() if template_name not in template_names: break - helpers.show_warning(self, - "Duplicate template", - "Template name already in use" - ) - Playlists.save_as_template(session, - self.visible_playlist_tab().playlist_id, - template_name) + helpers.show_warning( + self, "Duplicate template", "Template name already in use" + ) + Playlists.save_as_template( + session, self.visible_playlist_tab().playlist_id, template_name + ) helpers.show_OK(self, "Template", "Template saved") def search_playlist(self) -> None: @@ -1535,8 +1507,7 @@ class Window(QMainWindow, Ui_MainWindow): self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab) self.tabPlaylist.currentWidget().scroll_next_to_top() - def solicit_playlist_name(self, - default: Optional[str] = "") -> Optional[str]: + def solicit_playlist_name(self, default: Optional[str] = "") -> Optional[str]: """Get name of playlist from user""" dlg = QInputDialog(self) @@ -1581,11 +1552,13 @@ class Window(QMainWindow, Ui_MainWindow): # 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)) + 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)) + self.set_tab_colour( + self.current_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB) + ) # Run end-of-track actions self.end_of_track_actions() @@ -1612,8 +1585,9 @@ class Window(QMainWindow, Ui_MainWindow): self.set_next_plr_id(selected_plr_ids[0], playlist_tab) - def set_next_plr_id(self, next_plr_id: Optional[int], - playlist_tab: PlaylistTab) -> None: + def set_next_plr_id( + self, next_plr_id: Optional[int], playlist_tab: PlaylistTab + ) -> None: """ Set passed plr_id as next track to play, or clear next track if None @@ -1636,8 +1610,9 @@ class Window(QMainWindow, Ui_MainWindow): # Tell playlist tabs to update their 'next track' highlighting # Args must both be ints, so use zero for no next track - self.signals.set_next_track_signal.emit(old_next_track.plr_id, - next_plr_id or 0) + self.signals.set_next_track_signal.emit( + old_next_track.plr_id, next_plr_id or 0 + ) # Update headers self.update_headers() @@ -1650,12 +1625,15 @@ class Window(QMainWindow, Ui_MainWindow): # because it isn't quick if self.next_track.title: QTimer.singleShot( - 1, lambda: self.tabInfolist.open_in_wikipedia( - self.next_track.title) + 1, + lambda: self.tabInfolist.open_in_wikipedia( + self.next_track.title + ), ) def _set_next_track_playlist_tab_colours( - self, old_next_track: Optional[PlaylistTrack]) -> None: + self, old_next_track: Optional[PlaylistTrack] + ) -> None: """ Set playlist tab colour for next track. self.next_track needs to be set before calling. @@ -1664,14 +1642,14 @@ class Window(QMainWindow, Ui_MainWindow): # If the original next playlist tab isn't the same as the # new one or the current track, it needs its colour reset. if ( - old_next_track and - old_next_track.playlist_tab and - old_next_track.playlist_tab not in [ - self.next_track.playlist_tab, - self.current_track.playlist_tab - ]): - self.set_tab_colour(old_next_track.playlist_tab, - QColor(Config.COLOUR_NORMAL_TAB)) + old_next_track + and old_next_track.playlist_tab + and old_next_track.playlist_tab + not in [self.next_track.playlist_tab, self.current_track.playlist_tab] + ): + self.set_tab_colour( + old_next_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB) + ) # If the new next playlist tab isn't the same as the # old one or the current track, it needs its colour set. if old_next_track: @@ -1679,14 +1657,14 @@ class Window(QMainWindow, Ui_MainWindow): else: old_tab = None if ( - self.next_track and - self.next_track.playlist_tab and - self.next_track.playlist_tab not in [ - old_tab, - self.current_track.playlist_tab - ]): - self.set_tab_colour(self.next_track.playlist_tab, - QColor(Config.COLOUR_NEXT_TAB)) + self.next_track + and self.next_track.playlist_tab + and self.next_track.playlist_tab + not in [old_tab, self.current_track.playlist_tab] + ): + self.set_tab_colour( + self.next_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB) + ) def tick(self) -> None: """ @@ -1713,9 +1691,9 @@ class Window(QMainWindow, Ui_MainWindow): # Update volume fade curve if ( - self.current_track.track_id and - self.current_track.fade_graph and - self.current_track.start_time + self.current_track.track_id + and self.current_track.fade_graph + and self.current_track.start_time ): play_time = ( datetime.now() - self.current_track.start_time @@ -1727,8 +1705,7 @@ class Window(QMainWindow, Ui_MainWindow): Called every 500ms """ - self.lblTOD.setText(datetime.now().strftime( - Config.TOD_TIME_FORMAT)) + self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT)) # Update carts self.cart_tick() @@ -1748,15 +1725,19 @@ class Window(QMainWindow, Ui_MainWindow): # player.is_playing() returning True, so assume playing if less # than Config.PLAY_SETTLE microseconds have passed since # starting play. - if self.music.player and self.current_track.start_time and ( - self.music.player.is_playing() or - (datetime.now() - self.current_track.start_time) - < timedelta(microseconds=Config.PLAY_SETTLE)): + if ( + self.music.player + and self.current_track.start_time + and ( + self.music.player.is_playing() + or (datetime.now() - self.current_track.start_time) + < timedelta(microseconds=Config.PLAY_SETTLE) + ) + ): playtime = self.get_playtime() - time_to_fade = (self.current_track.fade_at - playtime) - time_to_silence = ( - self.current_track.silence_at - playtime) - time_to_end = (self.current_track.duration - playtime) + time_to_fade = self.current_track.fade_at - playtime + time_to_silence = self.current_track.silence_at - playtime + time_to_end = self.current_track.duration - playtime # Elapsed time self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) @@ -1787,9 +1768,7 @@ class Window(QMainWindow, Ui_MainWindow): self.frame_silent.setStyleSheet("") self.frame_fade.setStyleSheet("") - self.label_silent_timer.setText( - helpers.ms_to_mmss(time_to_silence) - ) + self.label_silent_timer.setText(helpers.ms_to_mmss(time_to_silence)) # Time to end self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end)) @@ -1834,8 +1813,9 @@ 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, musicmuster: Window, session: scoped_session, cart: Carts, *args, **kwargs + ) -> None: """ Manage carts """ @@ -1871,8 +1851,14 @@ 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, + musicmuster: Window, + session: scoped_session, + get_one_track: bool = False, + *args, + **kwargs, + ) -> None: """ Subclassed QDialog to manage track selection @@ -1911,17 +1897,16 @@ class DbDialog(QDialog): record = Settings.get_int_settings(self.session, "dbdialog_height") if record.f_int != self.height(): - record.update(self.session, {'f_int': self.height()}) + record.update(self.session, {"f_int": self.height()}) record = Settings.get_int_settings(self.session, "dbdialog_width") if record.f_int != self.width(): - record.update(self.session, {'f_int': self.width()}) + record.update(self.session, {"f_int": self.width()}) def add_selected(self) -> None: """Handle Add button""" - if (not self.ui.matchList.selectedItems() and - not self.ui.txtNote.text()): + if not self.ui.matchList.selectedItems() and not self.ui.txtNote.text(): return track = None @@ -1946,10 +1931,12 @@ class DbDialog(QDialog): if track: self.musicmuster.visible_playlist_tab().insert_track( - self.session, track, note=self.ui.txtNote.text()) + self.session, track, note=self.ui.txtNote.text() + ) else: self.musicmuster.visible_playlist_tab().insert_header( - self.session, note=self.ui.txtNote.text()) + self.session, note=self.ui.txtNote.text() + ) # TODO: this shouldn't be needed as insert_track() saves # playlist @@ -2041,11 +2028,11 @@ class SelectPlaylistDialog(QDialog): self.session = session self.playlist = None - record = Settings.get_int_settings( - self.session, "select_playlist_dialog_width") + record = Settings.get_int_settings(self.session, "select_playlist_dialog_width") width = record.f_int or 800 record = Settings.get_int_settings( - self.session, "select_playlist_dialog_height") + self.session, "select_playlist_dialog_height" + ) height = record.f_int or 600 self.resize(width, height) @@ -2057,20 +2044,20 @@ class SelectPlaylistDialog(QDialog): def __del__(self): # review record = Settings.get_int_settings( - self.session, "select_playlist_dialog_height") + self.session, "select_playlist_dialog_height" + ) if record.f_int != self.height(): - record.update(self.session, {'f_int': self.height()}) + record.update(self.session, {"f_int": self.height()}) - record = Settings.get_int_settings( - self.session, "select_playlist_dialog_width") + record = Settings.get_int_settings(self.session, "select_playlist_dialog_width") if record.f_int != self.width(): - record.update(self.session, {'f_int': self.width()}) + record.update(self.session, {"f_int": self.width()}) def list_doubleclick(self, entry): # review self.playlist = entry.data(Qt.ItemDataRole.UserRole) self.accept() - def open(self): # review + def open(self): # review if self.ui.lstPlaylists.selectedItems(): item = self.ui.lstPlaylists.currentItem() self.playlist = item.data(Qt.ItemDataRole.UserRole) @@ -2086,12 +2073,22 @@ if __name__ == "__main__": p = argparse.ArgumentParser() # Only allow at most one option to be specified group = p.add_mutually_exclusive_group() - group.add_argument('-b', '--bitrates', - action="store_true", dest="update_bitrates", - default=False, help="Update bitrates in database") - group.add_argument('-c', '--check-database', - action="store_true", dest="check_db", - default=False, help="Check and report on database") + group.add_argument( + "-b", + "--bitrates", + action="store_true", + dest="update_bitrates", + default=False, + help="Update bitrates in database", + ) + group.add_argument( + "-c", + "--check-database", + action="store_true", + dest="check_db", + default=False, + help="Check and report on database", + ) args = p.parse_args() # Run as required @@ -2111,13 +2108,16 @@ if __name__ == "__main__": app = QApplication(sys.argv) # PyQt6 defaults to a grey for labels palette = app.palette() - palette.setColor(QPalette.ColorRole.WindowText, - QColor(Config.COLOUR_LABEL_TEXT)) + palette.setColor( + QPalette.ColorRole.WindowText, QColor(Config.COLOUR_LABEL_TEXT) + ) # Set colours that will be used by playlist row stripes - palette.setColor(QPalette.ColorRole.Base, - QColor(Config.COLOUR_EVEN_PLAYLIST)) - palette.setColor(QPalette.ColorRole.AlternateBase, - QColor(Config.COLOUR_ODD_PLAYLIST)) + palette.setColor( + QPalette.ColorRole.Base, QColor(Config.COLOUR_EVEN_PLAYLIST) + ) + palette.setColor( + QPalette.ColorRole.AlternateBase, QColor(Config.COLOUR_ODD_PLAYLIST) + ) app.setPalette(palette) win = Window() win.show() @@ -2129,8 +2129,12 @@ if __name__ == "__main__": from helpers import send_mail msg = stackprinter.format(exc) - send_mail(Config.ERRORS_TO, Config.ERRORS_FROM, - "Exception from musicmuster", msg) + send_mail( + Config.ERRORS_TO, + Config.ERRORS_FROM, + "Exception from musicmuster", + msg, + ) print("\033[1;31;47mUnhandled exception starts") stackprinter.show(style="darkbg") diff --git a/app/playlists.py b/app/playlists.py index 05d981c..477f598 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -1,10 +1,10 @@ import os import re -import stackprinter # type: ignore +import stackprinter # type: ignore import subprocess import threading -import obsws_python as obs # type: ignore +import obsws_python as obs # type: ignore from collections import namedtuple from datetime import datetime, timedelta @@ -14,24 +14,14 @@ from PyQt6.QtCore import ( QEvent, QModelIndex, QObject, - QSize, Qt, QTimer, ) -from PyQt6.QtGui import ( - QAction, - QBrush, - QColor, - QFont, - QDropEvent, - QKeyEvent -) +from PyQt6.QtGui import QAction, QBrush, QColor, QFont, QDropEvent, QKeyEvent from PyQt6.QtWidgets import ( QAbstractItemDelegate, QAbstractItemView, QApplication, - QLineEdit, - QMainWindow, QMenu, QMessageBox, QPlainTextEdit, @@ -39,7 +29,7 @@ from PyQt6.QtWidgets import ( QStyleOptionViewItem, QTableWidget, QTableWidgetItem, - QWidget + QWidget, ) from config import Config @@ -54,14 +44,7 @@ from helpers import ( set_track_metadata, ) from log import log -from models import ( - Playdates, - Playlists, - PlaylistRows, - Settings, - Tracks, - NoteColours -) +from models import Playdates, Playlists, PlaylistRows, Settings, Tracks, NoteColours if TYPE_CHECKING: from musicmuster import Window, MusicMusterSignals @@ -73,11 +56,10 @@ start_time_re = re.compile(r"@\d\d:\d\d:\d\d") HEADER_NOTES_COLUMN = 2 # Columns -Column = namedtuple("Column", ['idx', 'heading']) +Column = namedtuple("Column", ["idx", "heading"]) columns = {} columns["userdata"] = Column(idx=0, heading=Config.COLUMN_NAME_AUTOPLAY) -columns["start_gap"] = Column(idx=1, - heading=Config.COLUMN_NAME_LEADING_SILENCE) +columns["start_gap"] = Column(idx=1, heading=Config.COLUMN_NAME_LEADING_SILENCE) columns["title"] = Column(idx=2, heading=Config.COLUMN_NAME_TITLE) columns["artist"] = Column(idx=3, heading=Config.COLUMN_NAME_ARTIST) columns["duration"] = Column(idx=4, heading=Config.COLUMN_NAME_LENGTH) @@ -101,16 +83,17 @@ ROW_NOTES = columns["row_notes"].idx class EscapeDelegate(QStyledItemDelegate): """ - - increases the height of a row when editing to make editing easier - - closes the edit on control-return - - checks with user before abandoning edit on Escape + - increases the height of a row when editing to make editing easier + - closes the edit on control-return + - checks with user before abandoning edit on Escape """ def __init__(self, parent) -> None: super().__init__(parent) - def createEditor(self, parent: QWidget, option: QStyleOptionViewItem, - index: QModelIndex): + def createEditor( + self, parent: QWidget, option: QStyleOptionViewItem, index: QModelIndex + ): """ Intercept createEditor call and make row just a little bit taller """ @@ -130,15 +113,14 @@ class EscapeDelegate(QStyledItemDelegate): if event.type() == QEvent.Type.KeyPress: key_event = cast(QKeyEvent, event) if key_event.key() == Qt.Key.Key_Return: - if key_event.modifiers() == ( - Qt.KeyboardModifier.ControlModifier - ): + if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier): self.commitData.emit(editor) self.closeEditor.emit(editor) return True elif key_event.key() == Qt.Key.Key_Escape: discard_edits = QMessageBox.question( - self.parent(), "Abandon edit", "Discard changes?") + self.parent(), "Abandon edit", "Discard changes?" + ) if discard_edits == QMessageBox.StandardButton.Yes: self.closeEditor.emit(editor) return True @@ -153,9 +135,13 @@ class PlaylistTab(QTableWidget): TRACK_PATH = Qt.ItemDataRole.UserRole + 3 PLAYED = Qt.ItemDataRole.UserRole + 4 - def __init__(self, musicmuster: "Window", - session: scoped_session, - playlist_id: int, signals: "MusicMusterSignals") -> None: + def __init__( + self, + musicmuster: "Window", + session: scoped_session, + playlist_id: int, + signals: "MusicMusterSignals", + ) -> None: super().__init__() self.musicmuster: Window = musicmuster self.playlist_id = playlist_id @@ -165,10 +151,8 @@ class PlaylistTab(QTableWidget): self.menu = QMenu() self.setItemDelegate(EscapeDelegate(self)) self.setAlternatingRowColors(True) - self.setSelectionMode( - QAbstractItemView.SelectionMode.ExtendedSelection) - self.setSelectionBehavior( - QAbstractItemView.SelectionBehavior.SelectRows) + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked) self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.setRowCount(0) @@ -184,11 +168,12 @@ class PlaylistTab(QTableWidget): self.horizontalHeader().setMinimumSectionSize(0) # Set column headings sorted by idx self.setHorizontalHeaderLabels( - [a.heading for a in list(sorted(columns.values(), - key=lambda item: item.idx))] + [ + a.heading + for a in list(sorted(columns.values(), key=lambda item: item.idx)) + ] ) - self.horizontalHeader().sectionResized.connect( - self.resizeRowsToContents) + self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents) # Drag and drop setup self.setAcceptDrops(True) @@ -221,7 +206,7 @@ class PlaylistTab(QTableWidget): def __repr__(self) -> str: return f"" -# ########## Events other than cell editing ########## + # ########## Events other than cell editing ########## def dropEvent(self, event: QDropEvent) -> None: """ @@ -257,15 +242,19 @@ class PlaylistTab(QTableWidget): for col in range(0, colCount): self.setItem(tgtRow, col, self.takeItem(srcRow, col)) else: - self.setItem(tgtRow, HEADER_NOTES_COLUMN, - self.takeItem(srcRow, HEADER_NOTES_COLUMN)) + self.setItem( + tgtRow, + HEADER_NOTES_COLUMN, + self.takeItem(srcRow, HEADER_NOTES_COLUMN), + ) self.setSpan(tgtRow, HEADER_NOTES_COLUMN, 1, len(columns) - 1) for row in reversed(sorted(rowMapping.keys())): self.removeRow(row) self.resizeRowsToContents() # Scroll to drop zone - self.scrollToItem(self.item(top_row, 1), - QAbstractItemView.ScrollHint.PositionAtTop) + self.scrollToItem( + self.item(top_row, 1), QAbstractItemView.ScrollHint.PositionAtTop + ) event.accept() # Reset drag mode to allow row selection by dragging @@ -277,8 +266,9 @@ class PlaylistTab(QTableWidget): self.hide_or_show_played_tracks() - def _add_context_menu(self, text: str, action: Callable, - disabled: bool = False) -> QAction: + def _add_context_menu( + self, text: str, action: Callable, disabled: bool = False + ) -> QAction: """ Add item to self.menu """ @@ -300,7 +290,7 @@ class PlaylistTab(QTableWidget): self.setDragEnabled(False) super().mouseReleaseEvent(event) -# ########## Cell editing ########## + # ########## Cell editing ########## # We only want to allow cell editing on tracks, artists and notes, # although notes may be section headers. @@ -368,8 +358,7 @@ class PlaylistTab(QTableWidget): elif self.edit_cell_type == ARTIST: track.artist = new_text if update_current: - self.musicmuster.current_track.artist = \ - new_text + self.musicmuster.current_track.artist = new_text if update_next: self.musicmuster.next_track.artist = new_text @@ -383,9 +372,9 @@ class PlaylistTab(QTableWidget): self.clear_selection() - def closeEditor(self, - editor: QWidget, - hint: QAbstractItemDelegate.EndEditHint) -> None: + def closeEditor( + self, editor: QWidget, hint: QAbstractItemDelegate.EndEditHint + ) -> None: """ Override PySide2.QAbstractItemView.closeEditor to enable play controls and update display. @@ -413,9 +402,12 @@ class PlaylistTab(QTableWidget): with Session() as session: self._update_start_end_times(session) - def edit(self, index: QModelIndex, # type: ignore # FIXME - trigger: QAbstractItemView.EditTrigger, - event: QEvent) -> bool: + def edit( + self, + index: QModelIndex, # type: ignore # FIXME + trigger: QAbstractItemView.EditTrigger, + event: QEvent, + ) -> bool: """ Override PySide2.QAbstractItemView.edit to catch when editing starts @@ -472,7 +464,7 @@ class PlaylistTab(QTableWidget): return result -# # ########## Externally called functions ########## + # # ########## Externally called functions ########## def clear_next(self) -> None: """ @@ -511,8 +503,7 @@ class PlaylistTab(QTableWidget): return [self._get_row_plr_id(a) for a in self._get_selected_rows()] - def get_selected_playlistrows( - self, session: scoped_session) -> List[PlaylistRows]: + def get_selected_playlistrows(self, session: scoped_session) -> List[PlaylistRows]: """ Return a list of PlaylistRows of the selected rows """ @@ -547,8 +538,10 @@ class PlaylistTab(QTableWidget): Never hide current or next track """ - current_next = [self._get_current_track_row_number(), - self._get_next_track_row_number()] + current_next = [ + self._get_current_track_row_number(), + self._get_next_track_row_number(), + ] for row_number in range(self.rowCount()): if row_number in current_next: @@ -581,8 +574,13 @@ class PlaylistTab(QTableWidget): self.save_playlist(session) self._update_start_end_times(session) - def insert_row(self, session: scoped_session, plr: PlaylistRows, - update_track_times: bool = True, played=False) -> None: + def insert_row( + self, + session: scoped_session, + plr: PlaylistRows, + update_track_times: bool = True, + played=False, + ) -> None: """ Insert passed playlist row (plr) into playlist tab. """ @@ -603,9 +601,7 @@ class PlaylistTab(QTableWidget): else: # This is a section header so it must have note text if plr.note is None: - log.debug( - f"insert_row({plr=}) with no track_id and no note" - ) + log.debug(f"insert_row({plr=}) with no track_id and no note") return # Use one QTableWidgetItem to span all columns from column 1 @@ -618,8 +614,13 @@ class PlaylistTab(QTableWidget): # Set bold as needed self._set_row_bold(row_number, bold) - def insert_track(self, session: scoped_session, track: Tracks, - note: str = "", repaint: bool = True) -> None: + def insert_track( + self, + session: scoped_session, + track: Tracks, + note: str = "", + repaint: bool = True, + ) -> None: """ Insert track into playlist tab. @@ -640,12 +641,12 @@ class PlaylistTab(QTableWidget): row_number = self.get_new_row_number() # Check to see whether track is already in playlist - existing_plr = PlaylistRows.get_track_plr(session, track.id, - self.playlist_id) - if existing_plr and ask_yes_no("Duplicate row", - "Track already in playlist. " - "Move to new location?", - default_yes=True): + existing_plr = PlaylistRows.get_track_plr(session, track.id, self.playlist_id) + if existing_plr and ask_yes_no( + "Duplicate row", + "Track already in playlist. " "Move to new location?", + default_yes=True, + ): # Yes it is and we should reuse it # If we've been passed a note, we need to add that to the # existing track @@ -654,8 +655,7 @@ class PlaylistTab(QTableWidget): return self._move_row(session, existing_plr, row_number) # Build playlist_row object - plr = PlaylistRows(session, self.playlist_id, track.id, - row_number, note) + plr = PlaylistRows(session, self.playlist_id, track.id, row_number, note) self.insert_row(session, plr) self.save_playlist(session) self._update_start_end_times(session) @@ -698,7 +698,8 @@ class PlaylistTab(QTableWidget): self._set_row_colour_default(row_number) self.clear_selection() self._set_row_last_played_time( - row_number, self.musicmuster.current_track.start_time) + row_number, self.musicmuster.current_track.start_time + ) with Session() as session: self._set_row_note_colour(session, row_number) @@ -719,11 +720,12 @@ class PlaylistTab(QTableWidget): current_row = self._get_current_track_row_number() if current_row is None: if os.environ["MM_ENV"] == "PRODUCTION": - send_mail(Config.ERRORS_TO, - Config.ERRORS_FROM, - "playlists:play_started:current_row is None", - stackprinter.format() - ) + send_mail( + Config.ERRORS_TO, + Config.ERRORS_FROM, + "playlists:play_started:current_row is None", + stackprinter.format(), + ) print("playlists:play_started:current_row is None") stackprinter.show(add_summary=True, style="darkbg") return @@ -734,8 +736,7 @@ class PlaylistTab(QTableWidget): # Set next track next_row = self._find_next_track_row(session, current_row + 1) if next_row: - self.musicmuster.set_next_plr_id(self._get_row_plr_id(next_row), - self) + self.musicmuster.set_next_plr_id(self._get_row_plr_id(next_row), self) # Display row as current track self._set_row_colour_current(current_row) @@ -747,11 +748,13 @@ class PlaylistTab(QTableWidget): self._obs_change_scene(current_row) # Update hidden tracks - QTimer.singleShot(Config.HIDE_AFTER_PLAYING_OFFSET, - self.hide_or_show_played_tracks) + QTimer.singleShot( + Config.HIDE_AFTER_PLAYING_OFFSET, self.hide_or_show_played_tracks + ) - def populate_display(self, session: scoped_session, playlist_id: int, - scroll_to_top: bool = True) -> None: + def populate_display( + self, session: scoped_session, playlist_id: int, scroll_to_top: bool = True + ) -> None: """ Populate display from the associated playlist ID """ @@ -769,25 +772,29 @@ class PlaylistTab(QTableWidget): playlist = session.get(Playlists, playlist_id) if not playlist: if os.environ["MM_ENV"] == "PRODUCTION": - send_mail(Config.ERRORS_TO, - Config.ERRORS_FROM, - "playlists:populate_display:no playlist", - stackprinter.format() - ) + send_mail( + Config.ERRORS_TO, + Config.ERRORS_FROM, + "playlists:populate_display:no playlist", + stackprinter.format(), + ) print("playlists:populate_display:no playlist") stackprinter.show(add_summary=True, style="darkbg") return for plr in playlist.rows: - self.insert_row(session, plr, update_track_times=False, - played=plr.plr_rownum in played_rows) + self.insert_row( + session, + plr, + update_track_times=False, + played=plr.plr_rownum in played_rows, + ) # Scroll to top if scroll_to_top: row0_item = self.item(0, 0) if row0_item: - self.scrollToItem(row0_item, - QAbstractItemView.ScrollHint.PositionAtTop) + self.scrollToItem(row0_item, QAbstractItemView.ScrollHint.PositionAtTop) # Set widths self._set_column_widths(session) @@ -835,8 +842,7 @@ class PlaylistTab(QTableWidget): # Any rows in the database for this playlist that has a row # number equal to or greater than the row count needs to be # removed. - PlaylistRows.delete_higher_rows( - session, self.playlist_id, self.rowCount() - 1) + PlaylistRows.delete_higher_rows(session, self.playlist_id, self.rowCount() - 1) # Get changes into db session.flush() @@ -957,7 +963,7 @@ class PlaylistTab(QTableWidget): # Hide/show rows self.hide_or_show_played_tracks() -# # ########## Internally called functions ########## + # # ########## Internally called functions ########## def _add_track(self, row_number: int) -> None: """Add a track to a section header making it a normal track row""" @@ -1004,49 +1010,54 @@ class PlaylistTab(QTableWidget): # Play with mplayer if track_row and not current: - self._add_context_menu("Play with mplayer", - lambda: self._mplayer_play(row_number)) + self._add_context_menu( + "Play with mplayer", lambda: self._mplayer_play(row_number) + ) # Paste - self._add_context_menu("Paste", - lambda: self.musicmuster.paste_rows(), - self.musicmuster.selected_plrs is None) + self._add_context_menu( + "Paste", + lambda: self.musicmuster.paste_rows(), + self.musicmuster.selected_plrs is None, + ) # Open in Audacity if track_row and not current: - self._add_context_menu("Open in Audacity", - lambda: self._open_in_audacity(row_number) - ) + self._add_context_menu( + "Open in Audacity", lambda: self._open_in_audacity(row_number) + ) # Rescan if track_row and not current: self._add_context_menu( - "Rescan track", lambda: self._rescan(row_number, track_id)) + "Rescan track", lambda: self._rescan(row_number, track_id) + ) # ---------------------- self.menu.addSeparator() # Remove row if not current and not next_row: - self._add_context_menu('Delete row', self._delete_rows) + self._add_context_menu("Delete row", self._delete_rows) # Move to playlist if not current and not next_row: - self._add_context_menu('Move to playlist...', - self.musicmuster.move_selected) + self._add_context_menu( + "Move to playlist...", self.musicmuster.move_selected + ) # ---------------------- self.menu.addSeparator() # Remove track from row if track_row and not current and not next_row: - self._add_context_menu('Remove track from row', - lambda: self._remove_track(row_number)) + self._add_context_menu( + "Remove track from row", lambda: self._remove_track(row_number) + ) # Add track to section header (ie, make this a track row) if header_row: - self._add_context_menu('Add a track', - lambda: self._add_track(row_number)) + self._add_context_menu("Add a track", lambda: self._add_track(row_number)) # Mark unplayed if self._get_row_userdata(row_number, self.PLAYED): @@ -1054,26 +1065,26 @@ class PlaylistTab(QTableWidget): # Unmark as next if next_row: - self._add_context_menu("Unmark as next track", - self.clear_next) + self._add_context_menu("Unmark as next track", self.clear_next) # ---------------------- self.menu.addSeparator() # Info if track_row: - self._add_context_menu('Info', - lambda: self._info_row(track_id)) + self._add_context_menu("Info", lambda: self._info_row(track_id)) # Track path if track_row: - self._add_context_menu("Copy track path", - lambda: self._copy_path(row_number)) + self._add_context_menu( + "Copy track path", lambda: self._copy_path(row_number) + ) # return super(PlaylistTab, self).eventFilter(source, event) - def _calculate_end_time(self, start: Optional[datetime], - duration: int) -> Optional[datetime]: + def _calculate_end_time( + self, start: Optional[datetime], duration: int + ) -> Optional[datetime]: """Return datetime 'duration' ms after 'start'""" if start is None: @@ -1099,7 +1110,7 @@ class PlaylistTab(QTableWidget): attribute_name = f"playlist_{column_name}_col_width" record = Settings.get_int_settings(session, attribute_name) if record.f_int != self.columnWidth(idx): - record.update(session, {'f_int': width}) + record.update(session, {"f_int": width}) def _context_menu(self, pos): """Display right-click menu""" @@ -1149,9 +1160,8 @@ class PlaylistTab(QTableWidget): return # Get confirmation - plural = 's' if row_count > 1 else '' - if not ask_yes_no("Delete rows", - f"Really delete {row_count} row{plural}?"): + plural = "s" if row_count > 1 else "" + if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"): return rows_to_delete = [plr.plr_rownum for plr in plrs] @@ -1173,9 +1183,9 @@ class PlaylistTab(QTableWidget): self._update_start_end_times(session) - def _find_next_track_row(self, session: scoped_session, - starting_row: Optional[int] = None) \ - -> Optional[int]: + def _find_next_track_row( + self, session: scoped_session, 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. @@ -1189,12 +1199,12 @@ class PlaylistTab(QTableWidget): starting_row = 0 track_rows = [ - p.plr_rownum for p in PlaylistRows.get_rows_with_tracks( - session, self.playlist_id) + p.plr_rownum + for p in PlaylistRows.get_rows_with_tracks(session, self.playlist_id) ] played_rows = [ - p.plr_rownum for p in PlaylistRows.get_played_rows( - session, self.playlist_id) + p.plr_rownum + for p in PlaylistRows.get_played_rows(session, self.playlist_id) ] for row_number in range(starting_row, self.rowCount()): if row_number not in track_rows or row_number in played_rows: @@ -1239,8 +1249,7 @@ class PlaylistTab(QTableWidget): return None try: - return datetime.strptime(match.group(0)[1:], - Config.NOTE_TIME_FORMAT) + return datetime.strptime(match.group(0)[1:], Config.NOTE_TIME_FORMAT) except ValueError: return None @@ -1250,8 +1259,9 @@ class PlaylistTab(QTableWidget): """ return [ - p.plr_rownum for p in PlaylistRows.get_played_rows( - session, self.playlist_id) if p.plr_rownum is not None + p.plr_rownum + for p in PlaylistRows.get_played_rows(session, self.playlist_id) + if p.plr_rownum is not None ] def _get_row_artist(self, row_number: int) -> str: @@ -1296,8 +1306,9 @@ class PlaylistTab(QTableWidget): return path - def _get_row_plr(self, session: scoped_session, - row_number: int) -> Optional[PlaylistRows]: + def _get_row_plr( + self, session: scoped_session, row_number: int + ) -> Optional[PlaylistRows]: """ Return PlaylistRows object for this row_number """ @@ -1326,8 +1337,9 @@ class PlaylistTab(QTableWidget): return item_title.text() - def _get_row_track(self, session: scoped_session, - row_number: int) -> Optional[Tracks]: + def _get_row_track( + self, session: scoped_session, row_number: int + ) -> Optional[Tracks]: """Return the track associated with this row_number or None""" track_id = self._get_row_track_id(row_number) @@ -1346,7 +1358,7 @@ class PlaylistTab(QTableWidget): return int(track_id) def _get_row_track_path(self, row_number: int) -> str: - """Return the track path associated with this row_number or '' """ + """Return the track path associated with this row_number or ''""" path = self._get_row_userdata(row_number, self.TRACK_PATH) if not path: @@ -1354,8 +1366,9 @@ class PlaylistTab(QTableWidget): else: return str(path) - def _get_row_userdata(self, row_number: int, - role: int) -> Optional[Union[str, int]]: + def _get_row_userdata( + self, row_number: int, role: int + ) -> Optional[Union[str, int]]: """ Return the specified userdata, if any. """ @@ -1366,15 +1379,14 @@ class PlaylistTab(QTableWidget): return userdata_item.data(role) - def _get_section_timing_string(self, ms: int, - no_end: bool = False) -> str: + def _get_section_timing_string(self, ms: int, no_end: bool = False) -> str: """Return string describing section duration""" duration = ms_to_mmss(ms) caveat = "" if no_end: caveat = " (to end of playlist)" - return ' [' + duration + caveat + ']' + return " [" + duration + caveat + "]" def _get_selected_row(self) -> Optional[int]: """ @@ -1393,8 +1405,7 @@ class PlaylistTab(QTableWidget): # Use a set to deduplicate result (a selected row will have all # items in that row selected) return sorted( - [row_number for row_number in - set([a.row() for a in self.selectedItems()])] + [row_number for row_number in set([a.row() for a in self.selectedItems()])] ) def _info_row(self, track_id: int) -> None: @@ -1445,13 +1456,11 @@ class PlaylistTab(QTableWidget): if website == "wikipedia": QTimer.singleShot( - 0, - lambda: self.musicmuster.tabInfolist.open_in_wikipedia(title) + 0, lambda: self.musicmuster.tabInfolist.open_in_wikipedia(title) ) elif website == "songfacts": QTimer.singleShot( - 0, - lambda: self.musicmuster.tabInfolist.open_in_songfacts(title) + 0, lambda: self.musicmuster.tabInfolist.open_in_songfacts(title) ) else: return @@ -1474,8 +1483,9 @@ class PlaylistTab(QTableWidget): self.clear_selection() self.hide_or_show_played_tracks() - def _move_row(self, session: scoped_session, plr: PlaylistRows, - new_row_number: int) -> None: + def _move_row( + self, session: scoped_session, plr: PlaylistRows, new_row_number: int + ) -> None: """Move playlist row to new_row_number using parent copy/paste""" if plr.plr_rownum is None: @@ -1506,9 +1516,8 @@ class PlaylistTab(QTableWidget): ) return - cmd_list = ['gmplayer', '-vc', 'null', '-vo', 'null', track_path] - thread = threading.Thread( - target=self._run_subprocess, args=(cmd_list,)) + cmd_list = ["gmplayer", "-vc", "null", "-vo", "null", track_path] + thread = threading.Thread(target=self._run_subprocess, args=(cmd_list,)) thread.start() def _obs_change_scene(self, current_row: int) -> None: @@ -1527,11 +1536,13 @@ class PlaylistTab(QTableWidget): scene_name = match_obj.group(1) if scene_name: try: - cl = obs.ReqClient(host=Config.OBS_HOST, - port=Config.OBS_PORT, - password=Config.OBS_PASSWORD) + cl = obs.ReqClient( + host=Config.OBS_HOST, + port=Config.OBS_PORT, + password=Config.OBS_PASSWORD, + ) except ConnectionRefusedError: - log.error(f"OBS connection refused") + log.error("OBS connection refused") return try: @@ -1578,8 +1589,9 @@ class PlaylistTab(QTableWidget): """Remove track from row, making it a section header""" # Get confirmation - if not ask_yes_no("Remove music", - "Really remove the music track from this row?"): + if not ask_yes_no( + "Remove music", "Really remove the music track from this row?" + ): return # Update playlist_rows record @@ -1635,10 +1647,7 @@ class PlaylistTab(QTableWidget): else: note_text += f"{track_id=} not found" self._set_row_header_text(session, row_number, note_text) - log.error( - f"playlists._rescan({track_id=}): " - "Track not found" - ) + log.error(f"playlists._rescan({track_id=}): " "Track not found") self._set_row_colour_unreadable(row_number) self._update_start_end_times(session) @@ -1675,8 +1684,7 @@ class PlaylistTab(QTableWidget): padding_required -= 1 scroll_item = self.item(top_row, 0) - self.scrollToItem(scroll_item, - QAbstractItemView.ScrollHint.PositionAtTop) + self.scrollToItem(scroll_item, QAbstractItemView.ScrollHint.PositionAtTop) def _search(self, next: bool = True) -> None: """ @@ -1759,12 +1767,14 @@ class PlaylistTab(QTableWidget): if ms > 0: self.musicmuster.lblSumPlaytime.setText( - f"Selected duration: {ms_to_mmss(ms)}") + f"Selected duration: {ms_to_mmss(ms)}" + ) else: self.musicmuster.lblSumPlaytime.setText("") - def _set_cell_colour(self, row_number: int, column: int, - colour: Optional[str] = None) -> None: + def _set_cell_colour( + self, row_number: int, column: int, colour: Optional[str] = None + ) -> None: """ Set or reset a cell background colour """ @@ -1794,8 +1804,9 @@ class PlaylistTab(QTableWidget): else: self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH) - def _set_item_text(self, row_number: int, column: int, - text: Optional[str]) -> QTableWidgetItem: + def _set_item_text( + self, row_number: int, column: int, text: Optional[str] + ) -> QTableWidgetItem: """ Set text for item if it exists, else create it, and return item """ @@ -1849,8 +1860,7 @@ class PlaylistTab(QTableWidget): self.clear_selection() - def _set_played_row(self, session: scoped_session, - row_number: int) -> None: + def _set_played_row(self, session: scoped_session, row_number: int) -> None: """Mark this row as played""" _ = self._set_row_userdata(row_number, self.PLAYED, True) @@ -1863,8 +1873,9 @@ class PlaylistTab(QTableWidget): plr.played = True session.flush() - def _set_row_artist(self, row_number: int, - artist: Optional[str]) -> QTableWidgetItem: + def _set_row_artist( + self, row_number: int, artist: Optional[str] + ) -> QTableWidgetItem: """ Set row artist. @@ -1876,8 +1887,9 @@ class PlaylistTab(QTableWidget): return self._set_item_text(row_number, ARTIST, artist) - def _set_row_bitrate(self, row_number: int, - bitrate: Optional[int]) -> QTableWidgetItem: + def _set_row_bitrate( + self, row_number: int, bitrate: Optional[int] + ) -> QTableWidgetItem: """Set bitrate of this row.""" if not bitrate: @@ -1915,8 +1927,7 @@ class PlaylistTab(QTableWidget): if item: item.setFont(boldfont) - def _set_row_colour(self, row_number: int, - colour: Optional[str] = None) -> None: + def _set_row_colour(self, row_number: int, colour: Optional[str] = None) -> None: """ Set or reset row background colour """ @@ -1952,27 +1963,26 @@ class PlaylistTab(QTableWidget): Set next track row colour """ - self._set_row_colour(row_number, Config.COLOUR_NEXT_PLAYLIST) + self._set_row_colour(row_number, Config.COLOUR_NEXT_PLAYLIST) def _set_row_colour_unreadable(self, row_number: int) -> None: """ Set unreadable row colour """ - self._set_row_colour(row_number, Config.COLOUR_UNREADABLE) + self._set_row_colour(row_number, Config.COLOUR_UNREADABLE) - def _set_row_duration(self, row_number: int, - ms: Optional[int]) -> QTableWidgetItem: + def _set_row_duration(self, row_number: int, ms: Optional[int]) -> QTableWidgetItem: """Set duration of this row. Also set in row metadata""" - duration_item = self._set_item_text( - row_number, DURATION, ms_to_mmss(ms)) + duration_item = self._set_item_text(row_number, DURATION, ms_to_mmss(ms)) self._set_row_userdata(row_number, self.ROW_DURATION, ms) return duration_item - def _set_row_end_time(self, row_number: int, - time: Optional[datetime]) -> QTableWidgetItem: + def _set_row_end_time( + self, row_number: int, time: Optional[datetime] + ) -> QTableWidgetItem: """Set row end time""" if not time: @@ -1985,8 +1995,9 @@ class PlaylistTab(QTableWidget): return self._set_item_text(row_number, END_TIME, time_str) - def _set_row_header_text(self, session: scoped_session, - row_number: int, text: str) -> None: + def _set_row_header_text( + self, session: scoped_session, row_number: int, text: str + ) -> None: """ Set header text and row colour """ @@ -1999,7 +2010,7 @@ class PlaylistTab(QTableWidget): Config.ERRORS_TO, Config.ERRORS_FROM, "playlists:_set_row_header_text() called on track row", - stackprinter.format() + stackprinter.format(), ) print("playists:_set_row_header_text() called on track row") stackprinter.show(add_summary=True, style="darkbg") @@ -2016,16 +2027,15 @@ class PlaylistTab(QTableWidget): self._set_row_colour(row_number, note_colour) def _set_row_last_played_time( - self, row_number: int, - last_played: Optional[datetime]) -> QTableWidgetItem: + self, row_number: int, last_played: Optional[datetime] + ) -> QTableWidgetItem: """Set row last played time""" last_played_str = get_relative_date(last_played) return self._set_item_text(row_number, LASTPLAYED, last_played_str) - def _set_row_note_colour(self, session: scoped_session, - row_number: int) -> None: + def _set_row_note_colour(self, session: scoped_session, row_number: int) -> None: """ Set row note colour """ @@ -2034,11 +2044,12 @@ class PlaylistTab(QTableWidget): # track associated if not self._get_row_track_id(row_number): if os.environ["MM_ENV"] == "PRODUCTION": - send_mail(Config.ERRORS_TO, - Config.ERRORS_FROM, - "playlists:_set_row_note_colour() on header row", - stackprinter.format() - ) + send_mail( + Config.ERRORS_TO, + Config.ERRORS_FROM, + "playlists:_set_row_note_colour() on header row", + stackprinter.format(), + ) print("playists:_set_row_note_colour() called on header row") stackprinter.show(add_summary=True, style="darkbg") return @@ -2048,8 +2059,9 @@ class PlaylistTab(QTableWidget): note_colour = NoteColours.get_colour(session, note_text) self._set_cell_colour(row_number, ROW_NOTES, note_colour) - def _set_row_note_text(self, session: scoped_session, - row_number: int, text: str) -> None: + def _set_row_note_text( + self, session: scoped_session, row_number: int, text: str + ) -> None: """ Set row note text and note colour """ @@ -2062,7 +2074,7 @@ class PlaylistTab(QTableWidget): Config.ERRORS_TO, Config.ERRORS_FROM, "playlists:_set_row_note_text() called on header row", - stackprinter.format() + stackprinter.format(), ) print("playists:_set_row_note_text() called on header row") stackprinter.show(add_summary=True, style="darkbg") @@ -2074,16 +2086,16 @@ class PlaylistTab(QTableWidget): # Set colour self._set_row_note_colour(session, row_number) - def _set_row_plr_id(self, row_number: int, - plr_id: int) -> QTableWidgetItem: + def _set_row_plr_id(self, row_number: int, plr_id: int) -> QTableWidgetItem: """ Set PlaylistRows id """ return self._set_row_userdata(row_number, self.PLAYLISTROW_ID, plr_id) - def _set_row_start_gap(self, row_number: int, - start_gap: Optional[int]) -> QTableWidgetItem: + def _set_row_start_gap( + self, row_number: int, start_gap: Optional[int] + ) -> QTableWidgetItem: """ Set start gap on row, set backgroud colour. @@ -2092,8 +2104,7 @@ class PlaylistTab(QTableWidget): if not start_gap: start_gap = 0 - start_gap_item = self._set_item_text( - row_number, START_GAP, str(start_gap)) + start_gap_item = self._set_item_text(row_number, START_GAP, str(start_gap)) if start_gap >= 500: brush = QBrush(QColor(Config.COLOUR_LONG_START)) else: @@ -2102,8 +2113,9 @@ class PlaylistTab(QTableWidget): return start_gap_item - def _set_row_start_time(self, row_number: int, - time: Optional[datetime]) -> QTableWidgetItem: + def _set_row_start_time( + self, row_number: int, time: Optional[datetime] + ) -> QTableWidgetItem: """Set row start time""" if not time: @@ -2116,8 +2128,9 @@ class PlaylistTab(QTableWidget): return self._set_item_text(row_number, START_TIME, time_str) - def _set_row_times(self, row_number: int, start: datetime, - duration: int) -> Optional[datetime]: + def _set_row_times( + self, row_number: int, start: datetime, duration: int + ) -> Optional[datetime]: """ Set row start and end times, return end time """ @@ -2128,8 +2141,7 @@ class PlaylistTab(QTableWidget): return end_time - def _set_row_title(self, row_number: int, - title: Optional[str]) -> QTableWidgetItem: + def _set_row_title(self, row_number: int, title: Optional[str]) -> QTableWidgetItem: """ Set row title. """ @@ -2139,25 +2151,23 @@ class PlaylistTab(QTableWidget): return self._set_item_text(row_number, TITLE, title) - def _set_row_track_id(self, row_number: int, - track_id: int) -> QTableWidgetItem: + def _set_row_track_id(self, row_number: int, track_id: int) -> QTableWidgetItem: """ Set track id """ return self._set_row_userdata(row_number, self.ROW_TRACK_ID, track_id) - def _set_row_track_path(self, row_number: int, - path: str) -> QTableWidgetItem: + def _set_row_track_path(self, row_number: int, path: str) -> QTableWidgetItem: """ Set track path """ return self._set_row_userdata(row_number, self.TRACK_PATH, path) - def _set_row_userdata(self, row_number: int, role: int, - value: Optional[Union[str, int]]) \ - -> QTableWidgetItem: + def _set_row_userdata( + self, row_number: int, role: int, value: Optional[Union[str, int]] + ) -> QTableWidgetItem: """ Set passed userdata in USERDATA column """ @@ -2171,25 +2181,26 @@ class PlaylistTab(QTableWidget): return item - def _track_time_between_rows(self, session: scoped_session, - from_plr: PlaylistRows, - to_plr: PlaylistRows) -> int: + def _track_time_between_rows( + self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows + ) -> int: """ Returns the total duration of all tracks in rows between from_row and to_row inclusive """ plr_tracks = PlaylistRows.get_rows_with_tracks( - session, self.playlist_id, from_plr.plr_rownum, to_plr.plr_rownum) + session, self.playlist_id, from_plr.plr_rownum, to_plr.plr_rownum + ) total_time = 0 - total_time = sum([a.track.duration for a in plr_tracks - if a.track.duration]) + total_time = sum([a.track.duration for a in plr_tracks if a.track.duration]) return total_time - def _update_row_track_info(self, session: scoped_session, row: int, - track: Tracks) -> None: + def _update_row_track_info( + self, session: scoped_session, row: int, track: Tracks + ) -> None: """ Update the passed row with info from the passed track. """ @@ -2199,7 +2210,8 @@ class PlaylistTab(QTableWidget): _ = self._set_row_duration(row, track.duration) _ = self._set_row_end_time(row, None) _ = self._set_row_last_played_time( - row, Playdates.last_played(session, track.id)) + row, Playdates.last_played(session, track.id) + ) _ = self._set_row_start_gap(row, track.start_gap) _ = self._set_row_start_time(row, None) _ = self._set_row_title(row, track.title) @@ -2216,11 +2228,12 @@ class PlaylistTab(QTableWidget): section_start_rows: List[PlaylistRows] = [] - header_rows = [self._get_row_plr_id(row_number) for row_number in - range(self.rowCount()) - if self._get_row_track_id(row_number) == 0] - plrs = PlaylistRows.get_from_id_list(session, self.playlist_id, - header_rows) + header_rows = [ + self._get_row_plr_id(row_number) + for row_number in range(self.rowCount()) + if self._get_row_track_id(row_number) == 0 + ] + plrs = PlaylistRows.get_from_id_list(session, self.playlist_id, header_rows) for plr in plrs: if plr.note.endswith("+"): section_start_rows.append(plr) @@ -2230,25 +2243,29 @@ class PlaylistTab(QTableWidget): from_plr = section_start_rows.pop() to_plr = plr total_time = self._track_time_between_rows( - session, from_plr, to_plr) + session, from_plr, to_plr + ) time_str = self._get_section_timing_string(total_time) - self._set_row_header_text(session, from_plr.plr_rownum, - from_plr.note + time_str) + self._set_row_header_text( + session, from_plr.plr_rownum, from_plr.note + time_str + ) # Update section end if to_plr.note.strip() == "-": new_text = ( - "[End " + re.sub( - section_header_cleanup_re, '', from_plr.note, - ).strip() + "]" + "[End " + + re.sub( + section_header_cleanup_re, + "", + from_plr.note, + ).strip() + + "]" ) - self._set_row_header_text(session, to_plr.plr_rownum, - new_text) + self._set_row_header_text(session, to_plr.plr_rownum, new_text) except IndexError: # This ending row may have a time left from before a # starting row above was deleted, so replace content - self._set_row_header_text(session, plr.plr_rownum, - plr.note) + self._set_row_header_text(session, plr.plr_rownum, plr.note) continue # If we still have plrs in section_start_rows, there isn't an end @@ -2257,20 +2274,18 @@ class PlaylistTab(QTableWidget): if possible_plr: to_plr = possible_plr for from_plr in section_start_rows: - total_time = self._track_time_between_rows(session, - from_plr, to_plr) - time_str = self._get_section_timing_string(total_time, - no_end=True) - self._set_row_header_text(session, from_plr.plr_rownum, - from_plr.note + time_str) + total_time = self._track_time_between_rows(session, from_plr, to_plr) + time_str = self._get_section_timing_string(total_time, no_end=True) + self._set_row_header_text( + session, from_plr.plr_rownum, from_plr.note + time_str + ) def _update_start_end_times(self, session: scoped_session) -> None: - """ Update track start and end times """ + """Update track start and end times""" current_track_end_time = self.musicmuster.current_track.end_time current_track_row = self._get_current_track_row_number() - current_track_start_time = ( - self.musicmuster.current_track.start_time) + current_track_start_time = self.musicmuster.current_track.start_time next_start_time = None next_track_row = self._get_next_track_row_number() played_rows = self._get_played_rows(session) @@ -2279,13 +2294,14 @@ class PlaylistTab(QTableWidget): # Don't change start times for tracks that have been # played other than current/next row if row_number in played_rows and row_number not in [ - current_track_row, next_track_row]: + current_track_row, + next_track_row, + ]: continue # Get any timing from header row (that's all we need) if self._get_row_track_id(row_number) == 0: - note_time = self._get_note_text_time( - self._get_row_note(row_number)) + note_time = self._get_note_text_time(self._get_row_note(row_number)) if note_time: next_start_time = note_time continue @@ -2298,16 +2314,17 @@ class PlaylistTab(QTableWidget): if row_number == next_track_row: if current_track_end_time: next_start_time = self._set_row_times( - row_number, current_track_end_time, - self._get_row_duration(row_number)) + row_number, + current_track_end_time, + self._get_row_duration(row_number), + ) continue # Else set track times below if row_number == current_track_row: if not current_track_start_time: continue - self._set_row_start_time(row_number, - current_track_start_time) + self._set_row_start_time(row_number, current_track_start_time) self._set_row_end_time(row_number, current_track_end_time) # Next track may be above us so only reset # next_start_time if it's not set @@ -2323,13 +2340,16 @@ class PlaylistTab(QTableWidget): # If we're between the current and next row, zero out # times - if (current_track_row and next_track_row and - current_track_row < row_number < next_track_row): + if ( + current_track_row + and next_track_row + and current_track_row < row_number < next_track_row + ): self._set_row_start_time(row_number, None) self._set_row_end_time(row_number, None) else: next_start_time = self._set_row_times( - row_number, next_start_time, - self._get_row_duration(row_number)) + row_number, next_start_time, self._get_row_duration(row_number) + ) self._update_section_headers(session) diff --git a/app/replace_files.py b/app/replace_files.py index 50f8037..d645f71 100755 --- a/app/replace_files.py +++ b/app/replace_files.py @@ -5,16 +5,12 @@ # parent (eg, bettet bitrate). import os -import pydymenu # type: ignore +import pydymenu # type: ignore import shutil import sys from helpers import ( - fade_point, - get_audio_segment, get_tags, - leading_silence, - trailing_silence, set_track_metadata, ) @@ -29,7 +25,7 @@ process_tag_matches = True do_processing = True process_no_matches = True -source_dir = '/home/kae/music/Singles/tmp' +source_dir = "/home/kae/music/Singles/tmp" parent_dir = os.path.dirname(source_dir) # ######################################################### @@ -46,7 +42,7 @@ def main(): # We only want to run this against the production database because # we will affect files in the common pool of tracks used by all # databases - if 'musicmuster_prod' not in os.environ.get('MM_DB'): + if "musicmuster_prod" not in os.environ.get("MM_DB"): response = input("Not on production database - c to continue: ") if response != "c": sys.exit(0) @@ -68,8 +64,8 @@ def main(): artists_to_path = {} for k, v in parents.items(): try: - titles_to_path[v['title'].lower()] = k - artists_to_path[v['artist'].lower()] = k + titles_to_path[v["title"].lower()] = k + artists_to_path[v["artist"].lower()] = k except AttributeError: continue @@ -78,44 +74,43 @@ def main(): if not os.path.isfile(new_path): continue new_tags = get_tags(new_path) - new_title = new_tags['title'] - new_artist = new_tags['artist'] - bitrate = new_tags['bitrate'] + new_title = new_tags["title"] + new_artist = new_tags["artist"] + bitrate = new_tags["bitrate"] # If same filename exists in parent direcory, check tags parent_path = os.path.join(parent_dir, new_fname) if os.path.exists(parent_path): parent_tags = get_tags(parent_path) - parent_title = parent_tags['title'] - parent_artist = parent_tags['artist'] - if ( - (str(parent_title).lower() == str(new_title).lower()) and - (str(parent_artist).lower() == str(new_artist).lower()) + parent_title = parent_tags["title"] + parent_artist = parent_tags["artist"] + if (str(parent_title).lower() == str(new_title).lower()) and ( + str(parent_artist).lower() == str(new_artist).lower() ): name_and_tags.append( f" {new_fname=}, {parent_title} → {new_title}, " f" {parent_artist} → {new_artist}" ) if process_name_and_tags_matches: - process_track(new_path, parent_path, new_title, - new_artist, bitrate) + process_track(new_path, parent_path, new_title, new_artist, bitrate) continue # Check for matching tags although filename is different if new_title.lower() in titles_to_path: possible_path = titles_to_path[new_title.lower()] - if parents[possible_path]['artist'].lower() == new_artist.lower(): + if parents[possible_path]["artist"].lower() == new_artist.lower(): # print( # f"title={new_title}, artist={new_artist}:\n" # f" {new_path} → {parent_path}" # ) tags_not_name.append( - f"title={new_title}, artist={new_artist}:\n" - f" {new_path} → {parent_path}" - ) + f"title={new_title}, artist={new_artist}:\n" + f" {new_path} → {parent_path}" + ) if process_tag_matches: - process_track(new_path, possible_path, new_title, - new_artist, bitrate) + process_track( + new_path, possible_path, new_title, new_artist, bitrate + ) continue else: no_match += 1 @@ -132,8 +127,8 @@ def main(): if choice: old_file = os.path.join(parent_dir, choice[0]) oldtags = get_tags(old_file) - old_title = oldtags['title'] - old_artist = oldtags['artist'] + old_title = oldtags["title"] + old_artist = oldtags["artist"] print() print(f" File name will change {choice[0]}") print(f" → {new_fname}") @@ -232,11 +227,8 @@ def main(): def process_track(src, dst, title, artist, bitrate): - new_path = os.path.join(os.path.dirname(dst), os.path.basename(src)) - print( - f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n" - ) + print(f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n") if not do_processing: return diff --git a/app/utilities.py b/app/utilities.py index b769128..878a7aa 100755 --- a/app/utilities.py +++ b/app/utilities.py @@ -4,13 +4,7 @@ import os from config import Config from helpers import ( - fade_point, - get_audio_segment, get_tags, - leading_silence, - normalise_track, - set_track_metadata, - trailing_silence, ) from log import log from models import Tracks @@ -72,12 +66,14 @@ def check_db(session): print("Invalid paths in database") print("-------------------------") for t in paths_not_found: - print(f""" + print( + f""" Track ID: {t.id} Path: {t.path} Title: {t.title} Artist: {t.artist} - """) + """ + ) if more_files_to_report: print("There were more paths than listed that were not found") diff --git a/conftest.py b/conftest.py index db28026..e6c0b36 100644 --- a/conftest.py +++ b/conftest.py @@ -1,15 +1,15 @@ # https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629 import pytest -import sys -sys.path.append("app") -import models + +# Flake8 doesn't like the sys.append within imports +# import sys +# sys.path.append("app") from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker - @pytest.fixture(scope="session") def connection(): engine = create_engine( @@ -21,7 +21,8 @@ def connection(): @pytest.fixture(scope="session") def setup_database(connection): - from app.models import Base # noqa E402 + from app.models import Base # noqa E402 + Base.metadata.bind = connection Base.metadata.create_all() # seed_database() diff --git a/devnotes.txt b/devnotes.txt new file mode 100644 index 0000000..08ac9e3 --- /dev/null +++ b/devnotes.txt @@ -0,0 +1 @@ +Run Flake8 and Black diff --git a/docs/build/doctrees/environment.pickle b/docs/build/doctrees/environment.pickle index 26141455eca6e81f926b6f29a0abcf265b11f402..d89b15e2c7680fae75547a1263d9e6a213235d96 100644 GIT binary patch delta 3644 zcma);eNa%dST|4e}r#s!<#@deC*|xT8cUO02$JxF2y?lW>yZ_vG z?z!ju&UxpaoVJ+ zFg1+HVy$A#oT~#H+Bu!cq!SFojAjE<=T46A35}?uLP8ejeV*Jf(p$L7{TaPnODC+b zGG@BUVd9Kd6GK}~x_Xn*&e>_BndWrmc9>N%30AIEC|_&{i5sG!b*%@+muTVWIt6Sf zQNz7;9$-rI;qiI}Tq@N<)&|eG1$hq6Ql;Z?W_nr=pKn|SZG1(vg5 zM!5$rF!Nxp9y6FENZk?vcD={_azzS)KNvhR)e0*q6E0Uh)Ea46XpD`c#UUZ=r?^z5 zF7^xj=ooF3il#zhWjP$(BBrK@VX)XHo|W(5IExu}vx{Q2W4!~1>Tx?=&8|jQGW!+A zvU3SWq82t;gj7wf?S%hSUWZ{*g4o393`TPW+%v_8aWVCmr~#i(0Fn6+^qS+J zWAml?dsK?S1#uflBL5(PY+-nLjHRVoAy_eJt+mJj1J-G%78@HWI|-kMtk7JYjXK~` z^>nnCPcL2Y*c~{Z0AJfC!4+F9^jj5Jeg!_Yp5;9!z#Y35hN=}1!>QqmYM(aDk%fK> z^Bg%KvHNs$YcfH^DPVhz3i3FwHfn0V5x1U|vC(!$Z!sI_WN5CP2{{gh^4X(uBQ6Wa zs9>XGM2H`ivy3rUa9e20X8PIk-LIpesz&hwBROAV8vk@`oE?5%^DB%*4drmARsmc? z9z3e`Ch4x{rjGg{I6D7}_8(WYCBi$c9+cwO zwCqX?a{tY)#Gsv|JrSaJ-hn6WsiHD+HcuI3Z1YB^y5DU@J{(;DYp1K8J#gw7*1D$=E!VWY(NR-Bi%7EfjyD< zNvndyqxneX{#Q>1!kv)vi~BRu$)OrAc5mh^(Kzki)c7MdRtonQG@%%vk2TBJ5)VFl zp+9=e>L8^zT1Q6Lssu>uvBeZVd&q9)Q`sz}vYAiCM9hqM@uxyGfmFy5yIM%45YE59 zk2)ZQ6{j^2sg^<0@z+o<+&sPwH^R;1ufetxIp``cJw7oPeMBTw2iB9h)Mrv?zNjIS z4<4M1&pbe=eWaMZL^?~P0V4Gi=_-*1i1ZPWNSCu1kp0UVbP4hX;=w#^R=ChTKapgL z;qHKv`cVo!sU)wLpzoBD`c?{>p?HWGm?TPOf52zC6R~&K`6)2xbPO5~1*a?Jlnj&k z!5k^`I*a8JzcNBb5bo(`C`8Tl8$el~6h*+JzI;kLN=ZWMGNYlXFO~X63V-9GL32$? z`D2sJcb}!vCLDVmi2CD#;+ObILGjlK%A2zlgh$XTXD8wCB6!%J6U@z~-j>1eaa>>b z1h_GfLbc1_hC3Uko>Njg1@W(GQB;c{UO%@a*f}0v9aK_R0)Y1hmGZ;>A`B5Ht-RN- zI4>wp`4xk_BK#ayILg_6c+{Ugn*ZL3iPQrhUxNeY zYmS^B$`48R@$HcX=|gkJywuPXwBGCe(xsU}-o?DPJm6hK6nSVB3|~qZ4Uw;m6AKX< zy{MMUX+fxlv8dYTqDWVNQuMMHbtmBD*py$IId$VGMT zgEtbWX{izTj#|ltNW&7iRDqA_0yYDy3fS53$KgymiMrrX|-ey;lGdV_^~tun^@}w6AWl{u{z=5I}Nqj zIWHmo_6$rYz8#Nhz;T-$SC1`mn(S8mWVgY?+leu4Sllw79dnX2vQbk*jL5QOew&MJ pBD-eaj(D+xD?WRh>^fj@WGU|Frz45f(=|YUxCr&SnGZA2e*r%T?o0px delta 3792 zcmai1dr(u^8RuM#ge0g}Fou`8K32sSpty-uke4zRA22%08wtrpat#S2Hy|#^s}Zq5 z#qU~Qt*e4hP;p~zQM;mccDhwtXFAi}x;3tyaof6J*S6d3Y-h)wb8iTUIR5k9@AdnA z-}%nH=icky6?m{cP}V3r;Qsis>}ixnX=1Bci>c1qi4P`gQ@gYbHL-g zfe7u!a$5~k;k5APN((z2WNeU_tA&sPnt5}ImEjGg4#+4NfZ9AAv=`EFD{lb|7JA`s zekx2TreRuv4swdUs-zSrZ>uoy2D6cYl)|-8vY8(3On@&6L!e_bMi-?)X9*43Vm&-6 z@xs#0^WlF68ec;pq4X=Tl_WsEk%lWJF>uuAh3kf7N*)B?8`7bR^@7&8oYDqCp>ZWO zJqS*?48ilbzu>`h(Lr!I$L^^$%}118ip-ZH31UPUlTvE7nE6^V$|q$ETqx5-tq;P; zKVW!~z&*kj_p3<@sVtEREd=lTh0wb}0TXk>>}Z-18Ox|G zQa6jFZj7XxDk|3vTg)E?Ryr7%#W4_99VM%9I?P6>;091N%+nc>x`R7uI)yucAS3n*x`sB##<4 zR))j8N)4R1(^#JkPwhjI#3~&;a?s$aS^!h|A+OWE6s9_9xNe^ZdCnp4IY$DemcwI* z9`08Aoa))BX5PV6vUbM78f{h+Gq;qibdaA1pV!cD3je^Vg@xOE9E!zjS?d}ebI?>54>uaT6Jyues#!cl3v1+AQ!Ko!p8>zF*T83u z^cb;Nh-!MQO1ITmnY4}X;mI+8cBtWSgBsQ}XdttB@>s!{aI)D8M;ephP74ixZ(IVZ zA9>LdC~A&Gu~6Tvf$kQ~WLfa2sShW-10T5VLqk1{7C}=>sC=khXlf17AdY}^s&cXxal6H2t>@J43`8{( zd<4kdNyrTu?)lUn;)sV+?iJK-0*s4tkH>Eilg%h_|O`!~BfD-IFmC$bkl6VAx3O*C@Ex|eihYauZs2ttoo)oHIMa5)N? zl$L^WJQW`-Mkoi4AAd5Zh~(vC;qWbwBc3*guYWPbtyaL3(-+JDeD0}9&%DYy`z8$j0IZL@Z*nDyZJoH3R*Css^_%03yaJgq|v zFx|QE`_mC98jg4C;NXP_N~46tGkPjYNis(Qdd{d(3I^ms)Y-|(I3=bQAKN4h)hl&= zd%36XEQ3%kaYMm*1zHYYoJ&P|zpWuQH3cu}+O#q1ygbjKc3j6fqQ>q~HCjco&G8B} z1(WHaxF=~0rpV54)CYq-cKm>sT*yWqEcNhg5si99E#YFuYpC&X2&4W6_b(chpZM4G z6u$1&qX+P1Zz}o}VlJm5FZ}mX;%jIbSbC>Yp(=RZvl{O9MyQ4uMTtk^h3h& z7)vi)x;z%go-Q?&Bylu-IFe(z$f0`R?@KQ^KJ`16iVnFKJH#94<)x+L=5nJqb3AtunV02%dfx3NM-(Fobg6r%ku2XO2y1!|Xue;y*7`Og% z<_N~SqZohl7Dg?4;p6$NZ_Wsw=c9POdJB&X=5$2LrMZRSPx-#pcq_85FBtLmphLtr z2R-o25G>O87nX{lKSAvmFhoCAee10iPk^}U`e z!LIe30XE!BV2q+}5+(WYUC*&tZ{y0a#2@nI(0Q|PGI_}s2~MY*;vC{<00(scEX2XP z{W{#awO`F}IKN)t_DYu>7?2s@bO+iUybsXTKL-Qu^+%&Fc-GIQpTU{9EAAY=a_6yd ztJdLvK>TyH#%f}>39UkDtKc5*U=4O-xxih)8Rbdbt0*{ht2nNU5D-zGv{T?N66?~C zMNmE{j`b1i8qB_(IZOO(keduV%bP1${6|VICf33m1Q0TD@9U1|!hzfA=z-_q?ZxQ- Dy%8O+ diff --git a/docs/build/doctrees/introduction.doctree b/docs/build/doctrees/introduction.doctree index 2f2132ca5fccd2008bc168852b9d4b7188c87866..d4938e104646011dc27e634ed616d2fb2f747769 100644 GIT binary patch delta 1759 zcmah}ZERCj80OsWvX3rZ*LCe?zi!)gUDuCo%?fl~#$cF$fI|l3Lyg>Zce^cJTen;1 zs0N6cvSdFXPyHpR#P|^+id19#L$V+G!(SvOM$nK&4FR%YCz5psT(@Q3(UKVSb5|F({I3}&S?j_Hk+iL{`IBU3o1Yg#6HO?L?4`Vdak zX|ccFhp+4Ms53-~51R~q_*9?Q^{>k*nF&!5l|)>KL)S2Xef8RZhP!dRp$3KeJe9lSVD-O|5v%n8#VuYH}RcOug9Gn8!(TxTZ)} ztdJ|8F32XlX>LK#G>a=uyK$*$0j=h|);E|{5}CA^O3fhoh=8b}&h-5~%KU@rcJd>{ z#t841Ey+dH>-#Ia*5?!@lZM$6GyGPjmBRt;Y~gU>#X=jU+)~K_`t>U%ujO_hh1q%z z7p#uTlq8NO(&PBdW8;-cF}@$owu|HdZraY^J-cn0{AK$^;=qC!7(UcX9`u zYlwuRGl;A(La_s`KKw3dG4Jxh>|~K9cP{~Uy1THb?ytIIPA>#ak$-J+#mKik9T@M_WPq0JXhc&i9v zt;~n4&jGKmS#`F9y637+2H8<;CRv|bZ)ThFpo}Gmp#;&2UE=&q17wQp;~s%Uw0eKN-%FT+$Tv-5UupV^3r+apU8Ni}NzD z6*nWkYEOlVa#rAm%Z?4JJoI)YS8dTdSiE$DHc}OH@rzYDQemc1oae(US!_vzb+HDx z*Sy7TQ%_&HwG`!|!(;$Y)Y>r}jd2Nv;Y7A delta 2055 zcmah~ZD>I@-_0NHO z&htFyyyv{Y`CO8yL`mBHOWpVg`CW!*fhJBOL1wr%rD(l=3jEZ zRertf;??|L)$f+lGqH!u3e3d+1NU145YY{{I-+b&NKP~C)D)BE_A_#pO}v(4WC?b= z+h9gVxBoAK$;{Q0gCBKACT!!pw4X`kld_OWafZdH@xM-l^Ok)u48VcHVLwQ+Bu`(+p7?{+rhmw(g(iwz%`Ud#W zSb)4?0CqOf@TnmLN1F;D8Xd59#$hB9Qi|uM101Ep}3F~q~*D0K`Aa*oMxStiFNr1TWyijz|Z+wFn7waz^o6&LUd$ef#z z_9)8$$DJwDQz-%?Tzap}V-#2|mfmV3nVigK<)<#Xrfz=7kF&H3S@3s93QeAHjyG85 z@t|{tw6z}E>fpX1sP4zjHv#f?P~v3eUuQj{Rx1HAl>iCUiIWs$!d57DwyJxucNe|{ zjs|gp93D@O1nqh;`uISH322!ZQ9x_(R~(96G@No-;d)m=*F7rj<8VilTteoix?z*M z7mj-9XZRiPndeCB25EnqSw9{log_~MH#PUVz(kiHieBxa2pfFuJq$5V2ueO0-t+Xp zRbRorYHrQ(mghJ&n;-*V68UTvzlGgt?tlz}uig9Qdg8yiE`)7n)0GpEK1#0rV_y&zk#O`G50K{{;J=(c>4GKro{nQ{Z{IMl5+^> z{0-*}!Om`zW-BgQ+y;g2bCgAeuaLJ%hvRK3d>PDq-P}q?D?-m{MD-FtP_q#Od@B5? z*&9R;0TO&g3T#3s3Lmf9iMD|;=%8LEXfuol-OI)m)EFUI;Y`qp*Y&I5NaHB+E3d|8 ztP-s_L(7pWioI}69ncV8n+l}WA5n*t)MJW=?Tg`5+q72rzHbm7g#Fc`%S5eK^mZ8U zkD;^hLH}OrBB8rrFzl^AuheoDq_9UF!+K>n9e#!SLP@ZLI8?y)8DZnV7}{PrIUrNi zQg!8pwRh^?xP`gm4I+@|v-}KyUR*&uO~f20<`^+Q5c3^2`O>m9o8si@43{JSV3F{P ve7?xf6syc)l~t@Vih15nFjaXimiO`J`2$sLzM=@zWGq!>v0OPg6hr?3G_9oc diff --git a/docs/build/html/_sources/introduction.rst.txt b/docs/build/html/_sources/introduction.rst.txt index c07c65c..39e088a 100644 --- a/docs/build/html/_sources/introduction.rst.txt +++ b/docs/build/html/_sources/introduction.rst.txt @@ -29,12 +29,10 @@ Features * Database backed * Can be almost entirely keyboard driven -* Playlist management -* Easily add new tracks to playlists -* Show multiple playlists on tabs +* Open multiple playlists in tabs * Play tracks from any playlist * Add notes/comments to tracks on playlist -* Automataic olour-coding of notes/comments according to content +* Automatatic colour-coding of notes/comments according to content * Preview tracks before playing to audience * Time of day clock * Elapsed track time counter @@ -42,8 +40,8 @@ Features * Time to run until track is silent * Graphic of volume from 5 seconds (configurable) before fade until track is silent -* Ability to hide played tracks in playlist -* Buttone to drop playout volume by 3dB for talkover +* Optionally hide played tracks in playlist +* Button to drop playout volume by 3dB for talkover * Playlist displays: * Title * Artist @@ -51,18 +49,18 @@ Features * Estimated start time of track * Estimated end time of track * When track was last played - * Bits per second (bps bitrate) of track - * Length of silence in recording before music starts + * Bits per second (bitrate) of track + * Length of leading silence in recording before track starts * Total track length of arbitrary sections of tracks * Commands that are sent to OBS Studio (eg, for automated scene changes) * Playlist templates -* Move selected/unplayed tracks between playlists -* Down CSV of played tracks between arbitrary dates/times +* Move selected or unplayed tracks between playlists +* Download CSV of tracks played between arbitrary dates/times * Search for tracks by title or artist -* Automatic search of current/next track in Wikipedia -* Optional search of selected track in Wikipedia -* Optional search of selected track in Songfacts +* Automatic search of current and next track in Wikipedia +* Optional search of any track in Wikipedia +* Optional search of any track in Songfacts Requirements diff --git a/docs/build/html/introduction.html b/docs/build/html/introduction.html index 706bc74..b558719 100644 --- a/docs/build/html/introduction.html +++ b/docs/build/html/introduction.html @@ -221,12 +221,10 @@ production of live internet radio shows.

  • Database backed

  • Can be almost entirely keyboard driven

  • -
  • Playlist management

  • -
  • Easily add new tracks to playlists

  • -
  • Show multiple playlists on tabs

  • +
  • Open multiple playlists in tabs

  • Play tracks from any playlist

  • Add notes/comments to tracks on playlist

  • -
  • Automataic olour-coding of notes/comments according to content

  • +
  • Automatatic colour-coding of notes/comments according to content

  • Preview tracks before playing to audience

  • Time of day clock

  • Elapsed track time counter

  • @@ -234,8 +232,8 @@ production of live internet radio shows.

  • Time to run until track is silent

  • Graphic of volume from 5 seconds (configurable) before fade until track is silent

  • -
  • Ability to hide played tracks in playlist

  • -
  • Buttone to drop playout volume by 3dB for talkover

  • +
  • Optionally hide played tracks in playlist

  • +
  • Button to drop playout volume by 3dB for talkover

  • Playlist displays:
    • Title

    • @@ -244,8 +242,8 @@ track is silent

    • Estimated start time of track

    • Estimated end time of track

    • When track was last played

    • -
    • Bits per second (bps bitrate) of track

    • -
    • Length of silence in recording before music starts

    • +
    • Bits per second (bitrate) of track

    • +
    • Length of leading silence in recording before track starts

    • Total track length of arbitrary sections of tracks

    • Commands that are sent to OBS Studio (eg, for automated scene changes)

    • @@ -254,12 +252,12 @@ changes)

  • Playlist templates

  • -
  • Move selected/unplayed tracks between playlists

  • -
  • Down CSV of played tracks between arbitrary dates/times

  • +
  • Move selected or unplayed tracks between playlists

  • +
  • Download CSV of tracks played between arbitrary dates/times

  • Search for tracks by title or artist

  • -
  • Automatic search of current/next track in Wikipedia

  • -
  • Optional search of selected track in Wikipedia

  • -
  • Optional search of selected track in Songfacts

  • +
  • Automatic search of current and next track in Wikipedia

  • +
  • Optional search of any track in Wikipedia

  • +
  • Optional search of any track in Songfacts

diff --git a/docs/build/html/searchindex.js b/docs/build/html/searchindex.js index 4f93a78..6ba8009 100644 --- a/docs/build/html/searchindex.js +++ b/docs/build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["development", "index", "installation", "introduction", "reference", "tutorial"], "filenames": ["development.rst", "index.rst", "installation.rst", "introduction.rst", "reference.rst", "tutorial.rst"], "titles": ["Development", "Welcome to MusicMuster\u2019s documentation!", "Installation", "Introduction", "Reference", "Tutorial"], "terms": {"index": 1, "modul": 1, "search": [1, 3], "page": [1, 3], "i": 1, "music": [1, 3], "player": [1, 3], "target": 1, "product": [1, 3], "live": [1, 3], "internet": [1, 3], "radio": [1, 3], "show": [1, 3], "content": 3, "introduct": 1, "instal": [1, 3], "tutori": 1, "refer": 1, "develop": 1, "why": 1, "what": 1, "featur": 1, "requir": 1, "feedback": 1, "bug": 1, "etc": 1, "In": 3, "januari": 3, "2022": 3, "start": 3, "my": 3, "mixcloud": 3, "http": 3, "www": 3, "com": 3, "keithsmusicbox": 3, "until": 3, "had": 3, "been": 3, "an": 3, "station": 3, "which": 3, "me": 3, "us": 3, "window": 3, "playout": 3, "system": 3, "As": 3, "onli": 3, "linux": 3, "set": 3, "up": 3, "pc": 3, "specif": 3, "purpos": 3, "The": 3, "felt": 3, "were": 3, "shortcom": 3, "variou": 3, "area": 3, "onc": 3, "move": 3, "equival": 3, "didn": 3, "t": 3, "have": 3, "same": 3, "wa": 3, "unabl": 3, "find": 3, "one": 3, "met": 3, "criteria": 3, "decid": 3, "see": 3, "how": 3, "practic": 3, "would": 3, "write": 3, "own": 3, "born": 3, "It": 3, "base": 3, "whilst": 3, "could": 3, "gener": 3, "home": 3, "ar": 3, "much": 3, "better": 3, "applic": 3, "role": 3, "ha": 3, "design": 3, "support": 3, "databas": 3, "back": 3, "can": 3, "almost": 3, "entir": 3, "keyboard": 3, "driven": 3, "playlist": 3, "manag": 3, "easili": 3, "add": 3, "new": 3, "track": 3, "multipl": 3, "tab": 3, "plai": 3, "from": 3, "ani": 3, "note": 3, "comment": 3, "automata": 3, "olour": 3, "code": 3, "accord": 3, "preview": 3, "befor": 3, "audienc": 3, "time": 3, "dai": 3, "clock": 3, "elaps": 3, "counter": 3, "run": 3, "fade": 3, "silent": 3, "graphic": 3, "volum": 3, "5": 3, "second": 3, "configur": 3, "abil": 3, "hide": 3, "button": 3, "drop": 3, "3db": 3, "talkov": 3, "titl": 3, "artist": 3, "length": 3, "mm": 3, "ss": 3, "estim": 3, "end": 3, "when": 3, "last": 3, "bit": 3, "per": 3, "bp": 3, "bitrat": 3, "silenc": 3, "record": 3, "total": 3, "arbitrari": 3, "section": 3, "command": 3, "sent": 3, "ob": 3, "studio": 3, "eg": 3, "autom": 3, "scene": 3, "chang": 3, "templat": 3, "select": 3, "unplai": 3, "between": 3, "down": 3, "csv": 3, "date": 3, "automat": 3, "current": 3, "next": 3, "wikipedia": 3, "option": 3, "songfact": 3, "displai": 3, "test": 3, "debian": 3, "12": 3, "bookworm": 3, "howev": 3, "should": 3, "most": 3, "contemporari": 3, "explain": 3, "build": 3, "its": 3, "environ": 3, "automatc": 3, "all": 3, "except": 3, "version": 3, "mariadb": 3, "10": 3, "11": 3, "recent": 3, "suffic": 3, "python": 3, "3": 3, "8": 3, "later": 3, "pleas": 3, "send": 3, "keith": 3, "midnighthax": 3, "edmund": 3, "juli": 3, "2023": 3}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"welcom": 1, "musicmust": [1, 3], "": 1, "document": 1, "indic": 1, "tabl": 1, "content": 1, "develop": 0, "instal": 2, "introduct": 3, "refer": 4, "tutori": 5, "why": 3, "what": 3, "featur": 3, "requir": 3, "feedback": 3, "bug": 3, "etc": 3, "i": 3}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Welcome to MusicMuster\u2019s documentation!": [[1, "welcome-to-musicmuster-s-documentation"]], "Contents": [[1, "contents"]], "Indices and tables": [[1, "indices-and-tables"]], "Development": [[0, "development"]], "Installation": [[2, "installation"]], "Reference": [[4, "reference"]], "Tutorial": [[5, "tutorial"]], "Introduction": [[3, "introduction"]], "Why MusicMuster?": [[3, "why-musicmuster"]], "What is MusicMuster?": [[3, "what-is-musicmuster"]], "Features": [[3, "features"]], "Requirements": [[3, "requirements"]], "Feedback, bugs, etc": [[3, "feedback-bugs-etc"]]}, "indexentries": {}}) \ No newline at end of file +Search.setIndex({"docnames": ["development", "index", "installation", "introduction", "reference", "tutorial"], "filenames": ["development.rst", "index.rst", "installation.rst", "introduction.rst", "reference.rst", "tutorial.rst"], "titles": ["Development", "Welcome to MusicMuster\u2019s documentation!", "Installation", "Introduction", "Reference", "Tutorial"], "terms": {"index": 1, "modul": 1, "search": [1, 3], "page": [1, 3], "i": 1, "music": [1, 3], "player": [1, 3], "target": 1, "product": [1, 3], "live": [1, 3], "internet": [1, 3], "radio": [1, 3], "show": [1, 3], "content": 3, "introduct": 1, "instal": [1, 3], "tutori": 1, "refer": 1, "develop": 1, "why": 1, "what": 1, "featur": 1, "requir": 1, "feedback": 1, "bug": 1, "etc": 1, "In": 3, "januari": 3, "2022": 3, "start": 3, "my": 3, "mixcloud": 3, "http": 3, "www": 3, "com": 3, "keithsmusicbox": 3, "until": 3, "had": 3, "been": 3, "an": 3, "station": 3, "which": 3, "me": 3, "us": 3, "window": 3, "playout": 3, "system": 3, "As": 3, "onli": 3, "linux": 3, "set": 3, "up": 3, "pc": 3, "specif": 3, "purpos": 3, "The": 3, "felt": 3, "were": 3, "shortcom": 3, "variou": 3, "area": 3, "onc": 3, "move": 3, "equival": 3, "didn": 3, "t": 3, "have": 3, "same": 3, "wa": 3, "unabl": 3, "find": 3, "one": 3, "met": 3, "criteria": 3, "decid": 3, "see": 3, "how": 3, "practic": 3, "would": 3, "write": 3, "own": 3, "born": 3, "It": 3, "base": 3, "whilst": 3, "could": 3, "gener": 3, "home": 3, "ar": 3, "much": 3, "better": 3, "applic": 3, "role": 3, "ha": 3, "design": 3, "support": 3, "databas": 3, "back": 3, "can": 3, "almost": 3, "entir": 3, "keyboard": 3, "driven": 3, "playlist": 3, "manag": [], "easili": [], "add": 3, "new": [], "track": 3, "multipl": 3, "tab": 3, "plai": 3, "from": 3, "ani": 3, "note": 3, "comment": 3, "automata": [], "olour": [], "code": 3, "accord": 3, "preview": 3, "befor": 3, "audienc": 3, "time": 3, "dai": 3, "clock": 3, "elaps": 3, "counter": 3, "run": 3, "fade": 3, "silent": 3, "graphic": 3, "volum": 3, "5": 3, "second": 3, "configur": 3, "abil": [], "hide": 3, "button": 3, "drop": 3, "3db": 3, "talkov": 3, "titl": 3, "artist": 3, "length": 3, "mm": 3, "ss": 3, "estim": 3, "end": 3, "when": 3, "last": 3, "bit": 3, "per": 3, "bp": [], "bitrat": 3, "silenc": 3, "record": 3, "total": 3, "arbitrari": 3, "section": 3, "command": 3, "sent": 3, "ob": 3, "studio": 3, "eg": 3, "autom": 3, "scene": 3, "chang": 3, "templat": 3, "select": 3, "unplai": 3, "between": 3, "down": [], "csv": 3, "date": 3, "automat": 3, "current": 3, "next": 3, "wikipedia": 3, "option": 3, "songfact": 3, "displai": 3, "test": 3, "debian": 3, "12": 3, "bookworm": 3, "howev": 3, "should": 3, "most": 3, "contemporari": 3, "explain": 3, "build": 3, "its": 3, "environ": 3, "automatc": 3, "all": 3, "except": 3, "version": 3, "mariadb": 3, "10": 3, "11": 3, "recent": 3, "suffic": 3, "python": 3, "3": 3, "8": 3, "later": 3, "pleas": 3, "send": 3, "keith": 3, "midnighthax": 3, "edmund": 3, "juli": 3, "2023": 3, "open": 3, "automatat": 3, "colour": 3, "lead": 3, "download": 3}, "objects": {}, "objtypes": {}, "objnames": {}, "titleterms": {"welcom": 1, "musicmust": [1, 3], "": 1, "document": 1, "indic": 1, "tabl": 1, "content": 1, "develop": 0, "instal": 2, "introduct": 3, "refer": 4, "tutori": 5, "why": 3, "what": 3, "featur": 3, "requir": 3, "feedback": 3, "bug": 3, "etc": 3, "i": 3}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Welcome to MusicMuster\u2019s documentation!": [[1, "welcome-to-musicmuster-s-documentation"]], "Contents": [[1, "contents"]], "Indices and tables": [[1, "indices-and-tables"]], "Development": [[0, "development"]], "Installation": [[2, "installation"]], "Reference": [[4, "reference"]], "Tutorial": [[5, "tutorial"]], "Introduction": [[3, "introduction"]], "Why MusicMuster?": [[3, "why-musicmuster"]], "What is MusicMuster?": [[3, "what-is-musicmuster"]], "Features": [[3, "features"]], "Requirements": [[3, "requirements"]], "Feedback, bugs, etc": [[3, "feedback-bugs-etc"]]}, "indexentries": {}}) \ No newline at end of file diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index eea2e94..39e088a 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -29,11 +29,10 @@ Features * Database backed * Can be almost entirely keyboard driven -* Easily add new tracks to playlists -* Show multiple playlists on tabs +* Open multiple playlists in tabs * Play tracks from any playlist * Add notes/comments to tracks on playlist -* Automataic olour-coding of notes/comments according to content +* Automatatic colour-coding of notes/comments according to content * Preview tracks before playing to audience * Time of day clock * Elapsed track time counter @@ -41,8 +40,8 @@ Features * Time to run until track is silent * Graphic of volume from 5 seconds (configurable) before fade until track is silent -* Ability to hide played tracks in playlist -* Buttone to drop playout volume by 3dB for talkover +* Optionally hide played tracks in playlist +* Button to drop playout volume by 3dB for talkover * Playlist displays: * Title * Artist @@ -50,18 +49,18 @@ Features * Estimated start time of track * Estimated end time of track * When track was last played - * Bits per second (bps bitrate) of track - * Length of silence in recording before music starts + * Bits per second (bitrate) of track + * Length of leading silence in recording before track starts * Total track length of arbitrary sections of tracks * Commands that are sent to OBS Studio (eg, for automated scene changes) * Playlist templates -* Move selected/unplayed tracks between playlists -* Down CSV of played tracks between arbitrary dates/times +* Move selected or unplayed tracks between playlists +* Download CSV of tracks played between arbitrary dates/times * Search for tracks by title or artist -* Automatic search of current/next track in Wikipedia -* Optional search of selected track in Wikipedia -* Optional search of selected track in Songfacts +* Automatic search of current and next track in Wikipedia +* Optional search of any track in Wikipedia +* Optional search of any track in Songfacts Requirements diff --git a/poetry.lock b/poetry.lock index bbc334e..60cb759 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,14 +14,14 @@ files = [ [[package]] name = "alembic" -version = "1.10.3" +version = "1.11.1" description = "A database migration tool for SQLAlchemy." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "alembic-1.10.3-py3-none-any.whl", hash = "sha256:b2e0a6cfd3a8ce936a1168320bcbe94aefa3f4463cd773a968a55071beb3cd37"}, - {file = "alembic-1.10.3.tar.gz", hash = "sha256:32a69b13a613aeb7e8093f242da60eff9daed13c0df02fff279c1b06c32965d2"}, + {file = "alembic-1.11.1-py3-none-any.whl", hash = "sha256:dc871798a601fab38332e38d6ddb38d5e734f60034baeb8e2db5b642fccd8ab8"}, + {file = "alembic-1.11.1.tar.gz", hash = "sha256:6a810a6b012c88b33458fceb869aef09ac75d6ace5291915ba7fae44de372c01"}, ] [package.dependencies] @@ -105,6 +105,56 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "black" +version = "23.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2023.5.7" @@ -202,6 +252,21 @@ files = [ {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, ] +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -252,14 +317,14 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.1" +version = "1.1.2" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, - {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, ] [package.extras] @@ -282,20 +347,20 @@ tests = ["asttokens", "littleutils", "pytest", "rich"] [[package]] name = "flake8" -version = "6.0.0" +version = "3.9.0" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = ">=3.8.1" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ - {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, - {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, + {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"}, + {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"}, ] [package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.10.0,<2.11.0" -pyflakes = ">=3.0.0,<3.1.0" +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flakehell" @@ -488,14 +553,14 @@ tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < [[package]] name = "ipython" -version = "8.12.0" +version = "8.14.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "ipython-8.12.0-py3-none-any.whl", hash = "sha256:1c183bf61b148b00bcebfa5d9b39312733ae97f6dad90d7e9b4d86c8647f498c"}, - {file = "ipython-8.12.0.tar.gz", hash = "sha256:a950236df04ad75b5bc7f816f9af3d74dc118fd42f2ff7e80e8e60ca1f182e2d"}, + {file = "ipython-8.14.0-py3-none-any.whl", hash = "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf"}, + {file = "ipython-8.14.0.tar.gz", hash = "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1"}, ] [package.dependencies] @@ -649,14 +714,14 @@ testing = ["pytest"] [[package]] name = "markdown-it-py" -version = "2.2.0" +version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] @@ -669,67 +734,67 @@ compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0 linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "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.3" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, - {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] [[package]] @@ -749,14 +814,14 @@ traitlets = "*" [[package]] name = "mccabe" -version = "0.7.0" +version = "0.6.1" description = "McCabe checker, plugin for flake8" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = "*" files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] [[package]] @@ -848,69 +913,66 @@ files = [ [[package]] name = "mysqlclient" -version = "2.1.1" +version = "2.2.0" description = "Python interface to MySQL" category = "main" optional = false -python-versions = ">=3.5" -files = [ - {file = "mysqlclient-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1ed71bd6244993b526113cca3df66428609f90e4652f37eb51c33496d478b37"}, - {file = "mysqlclient-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:c812b67e90082a840efb82a8978369e6e69fc62ce1bda4ca8f3084a9d862308b"}, - {file = "mysqlclient-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c"}, - {file = "mysqlclient-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b355c8b5a7d58f2e909acdbb050858390ee1b0e13672ae759e5e784110022994"}, - {file = "mysqlclient-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:996924f3483fd36a34a5812210c69e71dea5a3d5978d01199b78b7f6d485c855"}, - {file = "mysqlclient-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dea88c8d3f5a5d9293dfe7f087c16dd350ceb175f2f6631c9cf4caf3e19b7a96"}, - {file = "mysqlclient-2.1.1.tar.gz", hash = "sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782"}, -] - -[[package]] -name = "numpy" -version = "1.24.3" -description = "Fundamental package for array computing in Python" -category = "main" -optional = false python-versions = ">=3.8" files = [ - {file = "numpy-1.24.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570"}, - {file = "numpy-1.24.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7"}, - {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463"}, - {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6"}, - {file = "numpy-1.24.3-cp310-cp310-win32.whl", hash = "sha256:f21c442fdd2805e91799fbe044a7b999b8571bb0ab0f7850d0cb9641a687092b"}, - {file = "numpy-1.24.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f23af8c16022663a652d3b25dcdc272ac3f83c3af4c02eb8b824e6b3ab9d7"}, - {file = "numpy-1.24.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a7721ec204d3a237225db3e194c25268faf92e19338a35f3a224469cb6039a3"}, - {file = "numpy-1.24.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6cc757de514c00b24ae8cf5c876af2a7c3df189028d68c0cb4eaa9cd5afc2bf"}, - {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76e3f4e85fc5d4fd311f6e9b794d0c00e7002ec122be271f2019d63376f1d385"}, - {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1d3c026f57ceaad42f8231305d4653d5f05dc6332a730ae5c0bea3513de0950"}, - {file = "numpy-1.24.3-cp311-cp311-win32.whl", hash = "sha256:c91c4afd8abc3908e00a44b2672718905b8611503f7ff87390cc0ac3423fb096"}, - {file = "numpy-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:5342cf6aad47943286afa6f1609cad9b4266a05e7f2ec408e2cf7aea7ff69d80"}, - {file = "numpy-1.24.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7776ea65423ca6a15255ba1872d82d207bd1e09f6d0894ee4a64678dd2204078"}, - {file = "numpy-1.24.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ae8d0be48d1b6ed82588934aaaa179875e7dc4f3d84da18d7eae6eb3f06c242c"}, - {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecde0f8adef7dfdec993fd54b0f78183051b6580f606111a6d789cd14c61ea0c"}, - {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4749e053a29364d3452c034827102ee100986903263e89884922ef01a0a6fd2f"}, - {file = "numpy-1.24.3-cp38-cp38-win32.whl", hash = "sha256:d933fabd8f6a319e8530d0de4fcc2e6a61917e0b0c271fded460032db42a0fe4"}, - {file = "numpy-1.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:56e48aec79ae238f6e4395886b5eaed058abb7231fb3361ddd7bfdf4eed54289"}, - {file = "numpy-1.24.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4719d5aefb5189f50887773699eaf94e7d1e02bf36c1a9d353d9f46703758ca4"}, - {file = "numpy-1.24.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ec87a7084caa559c36e0a2309e4ecb1baa03b687201d0a847c8b0ed476a7187"}, - {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea8282b9bcfe2b5e7d491d0bf7f3e2da29700cec05b49e64d6246923329f2b02"}, - {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210461d87fb02a84ef243cac5e814aad2b7f4be953b32cb53327bb49fd77fbb4"}, - {file = "numpy-1.24.3-cp39-cp39-win32.whl", hash = "sha256:784c6da1a07818491b0ffd63c6bbe5a33deaa0e25a20e1b3ea20cf0e43f8046c"}, - {file = "numpy-1.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:d5036197ecae68d7f491fcdb4df90082b0d4960ca6599ba2659957aafced7c17"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:352ee00c7f8387b44d19f4cada524586f07379c0d49270f87233983bc5087ca0"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d6acc2e7524c9955e5c903160aa4ea083736fde7e91276b0e5d98e6332812"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:35400e6a8d102fd07c71ed7dcadd9eb62ee9a6e84ec159bd48c28235bbb0f8e4"}, - {file = "numpy-1.24.3.tar.gz", hash = "sha256:ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155"}, + {file = "mysqlclient-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:68837b6bb23170acffb43ae411e47533a560b6360c06dac39aa55700972c93b2"}, + {file = "mysqlclient-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5670679ff1be1cc3fef0fa81bf39f0cd70605ba121141050f02743eb878ac114"}, + {file = "mysqlclient-2.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:004fe1d30d2c2ff8072f8ea513bcec235fd9b896f70dad369461d0ad7e570e98"}, + {file = "mysqlclient-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9c6b142836c7dba4f723bf9c93cc46b6e5081d65b2af807f400dda9eb85a16d0"}, + {file = "mysqlclient-2.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:955dba905a7443ce4788c63fdb9f8d688316260cf60b20ff51ac3b1c77616ede"}, + {file = "mysqlclient-2.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:530ece9995a36cadb6211b9787f0c9e05cdab6702549bdb4236af5e9b535ed6a"}, + {file = "mysqlclient-2.2.0.tar.gz", hash = "sha256:04368445f9c487d8abb7a878e3d23e923e6072c04a6c320f9e0dc8a82efba14e"}, +] + +[[package]] +name = "numpy" +version = "1.25.0" +description = "Fundamental package for array computing in Python" +category = "main" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8aa130c3042052d656751df5e81f6d61edff3e289b5994edcf77f54118a8d9f4"}, + {file = "numpy-1.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e3f2b96e3b63c978bc29daaa3700c028fe3f049ea3031b58aa33fe2a5809d24"}, + {file = "numpy-1.25.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6b267f349a99d3908b56645eebf340cb58f01bd1e773b4eea1a905b3f0e4208"}, + {file = "numpy-1.25.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aedd08f15d3045a4e9c648f1e04daca2ab1044256959f1f95aafeeb3d794c16"}, + {file = "numpy-1.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d183b5c58513f74225c376643234c369468e02947b47942eacbb23c1671f25d"}, + {file = "numpy-1.25.0-cp310-cp310-win32.whl", hash = "sha256:d76a84998c51b8b68b40448ddd02bd1081bb33abcdc28beee6cd284fe11036c6"}, + {file = "numpy-1.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0dc071017bc00abb7d7201bac06fa80333c6314477b3d10b52b58fa6a6e38f6"}, + {file = "numpy-1.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c69fe5f05eea336b7a740e114dec995e2f927003c30702d896892403df6dbf0"}, + {file = "numpy-1.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c7211d7920b97aeca7b3773a6783492b5b93baba39e7c36054f6e749fc7490c"}, + {file = "numpy-1.25.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecc68f11404930e9c7ecfc937aa423e1e50158317bf67ca91736a9864eae0232"}, + {file = "numpy-1.25.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e559c6afbca484072a98a51b6fa466aae785cfe89b69e8b856c3191bc8872a82"}, + {file = "numpy-1.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6c284907e37f5e04d2412950960894b143a648dea3f79290757eb878b91acbd1"}, + {file = "numpy-1.25.0-cp311-cp311-win32.whl", hash = "sha256:95367ccd88c07af21b379be1725b5322362bb83679d36691f124a16357390153"}, + {file = "numpy-1.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:b76aa836a952059d70a2788a2d98cb2a533ccd46222558b6970348939e55fc24"}, + {file = "numpy-1.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b792164e539d99d93e4e5e09ae10f8cbe5466de7d759fc155e075237e0c274e4"}, + {file = "numpy-1.25.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7cd981ccc0afe49b9883f14761bb57c964df71124dcd155b0cba2b591f0d64b9"}, + {file = "numpy-1.25.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa48bebfb41f93043a796128854b84407d4df730d3fb6e5dc36402f5cd594c0"}, + {file = "numpy-1.25.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5177310ac2e63d6603f659fadc1e7bab33dd5a8db4e0596df34214eeab0fee3b"}, + {file = "numpy-1.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0ac6edfb35d2a99aaf102b509c8e9319c499ebd4978df4971b94419a116d0790"}, + {file = "numpy-1.25.0-cp39-cp39-win32.whl", hash = "sha256:7412125b4f18aeddca2ecd7219ea2d2708f697943e6f624be41aa5f8a9852cc4"}, + {file = "numpy-1.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:26815c6c8498dc49d81faa76d61078c4f9f0859ce7817919021b9eba72b425e3"}, + {file = "numpy-1.25.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b1b90860bf7d8a8c313b372d4f27343a54f415b20fb69dd601b7efe1029c91e"}, + {file = "numpy-1.25.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85cdae87d8c136fd4da4dad1e48064d700f63e923d5af6c8c782ac0df8044542"}, + {file = "numpy-1.25.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cc3fda2b36482891db1060f00f881c77f9423eead4c3579629940a3e12095fe8"}, + {file = "numpy-1.25.0.tar.gz", hash = "sha256:f1accae9a28dc3cda46a91de86acf69de0d1b5f4edd44a9b0c3ceb8036dfff19"}, ] [[package]] name = "obsws-python" -version = "1.4.2" +version = "1.5.2" description = "A Python SDK for OBS Studio WebSocket v5.0" category = "main" optional = false python-versions = ">=3.9" files = [ - {file = "obsws_python-1.4.2-py3-none-any.whl", hash = "sha256:e33360b02f5c1525ed37d9034defef7faa15d4496648be77f0d44ac877afa044"}, - {file = "obsws_python-1.4.2.tar.gz", hash = "sha256:46834536b5f690004df66e05245353482bb095cddc583c8b089918bd5d0bef1f"}, + {file = "obsws_python-1.5.2-py3-none-any.whl", hash = "sha256:f0985de4fc17d85f8b46d4950f3d0757cad6c7ba362b0d1eea31ad71d5fe9f84"}, + {file = "obsws_python-1.5.2.tar.gz", hash = "sha256:c94775621ae4acab4292a3cee73e198f152775b85f63df569806c61dc8564a19"}, ] [package.dependencies] @@ -922,14 +984,14 @@ dev = ["black", "isort", "pytest", "pytest-randomly"] [[package]] name = "packaging" -version = "23.0" +version = "23.1" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] [[package]] @@ -948,6 +1010,18 @@ files = [ qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["docopt", "pytest (<6.0.0)"] +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + [[package]] name = "pexpect" version = "4.8.0" @@ -975,16 +1049,32 @@ files = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] +[[package]] +name = "platformdirs" +version = "3.8.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.8.0-py3-none-any.whl", hash = "sha256:ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e"}, + {file = "platformdirs-3.8.0.tar.gz", hash = "sha256:b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] + [[package]] name = "pluggy" -version = "1.0.0" +version = "1.2.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] [package.extras] @@ -993,14 +1083,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prompt-toolkit" -version = "3.0.38" +version = "3.0.39" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"}, - {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"}, + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, ] [package.dependencies] @@ -1008,26 +1098,26 @@ wcwidth = "*" [[package]] name = "psutil" -version = "5.9.4" +version = "5.9.5" description = "Cross-platform lib for process and system monitoring in Python." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"}, - {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"}, - {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"}, - {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"}, - {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"}, - {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"}, - {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"}, - {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"}, - {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"}, - {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"}, - {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"}, - {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"}, - {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"}, - {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"}, + {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"}, + {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"}, + {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"}, + {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"}, + {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"}, + {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"}, + {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"}, + {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"}, + {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"}, + {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"}, + {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"}, + {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"}, + {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"}, + {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"}, ] [package.extras] @@ -1083,14 +1173,14 @@ tests = ["pytest"] [[package]] name = "pycodestyle" -version = "2.10.0" +version = "2.7.0" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, - {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] [[package]] @@ -1134,14 +1224,14 @@ rich = "*" [[package]] name = "pyflakes" -version = "3.0.1" +version = "2.3.1" description = "passive checker of Python programs" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, - {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] [[package]] @@ -1158,92 +1248,79 @@ files = [ [[package]] name = "pygame" -version = "2.4.0" +version = "2.5.0" description = "Python Game Development" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "pygame-2.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:63591f381e5b28b90e115ac7a96f8ce5ecb917facb42d79d4f89714f89cc6d8e"}, - {file = "pygame-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:13511c7c29f0fc23636f3b95a96ab45f964e84073e7e27dc602a479cd274d89a"}, - {file = "pygame-2.4.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1b7201a44869eb420dd56c8e003251c9e7422c5304b3e78508e767a5634ab31b"}, - {file = "pygame-2.4.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3ab14e06c302921f33d25180813711a920acef386d3992fc21731d2d5e8e86f0"}, - {file = "pygame-2.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:418659c2d42f6a2356e2691006d79b6e07fd4992f9e904a2638c51c992f3e41b"}, - {file = "pygame-2.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e70fd71e0321a805001192e08ae4af45b86c68f155670230c3f6f4dd25089e70"}, - {file = "pygame-2.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab8115af26a9e95f39b08fff416f097803480f265500b218a5ca065d6e73124f"}, - {file = "pygame-2.4.0-cp310-cp310-win32.whl", hash = "sha256:4ffec9661731fb674ccc88d1de92709219047af3d8198d0e6203c21f3f1b54a7"}, - {file = "pygame-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:bb5a39320d00fa781959d2d0151e6f0293dd1398c6dc9dc934112ecce7b4fb52"}, - {file = "pygame-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:23543e2d206d8de7d6db4f7b1c74e6fea6c01ead63caf7252e63341e1cdb09f6"}, - {file = "pygame-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b562cfdd8caa76ba47ca2a9211fee6b0a95ceb95c9da94cf60a3909b2300854"}, - {file = "pygame-2.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:751bc57e4c3b7cd92762344562dcbd405e2b54488de1d7a1e083a470bdbc5ae9"}, - {file = "pygame-2.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9b1127f085d09c7c0a976d440e8fc2f7adc579d62abcfc20c23c2699bbe2dc1"}, - {file = "pygame-2.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47fd096ceb68d966681f8d0e820f7305bf05b30421ca562cfdb3c89a5aef26e5"}, - {file = "pygame-2.4.0-cp311-cp311-win32.whl", hash = "sha256:de963a4b8787d93a9fba8f4052d9dde8b12adbeac5781e48035be1557dfadb20"}, - {file = "pygame-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:75ef535ebe541b74a160bb59c3e520f543250d19f69d5973350ec1b9706e1469"}, - {file = "pygame-2.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ca2ecc65126eaaa5b8e6a119913cfb2c2b1ed4c8ee1b980baf333aa9d379f227"}, - {file = "pygame-2.4.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a98ed8c47e367b9233b5ca25c36c2b45ab61959ac543195f0b6349f0a599ec8"}, - {file = "pygame-2.4.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00ecd4688ee25d5d4cf48eddab18749a9bb2b382772f7fa8a987e4d21026c603"}, - {file = "pygame-2.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99c296ecb8ce6ea1f404f4d174fdb215c64515827778525301c29ddf6f8e6e07"}, - {file = "pygame-2.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6b94fc99487ce4a45ce00fa9145f4861f6e021254a005101d00bc17a4bb4f5c"}, - {file = "pygame-2.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3020fb98f27a6ea79418a5b332ca07be93884e4a455c8a0a31b2dcf39ee2d96"}, - {file = "pygame-2.4.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8f97c8be81b3262ad8dae982485c4a73c9f2374614dfc0db8eb0f754badb29d6"}, - {file = "pygame-2.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be7f948d33d0536c2922289e6f5983251cb0bd0d727db6ff692886c239f47a2c"}, - {file = "pygame-2.4.0-cp36-cp36m-win32.whl", hash = "sha256:a66b314f4a637784d5ca2970745bb2e6e554447dce8f4cfedd9b9fcef5e3ffc6"}, - {file = "pygame-2.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c09323eeae9e0cb2ced0cb3635485ae17f4f1b2b6b908a494ce2d830c609d4be"}, - {file = "pygame-2.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4241e1da3852a955d37a22157ed51b2d30a65f7987eac9d162bb302fb754d87"}, - {file = "pygame-2.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:802b03f6c367359c94eb6a90169666efa1aa1d6e24fce37a0b21642ccdfe48cf"}, - {file = "pygame-2.4.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:79b0962a8f09788ca735532cfcf2dd053424fe5eabbda564b74f8e5e2eb11f48"}, - {file = "pygame-2.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:627c8bb007a757da18d32c5d9b7ac50ab0356d9e922d570b0572765778080787"}, - {file = "pygame-2.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f576403c2b14f0eea694365b9018d5bacac70b1550261ffc7a54a92e18967541"}, - {file = "pygame-2.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5210cb09ec31281e16fda008bf8dfe2e024eef58e075dd0c01935d0004fdfffd"}, - {file = "pygame-2.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6060d68d10fafd51c4cb3a7d687d741009881860dfd429c863e570877e2ce9de"}, - {file = "pygame-2.4.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6c0081546749c0b4341ce575622a4f8eee05f675d3a0872ab6aed6e5bd2ba5a8"}, - {file = "pygame-2.4.0-cp37-cp37m-win32.whl", hash = "sha256:fa2531f83e7c5f6f7cc20a1b4e0f982bd608aad81ff6c385148e64256ab0419f"}, - {file = "pygame-2.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:cff815181437add5f3d702e8c7f1d2aa4ed04ed04cde27ec809e7ac516ee6b5f"}, - {file = "pygame-2.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53bfdc1aea619fa8d347be37b08de87089d543375948aacf8b163b0c5eb6d4e4"}, - {file = "pygame-2.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7fa1e65fd559823997f39a592cb49d8c89dd31c8bbde8273fe3922e2c1f294f6"}, - {file = "pygame-2.4.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:65ee75e0e83e393fdc5c06e55e376c7511881a5ebee006ecca89cb1b3b41d6f1"}, - {file = "pygame-2.4.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fbcba1b06f42338fecbd366227025f81729d9f4a577677fd3cd1ceff34d7286a"}, - {file = "pygame-2.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04451e5addae3a078a7a4f494e6b217025f4813dfb364a49c250fc5dfd1d2e2"}, - {file = "pygame-2.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faa3b63b71d555e7a21cecc11c65e059d9cb1902158d863ac3592e1947bc521a"}, - {file = "pygame-2.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef14750fa60b47510cfe9c7c37e7abe67617f5d1f1a8ffa257a59d49836dadda"}, - {file = "pygame-2.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5a2aee4214e5efed2cb3650139010dd4d0b1c29a9760278ab259d0b46281b66a"}, - {file = "pygame-2.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:47be83060a9dbc79763fd230f04d53a29064b5f64d1b59425c432d3570b22623"}, - {file = "pygame-2.4.0-cp38-cp38-win32.whl", hash = "sha256:14492d8c0eaad778bb10b6d53eaea4ef77f4d3431b6b7c857397dc6cf4397ac9"}, - {file = "pygame-2.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:6ba967d0e3fed8611f1face6695dc8fa554ee543d300681f8080f5de9cc7da73"}, - {file = "pygame-2.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f6b7a604812f447495829751dfe7ab57cb31c2c9acdb07ba4b7157490411a12"}, - {file = "pygame-2.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e2a3176b33b97ebae397f951d254e3155a0afe730e1b76fb35126555c27dd3b5"}, - {file = "pygame-2.4.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6ec870a63295ebff737640c4ef39868312e206dcba655b4ad5c7d0e8c2488b73"}, - {file = "pygame-2.4.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e75d8c2980d719045be366160568bf508cbbed21285efe32468c75abcd4cf8b3"}, - {file = "pygame-2.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e5d32def075e495b4802371fd8cda96ff4957aa39e215f83d87022dedf14cfb"}, - {file = "pygame-2.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cad74cbbefbdb81cb22a9ea22561614b8dc58fcd52cd54126bbb8ee9ee77a5d5"}, - {file = "pygame-2.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c75dd345707da622c78dbd6a63a025f7b89377ddc4e71ba40043929824f5d4"}, - {file = "pygame-2.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075c1282b1d307669c8ef29942564b91acb85623bedba3bfb841079d539ded31"}, - {file = "pygame-2.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1422673a2b485153cbc20dbbd37af791c9842ca98a1b7a89fe3ac115cce79805"}, - {file = "pygame-2.4.0-cp39-cp39-win32.whl", hash = "sha256:fb7bb86c4aedb4382d7f643ff7d21ab4731d59ddb9b448e78b9125ab1addc007"}, - {file = "pygame-2.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:93bb1406125ae9bd7a9bb0d45f11b30f157ea8d2efee1ebe9d781b1d1a9fce6b"}, - {file = "pygame-2.4.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:2946c151691c80ffb9f3f39e1f294d7ed9edaae1814e528d2f5b4751e7e6d903"}, - {file = "pygame-2.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80502eb26483b0206d0508475ec7d67a86bc0afc5bb4aad3a6172a7a85a27554"}, - {file = "pygame-2.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9c8bb7b77f97eb49dac900445fbf96a332d2072588949d6396581933843fb04"}, - {file = "pygame-2.4.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b6e1493724d29e46a0e7e8125d9808c9957c652db67afe9497d385509fc5ac5"}, - {file = "pygame-2.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43db3a6c9be3d94eececf7c86cde7584d2bb87f394ade40139c3b4e528fdff24"}, - {file = "pygame-2.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32ed4d4317bce8fe78592a7b5b4a07f2e0ff814e35c66cb5a3b398dae96c3f27"}, - {file = "pygame-2.4.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e5f043751840a07ff0160abe46ed42a88bc29baee93656abb5a050beda176306"}, - {file = "pygame-2.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:867cf19f1c7aa6f187a0a31b702f5668e935e700b46d94bd58e94ec8581cf081"}, - {file = "pygame-2.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a93d368311d40827dc5f0cad2a0e9a8700c1b017346808cfdfd9ea98aee45df"}, - {file = "pygame-2.4.0.tar.gz", hash = "sha256:e3603e70e96ee30af1954ce57d4922a059402f368013e7138e90f1c03d185267"}, + {file = "pygame-2.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e34a2b5660acc298d0a66ce16f13a7ca1c56c2a685e40afef3a0cf6eaf3f44b3"}, + {file = "pygame-2.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:875dbde88b899fb7f48d6f0e87f70c3dcc8ee87a947c3df817d949a9741dbcf5"}, + {file = "pygame-2.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:854dc9106210d1a3a83914af53fc234c0bed65a39f5e6098a8eb489da354ad0c"}, + {file = "pygame-2.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e1898db0fd7b868a31c29204813f447c59390350fd806904d80bebde094f3f8"}, + {file = "pygame-2.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d5eac9b9936c7dc2813a750bc8efd53234ad1afc32eb99d6f64bb403c2b9aa"}, + {file = "pygame-2.5.0-cp310-cp310-win32.whl", hash = "sha256:e9eed550b8921080a3c7524202822fc2cf7226e0ffd3c4e4d16510ba44b24e6f"}, + {file = "pygame-2.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:93128beb1154c443f05a66bfbf3f1d4eb8659157ab3b45e4a0454e5905440431"}, + {file = "pygame-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c71d5b3ec232113cbd2e23a19eb01eef1818db41892d0d5efbac3901f940da66"}, + {file = "pygame-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b763062b1996de26a28600e7a8f138d5b36ba0ddd63c65ccbd06124cd95bab70"}, + {file = "pygame-2.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b6a42109f922352c524565fceb22bf8f8b6e4b00d38306e6f5b4c673048f4a"}, + {file = "pygame-2.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bcb19c8ee3fc794ab3a7cb5b5fb1ad38da6866dfbba4af3699a84a828c8a4b9"}, + {file = "pygame-2.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c66b7abc38511c7ca08c5bb58a3bfc14fa51b4e5f85a1786777afc9e584a14dd"}, + {file = "pygame-2.5.0-cp311-cp311-win32.whl", hash = "sha256:46cf1c9b20fb11c7d836c02dd5fc2ca843b699c0e2bc4130cf4ad2f855db5f7f"}, + {file = "pygame-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:f7b77b5019a9a6342535f53c75cef912b218cd24e98505828418f135aacc0a1b"}, + {file = "pygame-2.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ddec0c823fd0869fe4a75ba906dcb7889db0e0c289ce8c03d4ce0a67351ab66"}, + {file = "pygame-2.5.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bae93ce29b8337a5e02507603094c51740c9f496272ef070e2624e9647776568"}, + {file = "pygame-2.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c80a1ad38d11102b4dfa0519aa2a26fea534503b259872609acc9adb1860884e"}, + {file = "pygame-2.5.0-cp312-cp312-win32.whl", hash = "sha256:8ffebcafda0add8072f82999498113be37494694fa36e02155cfaf1a0ba56fe2"}, + {file = "pygame-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:574e310ba708da0c34b71c4158aa7cdca3cf3e16c4100dcd1d3c931a9c6705b4"}, + {file = "pygame-2.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:275e4fab379620c3b262cd58c457eea80001e91bc2e04d306ddb0ba548c969bf"}, + {file = "pygame-2.5.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c116a96a2784bd1724476dbf9c48bfea466ee493a736bdfa04ecbc3f193de0bc"}, + {file = "pygame-2.5.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4a0787ade8723323a3ba874bb725010bb08990a77327fc85f42474f3a840447"}, + {file = "pygame-2.5.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb39b660da1b56a1704ec4aa72bac538030786e23607fb25b8a66f357ffe3a"}, + {file = "pygame-2.5.0-cp36-cp36m-win32.whl", hash = "sha256:d051420667dd9fc8103b3cf994c03e46ee90b1c4a72c174737b8c14729ddf68e"}, + {file = "pygame-2.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6b58356510b7c38836eb81cf08983b58f280da99580d4f17e89ed0ddb707c29c"}, + {file = "pygame-2.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:80f167d8fcec7cd3107f829784ad721b1b7532c19fdf42b3aabbb51f7347850f"}, + {file = "pygame-2.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef96e9a2d8fd9526b89657d192c42dd7c551bfa381fa98ec52d45443e9713818"}, + {file = "pygame-2.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e21d53279fb504b267ae06b565b72d9f95ecbf1f2dd8c705329b287f38295d98"}, + {file = "pygame-2.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147cc0256a5df1316590f351febf6205ef2907564fb0d902935834b91e183486"}, + {file = "pygame-2.5.0-cp37-cp37m-win32.whl", hash = "sha256:e47696154d689180e4eea8c1d6f2bac923986119219db6ad0d2e60ab1f525e8c"}, + {file = "pygame-2.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4c87fa8fa1f3ea94069119accd6d4b5fbf869c2b2954a19b45162dfb3b7c885e"}, + {file = "pygame-2.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:143550078ab10f290cd7c8715a46853e0dc598fd6cdd1561ecb4d6e3116a6b26"}, + {file = "pygame-2.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:969de806bed49b28972862acba652f05ece9420bbdf5f925c970c6a18a9fd1f9"}, + {file = "pygame-2.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d3b0da31ea341b86ef96d6b13c0ddcb25f5320186b7215bc870f08119d2f80"}, + {file = "pygame-2.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23effd50121468f1dc41022230485bff515154191a9d343224850aa3ed3b7f0"}, + {file = "pygame-2.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce4116db6924b544ff8ff03f7ef681c8baf9c6e039a1ec21e26b4ebdaa0e994"}, + {file = "pygame-2.5.0-cp38-cp38-win32.whl", hash = "sha256:50a89c15412506d95e98792435f49a73101788db30ad9c562f611c7aa7b437fa"}, + {file = "pygame-2.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:df1d8affdbe9f417cc7141581e3d08e4b09f708060d3127d2016fd591b2e4f68"}, + {file = "pygame-2.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:99965da24d0bf138d9ac6b7494b9a12781c1510cf936616d1d0c46a736777f6a"}, + {file = "pygame-2.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:daa81057c1beb71a9fb96253457197ad03ee988ba546a166f253bd92a98a9a11"}, + {file = "pygame-2.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ca85da605f6621c99c05f272a5dcf85bf0badcdca45f16ff2bee9a9d41ae042"}, + {file = "pygame-2.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:603d403997d46b07022097861c8b0ff76c6192f8a2f5f89f1a6a978d4411b715"}, + {file = "pygame-2.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7babaeac11544f3e4d7a15756a27f943dc5fff276481fdc9d90415a903ad31a9"}, + {file = "pygame-2.5.0-cp39-cp39-win32.whl", hash = "sha256:9d2126f91699223f0c36845d1c7b2cdfe2f1753ef85b8410ea613e8bd212ca33"}, + {file = "pygame-2.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:8062adc409f0b2742d7996b9b470494025c5e3b73d0d03e3798708dcf5d195cd"}, + {file = "pygame-2.5.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:1bd14adf6151b6ac2f617a8fd71621f1c125209c41a359d3c1cf4bf3904dba5f"}, + {file = "pygame-2.5.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11708f1c7b1671db15246275adcb18cf384f5f7e73532e26999968876c5099"}, + {file = "pygame-2.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6830e431575697f7a11f7731798445242e37eb07ae9007f7be33083f700e9b1e"}, + {file = "pygame-2.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7dd80addfdf7dc1f0e04f81c98acb96580726783172256f2ebc955a967e84124"}, + {file = "pygame-2.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c2cbd9d1a0a3969d6e1c6b0741279c843b4a36ef3804d324874d0a2f0e49816"}, + {file = "pygame-2.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef373b9865c740f18236f2324e17e7f2111e27c6a4a5b67c490c72a8a8b8de77"}, + {file = "pygame-2.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3959038a3e2034cee3f15471786a3bac35baeaa1f7503dc7402bb49d25b5ddbc"}, + {file = "pygame-2.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583d9c8ad033ad51da8485427139d047afb649f49e42d4fa88477f73734ad4b0"}, + {file = "pygame-2.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b650e925d2e8c82c16bdeae6e7fc5d6ca4ac659a1412da4ecd923ef9d688cb"}, + {file = "pygame-2.5.0.tar.gz", hash = "sha256:edd5745b79435976d92c0a7318aedcafcb7ac4567125ac6ba88aa473559ef9ab"}, ] [[package]] name = "pygments" -version = "2.14.0" +version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, - {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, + {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, + {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, ] [package.extras] @@ -1284,47 +1361,47 @@ files = [ [[package]] name = "pyqt5-sip" -version = "12.12.0" +version = "12.12.1" description = "The sip module support for PyQt5" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "PyQt5_sip-12.12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e683dcbdc9e7d36d7ccba82cf20e4835b54bb212470a8467f1d4b620ddeef6a"}, - {file = "PyQt5_sip-12.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7ca01f0508782374ad3621296780fbb31ac3c6f6c8ba1e7051cc4c08354c46a3"}, - {file = "PyQt5_sip-12.12.0-cp310-cp310-win32.whl", hash = "sha256:7741a57bf7980ef16ee975ea9790f95616d532053e4a4f16bf35449333cbcc5b"}, - {file = "PyQt5_sip-12.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:660416500991c4eaf20730c2ec4897cd75b476fae9c80fe09fa0f98750693516"}, - {file = "PyQt5_sip-12.12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e77e683916f44b82f880155e0c13566ad285c5708c53287d67b4c2971c4579a3"}, - {file = "PyQt5_sip-12.12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e19fcd0536cb74eda5dfab24490cb20966d371069a95f81f56cb9de7c18c2bee"}, - {file = "PyQt5_sip-12.12.0-cp311-cp311-win32.whl", hash = "sha256:da0c653c3a9843e0bb3bdd43c773c07458a0ae78bd056ebcad0b59ce1aa91e1c"}, - {file = "PyQt5_sip-12.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:56d08c822a8b4f0950285d1b316d81c3b80bf3ba8d98efc035a205051f03a05d"}, - {file = "PyQt5_sip-12.12.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d763dde46d5754c44aed1fbd9ef030c0850b8b341834b4d274d64f8fb1b25a05"}, - {file = "PyQt5_sip-12.12.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21639b03429baae810119239592762da9a30fa0fe3b1c2e26f456cf1a77a3cc0"}, - {file = "PyQt5_sip-12.12.0-cp37-cp37m-win32.whl", hash = "sha256:d33ec957b9c104b56756cc10f82e544d41d64d0e2048c95ba1b64ef6d1ad55dc"}, - {file = "PyQt5_sip-12.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1b0203381b895097c41cd1ca6bba7d88fd0d2fa8c3dde6d55c1bb95141080f35"}, - {file = "PyQt5_sip-12.12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:96ec94a7239f83e30b4c37a7f89c75df3504918a372d968e773532b5fbc7d268"}, - {file = "PyQt5_sip-12.12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6e4becfdbbe1da70887cd6baa0ef1ec394d81a39522e54a3118d1757e2fd0e06"}, - {file = "PyQt5_sip-12.12.0-cp38-cp38-win32.whl", hash = "sha256:bc0df0e4a95e2dc394009cf473098deb318ba6c5208390acc762889a253ef802"}, - {file = "PyQt5_sip-12.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:d26078f436952d774c51599b90d5aa4b9533406f7d65e0d80931a87a24268836"}, - {file = "PyQt5_sip-12.12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d66df3702adf375ec8d4d69ec2a80f57ea899180b1db62188c6313a2f81085da"}, - {file = "PyQt5_sip-12.12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:382a85ae7218de9b5ed136d0d865b49c09063e1ca428ed6901b1a5860223b444"}, - {file = "PyQt5_sip-12.12.0-cp39-cp39-win32.whl", hash = "sha256:a04933dacbba804623c5861d214e217e7e3454beab84566ba5e02c86fb86eded"}, - {file = "PyQt5_sip-12.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:1e91834cc98fda25c232666ca2e77b14520b47b1cee8d38bc93dbe0cd951443e"}, - {file = "PyQt5_sip-12.12.0.tar.gz", hash = "sha256:01bc2d443325505b07d35e4a70d239b4f97b77486e29117fb67f927a15cd8061"}, + {file = "PyQt5_sip-12.12.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ed598ff1b666f9e5e0214be7840f308f8fb347fe416a2a45fbedab046a7120b"}, + {file = "PyQt5_sip-12.12.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:644310dcfed4373075bc576717eea60b0f49899beb5cffb204ddaf5f27cddb85"}, + {file = "PyQt5_sip-12.12.1-cp310-cp310-win32.whl", hash = "sha256:51720277a53d99bac0914fb970970c9c2ec1a6ab3b7cc5580909d37d9cc6b152"}, + {file = "PyQt5_sip-12.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:2a344855b9c57d37cf000afa46967961122fb1867faee4f53054ebaa1ce51e24"}, + {file = "PyQt5_sip-12.12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:59229c8d30141220a472ba4e4212846e8bf0bed84c32cbeb57f70fe727c6dfc2"}, + {file = "PyQt5_sip-12.12.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5a9cbcfe8c15d3a34ef33570f0b0130b8ba68b98fd6ec92c28202b186f3ab870"}, + {file = "PyQt5_sip-12.12.1-cp311-cp311-win32.whl", hash = "sha256:7afc6ec06e79a3e0a7b447e28ef46dad372bdca32e7eff0dcbac6bc53b69a070"}, + {file = "PyQt5_sip-12.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:b5b7a6c76fe3eb6b245ac6599c807b18e9a718167878a0b547db1d071a914c08"}, + {file = "PyQt5_sip-12.12.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3dad0b2bbe0bae4916e43610186d425cd186469b2e6c7ff853177c113b6af6ed"}, + {file = "PyQt5_sip-12.12.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d1378815b15198ce6dddd367fbd81f5c018ce473a89ae938b7a58e1d97f25b10"}, + {file = "PyQt5_sip-12.12.1-cp37-cp37m-win32.whl", hash = "sha256:2e3d444f5cb81261c22e7c9d3a9a4484bb9db7a1a3077559100175d36297d1da"}, + {file = "PyQt5_sip-12.12.1-cp37-cp37m-win_amd64.whl", hash = "sha256:eee684532876775e1d0fa20d4aae1b568aaa6c732d74e6657ee832e427d46947"}, + {file = "PyQt5_sip-12.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1364f460ae07fc2f4c42dd7a3b3738611b29f5c033025e5e70b03e2687d4bda4"}, + {file = "PyQt5_sip-12.12.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6cb6139b00e347e7d961467d092e67c47a97893bc6ab83104bcaf50bf4815036"}, + {file = "PyQt5_sip-12.12.1-cp38-cp38-win32.whl", hash = "sha256:9e21e11eb6fb468affe0d72ff922788c2adc124480bb274941fce93ddb122b8f"}, + {file = "PyQt5_sip-12.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:f5ac060219c127a5b9009a4cfe33086e36c6bb8e26c0b757b31a6c04d29d630d"}, + {file = "PyQt5_sip-12.12.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a65f5869f3f35330c920c1b218319140c0b84f8c49a20727b5e3df2acd496833"}, + {file = "PyQt5_sip-12.12.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e0241b62f5ca9aaff1037f12e6f5ed68168e7200e14e73f05b632381cee0ff4b"}, + {file = "PyQt5_sip-12.12.1-cp39-cp39-win32.whl", hash = "sha256:0e30f6c9b99161d8a524d8b7aa5a001be5fe002797151c27414066b838beaa4e"}, + {file = "PyQt5_sip-12.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:8a2e48a331024a225128f94f5d0fb8089e924419693b2e03eda4d5dbc4313b52"}, + {file = "PyQt5_sip-12.12.1.tar.gz", hash = "sha256:8fdc6e0148abd12d977a1d3828e7b79aae958e83c6cb5adae614916d888a6b10"}, ] [[package]] name = "pyqt6" -version = "6.5.0" +version = "6.5.1" description = "Python bindings for the Qt cross platform application toolkit" category = "main" optional = false python-versions = ">=3.6.1" files = [ - {file = "PyQt6-6.5.0-cp37-abi3-macosx_10_14_universal2.whl", hash = "sha256:e3c8289d9a509be897265981b77eb29e64ce29e9d221fdf52545c2c95e819c9b"}, - {file = "PyQt6-6.5.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b0d9628134811fbfc988d1757111ca8e25cb697f136fa54c969fb1a4d4a61d1"}, - {file = "PyQt6-6.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:99ea0e68f548509b7ef97cded0feeaf3dca7d1fe719388569407326be3be38c2"}, - {file = "PyQt6-6.5.0.tar.gz", hash = "sha256:b97cb4be9b2c8997904ea668cf3b0a4ae5822196f7792590d05ecde6216a9fbc"}, + {file = "PyQt6-6.5.1-cp37-abi3-macosx_10_14_universal2.whl", hash = "sha256:ad91dcb34d4a70add6551745df631b36013e1c50349cf9f1883cd08913e8cd7e"}, + {file = "PyQt6-6.5.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b8d29f671cba9cfecd9cf6246cd43d10f7d32125b2b8958ad671fbdd1862e097"}, + {file = "PyQt6-6.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:d136fbf8cf18cafd4e45a55adfd114a29665dcf91f45bb0e82eec1789eecabbb"}, + {file = "PyQt6-6.5.1.tar.gz", hash = "sha256:e166a0568c27bcc8db00271a5043936226690b6a4a74ce0a5caeb408040a97c3"}, ] [package.dependencies] @@ -1333,42 +1410,42 @@ PyQt6-sip = ">=13.4,<14" [[package]] name = "pyqt6-qt6" -version = "6.5.0" +version = "6.5.1" description = "The subset of a Qt installation needed by PyQt6." category = "main" optional = false python-versions = "*" files = [ - {file = "PyQt6_Qt6-6.5.0-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:1178fcd5e9590fec4261e06a753a8fa028222ec0bd9a0788b3bd37720fbbe6cf"}, - {file = "PyQt6_Qt6-6.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9d82d8af986a0eef55905f309fdda4303d1354eba10175824ae62ab6547f7a96"}, - {file = "PyQt6_Qt6-6.5.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:5f40ef19eb632731828283361f800928517650c74c914c093af9a364d6843953"}, - {file = "PyQt6_Qt6-6.5.0-py3-none-win_amd64.whl", hash = "sha256:8c1f898f4d02a31615fe7613a38f82b489fb2c8554965c917d551470731635a8"}, + {file = "PyQt6_Qt6-6.5.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:6aa88aaf4e4e9236364d5b4da8ffdf2854dc579966f135e85e34a54a91ec8096"}, + {file = "PyQt6_Qt6-6.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dfc06f8da1728fa2ee2a9407d85704e2661415536190ec632dd8f657acc53842"}, + {file = "PyQt6_Qt6-6.5.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:7b04daf0e6ca1e79bf8d3e611392be06abc1687879e3475fcbad0d67672d3370"}, + {file = "PyQt6_Qt6-6.5.1-py3-none-win_amd64.whl", hash = "sha256:fc4de9c51bfa3e8eaef03ecd4e6346f42a3accf69555987d66dd1236f14e8225"}, ] [[package]] name = "pyqt6-sip" -version = "13.5.0" +version = "13.5.1" description = "The sip module support for PyQt6" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "PyQt6_sip-13.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:447c0df1c8796d2dbb9e5c1cef2ba2a59a38a2bce2fa438246c096a52530f331"}, - {file = "PyQt6_sip-13.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cd56a17e51bc84203219023e956ac42ba8aa4195adb1126476f0cb751a22e986"}, - {file = "PyQt6_sip-13.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:c69072f4afc8e75799d3166f5d3b405eaa7bba998f61e3c8f0dd3a78a234015c"}, - {file = "PyQt6_sip-13.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fed31d93b2ee8115621f2aeb686068ad1b75084df6af5262c4a1818064014d6"}, - {file = "PyQt6_sip-13.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ee6a198346f1d9e2b675232b6d19d1517652594d7fdc72bb32d0bced6cb2e08d"}, - {file = "PyQt6_sip-13.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a12a24ca84c482a8baa07081f73e11cee17c0c9220021319eada087d2ea8267"}, - {file = "PyQt6_sip-13.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:34e9d5a6f2d77fd7829ce93f59406193547dc77316b63a979bf8de84bb2d7d97"}, - {file = "PyQt6_sip-13.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1ffb48367e0a8bcfe6142c039a433905d606785f7085c3dff3f7801f0afd9fec"}, - {file = "PyQt6_sip-13.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:42e802b99293eff99061115b122801574682b950c2f01e68ac14162f35239bce"}, - {file = "PyQt6_sip-13.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d79d1c557d35747feef11e943723d9a662a819070fedf96e85920bfd5ad48d1"}, - {file = "PyQt6_sip-13.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6e72061953b0bd07d6b41c710bb654788ca61a8f336d169b59c96fd15fdf681a"}, - {file = "PyQt6_sip-13.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:640011d5642ee94dce6cfde234631830ca7164bef138772c4ad05b80dcb88e10"}, - {file = "PyQt6_sip-13.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2cbc73dd3a2e0d6669b47fbf0ed5494a3cda996a2d0db465eea2a825a0c12733"}, - {file = "PyQt6_sip-13.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:289c37bf808ecc110c6f85afe29083f90170dbdfb76db412281acabefc0b7ede"}, - {file = "PyQt6_sip-13.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:bf705dbbf77029c682234cdaa78970899d9d49b40b7b2d942b2af4f0f6c6d566"}, - {file = "PyQt6_sip-13.5.0.tar.gz", hash = "sha256:61c702b7f81796a27c294ba76f1cba3408161f06deb801373c42670ed36f722a"}, + {file = "PyQt6_sip-13.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2a15f080a994936ed182f4a81343baa19bac9063ec6efc0a93d026f5cfc95ace"}, + {file = "PyQt6_sip-13.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d90dfec64c35c91644a3e32d3e5680cdee549d00245ea7252cb6298797f9bcef"}, + {file = "PyQt6_sip-13.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:1e7bb3e45e57dfdb9437043d99b7cb797707e7f2475d122928b13688458f94b7"}, + {file = "PyQt6_sip-13.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18c0e75e6ebd91dc96dbc6290f044ec37e764890ef2182c82b99ea5b655ea466"}, + {file = "PyQt6_sip-13.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c3e2f155f92f96d73c680caf3d87f4f9f9aaf6487c125ecbe7140daad7d87245"}, + {file = "PyQt6_sip-13.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:08b69898aab0fcc73661b212d434f9e9eb50319481bc2ac3aaf1ac06bc9feca6"}, + {file = "PyQt6_sip-13.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d79957dd0c0ea1a17f0846806ea203dce827df6a9dcd93ebfe98fdd6186d9ecc"}, + {file = "PyQt6_sip-13.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a867601c38acc9b0c7f9aab4f96d9ec8cbedfcd5ae245f82a9c1c48f352413e4"}, + {file = "PyQt6_sip-13.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:52931307cf06c5ac992df2877e899f8b8ba72464e2828fe442b18fd51c7bf787"}, + {file = "PyQt6_sip-13.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c71d303ff654ad947d8c0cb5ebfde9a59390aac52eb695a775234a08bee8f44e"}, + {file = "PyQt6_sip-13.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e0d715bb5b86eb8f09d84b2b4400df7e4c96ef730801bc145a1c23be79f39fac"}, + {file = "PyQt6_sip-13.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:4b2e70d21069fe6e20bf22de1de2985e064e00d1368e0a171ce38824be4339ab"}, + {file = "PyQt6_sip-13.5.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7b4e3cad598afd9b50a32732007184141b400769d425cae86f4e702cbc882b3"}, + {file = "PyQt6_sip-13.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ad802005e68bb9bb6f869b7f904c73d7c7793d11b83d317c33ff6b0c163d785f"}, + {file = "PyQt6_sip-13.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:5e8fdb0821c0c556d2a34db1229d4bd711499a1102241b1b9fcf1ee34f87e564"}, + {file = "PyQt6_sip-13.5.1.tar.gz", hash = "sha256:d1e9141752966669576d04b37ba0b122abbc41cc9c35493751028d7d91c4dd49"}, ] [[package]] @@ -1392,16 +1469,16 @@ PyQt6-WebEngine-Qt6 = ">=6.5.0" [[package]] name = "pyqt6-webengine-qt6" -version = "6.5.0" +version = "6.5.1" description = "The subset of a Qt installation needed by PyQt6-WebEngine." category = "main" optional = false python-versions = "*" files = [ - {file = "PyQt6_WebEngine_Qt6-6.5.0-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:8d7eee4e864c89d6865ff95394dec3aa5b6620ac20412d09a313e83a5baaecb5"}, - {file = "PyQt6_WebEngine_Qt6-6.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ee2300d372cf38bfb2e426e5036f58bfcaf121e460dc7f89913dc7bd6c3c8953"}, - {file = "PyQt6_WebEngine_Qt6-6.5.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:6f2be9044060ed3e9e0c55e0d8863fae08c815e994bcf17f2ff24945a2264ff7"}, - {file = "PyQt6_WebEngine_Qt6-6.5.0-py3-none-win_amd64.whl", hash = "sha256:5acadcc6608df8d9eba385e04ced2fc88e7eb92e366556ee4ac3c57a02c00088"}, + {file = "PyQt6_WebEngine_Qt6-6.5.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:4d6e4e5b5e4301c1ee86fb3070469f7052325c6ea2ad83987e26d4864851cd9c"}, + {file = "PyQt6_WebEngine_Qt6-6.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5620e422d26d04df3fe919d069174b769f5d240df44b9a1f19edb1ef04be9f4c"}, + {file = "PyQt6_WebEngine_Qt6-6.5.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:7cf9cc03819486e997b4e5ddff645394f9a85d62efe114ee047edd3958670727"}, + {file = "PyQt6_WebEngine_Qt6-6.5.1-py3-none-win_amd64.whl", hash = "sha256:1ea8bc467a3ada7fe85fcd45318ee79796caefb8f61264ae755c9e771e5a02b9"}, ] [[package]] @@ -1455,14 +1532,14 @@ files = [ [[package]] name = "pytest" -version = "7.3.0" +version = "7.4.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.0-py3-none-any.whl", hash = "sha256:933051fa1bfbd38a21e73c3960cebdad4cf59483ddba7696c48509727e17f201"}, - {file = "pytest-7.3.0.tar.gz", hash = "sha256:58ecc27ebf0ea643ebfdf7fb1249335da761a00c9f955bcd922349bcb68ee57d"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] [package.dependencies] @@ -1474,7 +1551,7 @@ pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-qt" @@ -1529,14 +1606,14 @@ unidecode = ["Unidecode (>=1.1.1)"] [[package]] name = "python-vlc" -version = "3.0.18121" +version = "3.0.18122" description = "VLC bindings for python." category = "main" optional = false python-versions = "*" files = [ - {file = "python-vlc-3.0.18121.tar.gz", hash = "sha256:24550314a3e6ed55fd347b009491c98b865f9dfa05a92e889d7b0a2210e7485b"}, - {file = "python_vlc-3.0.18121-py3-none-any.whl", hash = "sha256:b8f4bdea22d363377c51996db94cac38d02df02fee9b79c03f1840ff6d376455"}, + {file = "python-vlc-3.0.18122.tar.gz", hash = "sha256:1039bde287853b4b7b61ba22d83761832434f78506da762dfb060291bf32897d"}, + {file = "python_vlc-3.0.18122-py3-none-any.whl", hash = "sha256:00e5133886a956ded7362b48926818e5486dfd73b2f9319b76d977acefe6042c"}, ] [[package]] @@ -1563,18 +1640,18 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.3.3" +version = "13.4.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.3.3-py3-none-any.whl", hash = "sha256:540c7d6d26a1178e8e8b37e9ba44573a3cd1464ff6348b99ee7061b95d1c6333"}, - {file = "rich-13.3.3.tar.gz", hash = "sha256:dc84400a9d842b3a9c5ff74addd8eb798d155f36c1c91303888e0a66850d2a15"}, + {file = "rich-13.4.2-py3-none-any.whl", hash = "sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec"}, + {file = "rich-13.4.2.tar.gz", hash = "sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898"}, ] [package.dependencies] -markdown-it-py = ">=2.2.0,<3.0.0" +markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" [package.extras] @@ -1582,19 +1659,19 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "67.6.1" +version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"}, - {file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"}, + {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, + {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -1784,53 +1861,47 @@ test = ["pytest"] [[package]] name = "sqlalchemy" -version = "1.4.47" +version = "1.4.49" description = "Database Abstraction Library" category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "SQLAlchemy-1.4.47-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:dcfb480bfc9e1fab726003ae00a6bfc67a29bad275b63a4e36d17fe7f13a624e"}, - {file = "SQLAlchemy-1.4.47-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:28fda5a69d6182589892422c5a9b02a8fd1125787aab1d83f1392aa955bf8d0a"}, - {file = "SQLAlchemy-1.4.47-cp27-cp27m-win32.whl", hash = "sha256:45e799c1a41822eba6bee4e59b0e38764e1a1ee69873ab2889079865e9ea0e23"}, - {file = "SQLAlchemy-1.4.47-cp27-cp27m-win_amd64.whl", hash = "sha256:10edbb92a9ef611f01b086e271a9f6c1c3e5157c3b0c5ff62310fb2187acbd4a"}, - {file = "SQLAlchemy-1.4.47-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7a4df53472c9030a8ddb1cce517757ba38a7a25699bbcabd57dcc8a5d53f324e"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:511d4abc823152dec49461209607bbfb2df60033c8c88a3f7c93293b8ecbb13d"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbe57f39f531c5d68d5594ea4613daa60aba33bb51a8cc42f96f17bbd6305e8d"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca8ab6748e3ec66afccd8b23ec2f92787a58d5353ce9624dccd770427ee67c82"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299b5c5c060b9fbe51808d0d40d8475f7b3873317640b9b7617c7f988cf59fda"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-win32.whl", hash = "sha256:684e5c773222781775c7f77231f412633d8af22493bf35b7fa1029fdf8066d10"}, - {file = "SQLAlchemy-1.4.47-cp310-cp310-win_amd64.whl", hash = "sha256:2bba39b12b879c7b35cde18b6e14119c5f1a16bd064a48dd2ac62d21366a5e17"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:795b5b9db573d3ed61fae74285d57d396829e3157642794d3a8f72ec2a5c719b"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:989c62b96596b7938cbc032e39431e6c2d81b635034571d6a43a13920852fb65"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b67bda733da1dcdccaf354e71ef01b46db483a4f6236450d3f9a61efdba35a"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-win32.whl", hash = "sha256:9a198f690ac12a3a807e03a5a45df6a30cd215935f237a46f4248faed62e69c8"}, - {file = "SQLAlchemy-1.4.47-cp311-cp311-win_amd64.whl", hash = "sha256:03be6f3cb66e69fb3a09b5ea89d77e4bc942f3bf84b207dba84666a26799c166"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:16ee6fea316790980779268da47a9260d5dd665c96f225d28e7750b0bb2e2a04"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:557675e0befafa08d36d7a9284e8761c97490a248474d778373fb96b0d7fd8de"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb2797fee8a7914fb2c3dc7de404d3f96eb77f20fc60e9ee38dc6b0ca720f2c2"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28297aa29e035f29cba6b16aacd3680fbc6a9db682258d5f2e7b49ec215dbe40"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-win32.whl", hash = "sha256:998e782c8d9fd57fa8704d149ccd52acf03db30d7dd76f467fd21c1c21b414fa"}, - {file = "SQLAlchemy-1.4.47-cp36-cp36m-win_amd64.whl", hash = "sha256:dde4d02213f1deb49eaaf8be8a6425948963a7af84983b3f22772c63826944de"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e98ef1babe34f37f443b7211cd3ee004d9577a19766e2dbacf62fce73c76245a"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14a3879853208a242b5913f3a17c6ac0eae9dc210ff99c8f10b19d4a1ed8ed9b"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7120a2f72599d4fed7c001fa1cbbc5b4d14929436135768050e284f53e9fbe5e"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:048509d7f3ac27b83ad82fd96a1ab90a34c8e906e4e09c8d677fc531d12c23c5"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-win32.whl", hash = "sha256:6572d7c96c2e3e126d0bb27bfb1d7e2a195b68d951fcc64c146b94f088e5421a"}, - {file = "SQLAlchemy-1.4.47-cp37-cp37m-win_amd64.whl", hash = "sha256:a6c3929df5eeaf3867724003d5c19fed3f0c290f3edc7911616616684f200ecf"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:71d4bf7768169c4502f6c2b0709a02a33703544f611810fb0c75406a9c576ee1"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd45c60cc4f6d68c30d5179e2c2c8098f7112983532897566bb69c47d87127d3"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fdbb8e9d4e9003f332a93d6a37bca48ba8095086c97a89826a136d8eddfc455"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f216a51451a0a0466e082e163591f6dcb2f9ec182adb3f1f4b1fd3688c7582c"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-win32.whl", hash = "sha256:bd988b3362d7e586ef581eb14771bbb48793a4edb6fcf62da75d3f0f3447060b"}, - {file = "SQLAlchemy-1.4.47-cp38-cp38-win_amd64.whl", hash = "sha256:32ab09f2863e3de51529aa84ff0e4fe89a2cb1bfbc11e225b6dbc60814e44c94"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:07764b240645627bc3e82596435bd1a1884646bfc0721642d24c26b12f1df194"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e2a42017984099ef6f56438a6b898ce0538f6fadddaa902870c5aa3e1d82583"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6b6d807c76c20b4bc143a49ad47782228a2ac98bdcdcb069da54280e138847fc"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a94632ba26a666e7be0a7d7cc3f7acab622a04259a3aa0ee50ff6d44ba9df0d"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-win32.whl", hash = "sha256:f80915681ea9001f19b65aee715115f2ad310730c8043127cf3e19b3009892dd"}, - {file = "SQLAlchemy-1.4.47-cp39-cp39-win_amd64.whl", hash = "sha256:fc700b862e0a859a37faf85367e205e7acaecae5a098794aff52fdd8aea77b12"}, - {file = "SQLAlchemy-1.4.47.tar.gz", hash = "sha256:95fc02f7fc1f3199aaa47a8a757437134cf618e9d994c84effd53f530c38586f"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:95b9df9afd680b7a3b13b38adf6e3a38995da5e162cc7524ef08e3be4e5ed3e1"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f835c050ebaa4e48b18403bed2c0fda986525896efd76c245bdd4db995e51a4c"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c21b172dfb22e0db303ff6419451f0cac891d2e911bb9fbf8003d717f1bcf91"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win32.whl", hash = "sha256:5fb1ebdfc8373b5a291485757bd6431de8d7ed42c27439f543c81f6c8febd729"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:f8a65990c9c490f4651b5c02abccc9f113a7f56fa482031ac8cb88b70bc8ccaa"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5debe7d49b8acf1f3035317e63d9ec8d5e4d904c6e75a2a9246a119f5f2fdf3d"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win32.whl", hash = "sha256:82b08e82da3756765c2e75f327b9bf6b0f043c9c3925fb95fb51e1567fa4ee87"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win_amd64.whl", hash = "sha256:171e04eeb5d1c0d96a544caf982621a1711d078dbc5c96f11d6469169bd003f1"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:36e58f8c4fe43984384e3fbe6341ac99b6b4e083de2fe838f0fdb91cebe9e9cb"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b31e67ff419013f99ad6f8fc73ee19ea31585e1e9fe773744c0f3ce58c039c30"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c14b29d9e1529f99efd550cd04dbb6db6ba5d690abb96d52de2bff4ed518bc95"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f3470e084d31247aea228aa1c39bbc0904c2b9ccbf5d3cfa2ea2dac06f26d"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win32.whl", hash = "sha256:706bfa02157b97c136547c406f263e4c6274a7b061b3eb9742915dd774bbc264"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win_amd64.whl", hash = "sha256:a7f7b5c07ae5c0cfd24c2db86071fb2a3d947da7bd487e359cc91e67ac1c6d2e"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:4afbbf5ef41ac18e02c8dc1f86c04b22b7a2125f2a030e25bbb4aff31abb224b"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:201de072b818f8ad55c80d18d1a788729cccf9be6d9dc3b9d8613b053cd4836d"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653ed6817c710d0c95558232aba799307d14ae084cc9b1f4c389157ec50df5c"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win32.whl", hash = "sha256:647e0b309cb4512b1f1b78471fdaf72921b6fa6e750b9f891e09c6e2f0e5326f"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win_amd64.whl", hash = "sha256:ab73ed1a05ff539afc4a7f8cf371764cdf79768ecb7d2ec691e3ff89abbc541e"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:37ce517c011560d68f1ffb28af65d7e06f873f191eb3a73af5671e9c3fada08a"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1878ce508edea4a879015ab5215546c444233881301e97ca16fe251e89f1c55"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e8e608983e6f85d0852ca61f97e521b62e67969e6e640fe6c6b575d4db68557"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccf956da45290df6e809ea12c54c02ace7f8ff4d765d6d3dfb3655ee876ce58d"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win32.whl", hash = "sha256:f167c8175ab908ce48bd6550679cc6ea20ae169379e73c7720a28f89e53aa532"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win_amd64.whl", hash = "sha256:45806315aae81a0c202752558f0df52b42d11dd7ba0097bf71e253b4215f34f4"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b6d0c4b15d65087738a6e22e0ff461b407533ff65a73b818089efc8eb2b3e1de"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a843e34abfd4c797018fd8d00ffffa99fd5184c421f190b6ca99def4087689bd"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c890421651b45a681181301b3497e4d57c0d01dc001e10438a40e9a9c25ee77"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d26f280b8f0a8f497bc10573849ad6dc62e671d2468826e5c748d04ed9e670d5"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win32.whl", hash = "sha256:ec2268de67f73b43320383947e74700e95c6770d0c68c4e615e9897e46296294"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win_amd64.whl", hash = "sha256:bbdf16372859b8ed3f4d05f925a984771cd2abd18bd187042f24be4886c2a15f"}, + {file = "SQLAlchemy-1.4.49.tar.gz", hash = "sha256:06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9"}, ] [package.dependencies] @@ -1859,14 +1930,14 @@ sqlcipher = ["sqlcipher3-binary"] [[package]] name = "sqlalchemy2-stubs" -version = "0.0.2a33" +version = "0.0.2a34" description = "Typing Stubs for SQLAlchemy 1.4" category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "sqlalchemy2-stubs-0.0.2a33.tar.gz", hash = "sha256:5a35a096964dfd985651662b7f175fe1ddcbf5ed4f2d0203e637cec38bed64b4"}, - {file = "sqlalchemy2_stubs-0.0.2a33-py3-none-any.whl", hash = "sha256:9809e7d8ea72cd92ac35aca4b43f588ae24b20376c55a0ef0112a08a6b537180"}, + {file = "sqlalchemy2-stubs-0.0.2a34.tar.gz", hash = "sha256:2432137ab2fde1a608df4544f6712427b0b7ff25990cfbbc5a9d1db6c8c6f489"}, + {file = "sqlalchemy2_stubs-0.0.2a34-py3-none-any.whl", hash = "sha256:a313220ac793404349899faf1272e821a62dbe1d3a029bd444faa8d3e966cd07"}, ] [package.dependencies] @@ -1933,13 +2004,13 @@ speedup = ["python-levenshtein (>=0.12)"] [[package]] name = "tinytag" -version = "1.8.1" +version = "1.9.0" description = "Read music meta data and length of MP3, OGG, OPUS, MP4, M4A, FLAC, WMA and Wave files" category = "main" optional = false python-versions = ">=2.7" files = [ - {file = "tinytag-1.8.1.tar.gz", hash = "sha256:363ab3107831a5598b68aaa061aba915fb1c7b4254d770232e65d5db8487636d"}, + {file = "tinytag-1.9.0.tar.gz", hash = "sha256:f8d71110e1e680a33d99202e00a5a698481d25d20173b81ba3e863423979e014"}, ] [package.extras] @@ -1987,44 +2058,45 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] [[package]] name = "types-psutil" -version = "5.9.5.11" +version = "5.9.5.15" description = "Typing stubs for psutil" category = "main" optional = false python-versions = "*" files = [ - {file = "types-psutil-5.9.5.11.tar.gz", hash = "sha256:3d59da0758f056bfb59fef757366e538c5dd5473d81c35b38956624ae2484f31"}, - {file = "types_psutil-5.9.5.11-py3-none-any.whl", hash = "sha256:01cc541b187a11e758d336c4cc89abf71d0098627fa95d5cfaca536be31a7d1a"}, + {file = "types-psutil-5.9.5.15.tar.gz", hash = "sha256:943a5f9556c1995097c89f50274330dd8d71d17bd73cbc6b09c61dbf4e114cc6"}, + {file = "types_psutil-5.9.5.15-py3-none-any.whl", hash = "sha256:7f183072d1fdb4e092fd6dd88ea017b957056bf10e58eafcdca26bf4372c0742"}, ] [[package]] name = "typing-extensions" -version = "4.5.0" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] [[package]] name = "urllib3" -version = "1.26.15" +version = "2.0.3" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7" files = [ - {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, - {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, + {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"}, + {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "urwid" @@ -2068,14 +2140,14 @@ files = [ [[package]] name = "websocket-client" -version = "1.5.1" +version = "1.6.1" description = "WebSocket client for Python with low level API options" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "websocket-client-1.5.1.tar.gz", hash = "sha256:3f09e6d8230892547132177f575a4e3e73cfdf06526e20cc02aa1c3b47184d40"}, - {file = "websocket_client-1.5.1-py3-none-any.whl", hash = "sha256:cdf5877568b7e83aa7cf2244ab56a3213de587bbe0ce9d8b9600fc77b455d89e"}, + {file = "websocket-client-1.6.1.tar.gz", hash = "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd"}, + {file = "websocket_client-1.6.1-py3-none-any.whl", hash = "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"}, ] [package.extras] @@ -2102,4 +2174,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "48adc85e7a44ebcad0729582bd2a2ea7dab46cf12127861161c778cf4008e200" +content-hash = "068d52d54597c5fbb5ccbb168ed5b1b76300c5df8621859d7d9b915dbda6e326" diff --git a/pyproject.toml b/pyproject.toml index 7d05d29..d83aeae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,8 @@ mypy = "^0.991" pudb = "^2022.1.3" sphinx = "^7.0.1" furo = "^2023.5.20" +black = "^23.3.0" +flakehell = "^0.9.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/test.py b/test.py index 1781c14..3658d12 100755 --- a/test.py +++ b/test.py @@ -2,6 +2,7 @@ from PyQt5 import QtGui, QtWidgets + class TabBar(QtWidgets.QTabBar): def paintEvent(self, event): painter = QtWidgets.QStylePainter(self) @@ -13,16 +14,18 @@ class TabBar(QtWidgets.QTabBar): painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option) painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option) + class Window(QtWidgets.QTabWidget): def __init__(self): QtWidgets.QTabWidget.__init__(self) self.setTabBar(TabBar(self)) - for color in 'tomato orange yellow lightgreen skyblue plum'.split(): + for color in "tomato orange yellow lightgreen skyblue plum".split(): self.addTab(QtWidgets.QWidget(self), color) -if __name__ == '__main__': +if __name__ == "__main__": import sys + app = QtWidgets.QApplication(sys.argv) window = Window() window.resize(420, 200) diff --git a/test_helpers.py b/test_helpers.py index e6251d4..cbb7c55 100644 --- a/test_helpers.py +++ b/test_helpers.py @@ -1,7 +1,12 @@ -from config import Config from datetime import datetime, timedelta -from helpers import * -from models import Tracks +from helpers import ( + fade_point, + get_audio_segment, + get_tags, + get_relative_date, + leading_silence, + ms_to_mmss, +) def test_fade_point(): @@ -18,8 +23,8 @@ def test_fade_point(): testdata = eval(f.read()) # Volume detection can vary, so ± 1 second is OK - assert fade_at < testdata['fade_at'] + 1000 - assert fade_at > testdata['fade_at'] - 1000 + assert fade_at < testdata["fade_at"] + 1000 + assert fade_at > testdata["fade_at"] - 1000 def test_get_tags(): @@ -32,8 +37,8 @@ def test_get_tags(): with open(test_track_data) as f: testdata = eval(f.read()) - assert tags['artist'] == testdata['artist'] - assert tags['title'] == testdata['title'] + assert tags["artist"] == testdata["artist"] + assert tags["title"] == testdata["title"] def test_get_relative_date(): @@ -44,8 +49,7 @@ def test_get_relative_date(): eight_days_ago = today_at_10 - timedelta(days=8) assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago" sixteen_days_ago = today_at_10 - timedelta(days=16) - assert get_relative_date( - sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago" + assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago" def test_leading_silence(): @@ -62,8 +66,8 @@ def test_leading_silence(): testdata = eval(f.read()) # Volume detection can vary, so ± 1 second is OK - assert silence_at < testdata['leading_silence'] + 1000 - assert silence_at > testdata['leading_silence'] - 1000 + assert silence_at < testdata["leading_silence"] + 1000 + assert silence_at > testdata["leading_silence"] - 1000 def test_ms_to_mmss(): diff --git a/test_models.py b/test_models.py index 901f119..9cd3ce2 100644 --- a/test_models.py +++ b/test_models.py @@ -135,7 +135,6 @@ def test_playdates_remove_track(session): track_path = "/a/b/c" track = Tracks(session, track_path) - playdate = Playdates(session, track.id) Playdates.remove_track(session, track.id) last_played = Playdates.last_played(session, track.id) @@ -149,10 +148,8 @@ def test_playlist_create(session): def test_playlist_add_note(session): note_text = "my note" - note_row = 2 playlist = Playlists(session, "my playlist") - note = playlist.add_note(session, note_row, note_text) assert len(playlist.notes) == 1 playlist_note = playlist.notes[0] @@ -356,9 +353,7 @@ def test_tracks_get_all_paths(session): def test_tracks_get_all_tracks(session): # Need two tracks track1_path = "/a/b/c" - track1 = Tracks(session, track1_path) track2_path = "/m/n/o" - track2 = Tracks(session, track2_path) result = Tracks.get_all_tracks(session) assert track1_path in [a.path for a in result] @@ -369,9 +364,7 @@ def test_tracks_by_filename(session): track1_path = "/a/b/c" track1 = Tracks(session, track1_path) - assert Tracks.get_by_filename( - session, os.path.basename(track1_path) - ) is track1 + assert Tracks.get_by_filename(session, os.path.basename(track1_path)) is track1 def test_tracks_by_path(session): @@ -403,26 +396,24 @@ def test_tracks_rescan(session): # Re-read the track track_read = Tracks.get_by_path(session, test_track_path) - assert track_read.duration == testdata['duration'] - assert track_read.start_gap == testdata['leading_silence'] + assert track_read.duration == testdata["duration"] + assert track_read.start_gap == testdata["leading_silence"] # Silence detection can vary, so ± 1 second is OK - assert track_read.fade_at < testdata['fade_at'] + 1000 - assert track_read.fade_at > testdata['fade_at'] - 1000 - assert track_read.silence_at < testdata['trailing_silence'] + 1000 - assert track_read.silence_at > testdata['trailing_silence'] - 1000 + assert track_read.fade_at < testdata["fade_at"] + 1000 + assert track_read.fade_at > testdata["fade_at"] - 1000 + assert track_read.silence_at < testdata["trailing_silence"] + 1000 + assert track_read.silence_at > testdata["trailing_silence"] - 1000 def test_tracks_remove_by_path(session): track1_path = "/a/b/c" - track1 = Tracks(session, track1_path) assert len(Tracks.get_all_tracks(session)) == 1 Tracks.remove_by_path(session, track1_path) assert len(Tracks.get_all_tracks(session)) == 0 def test_tracks_search_artists(session): - track1_path = "/a/b/c" track1_artist = "Artist One" track1 = Tracks(session, track1_path) @@ -435,7 +426,6 @@ def test_tracks_search_artists(session): session.commit() - x = Tracks.get_all_tracks(session) artist_first_word = track1_artist.split()[0].lower() assert len(Tracks.search_artists(session, artist_first_word)) == 2 assert len(Tracks.search_artists(session, track1_artist)) == 1 @@ -454,7 +444,6 @@ def test_tracks_search_titles(session): session.commit() - x = Tracks.get_all_tracks(session) title_first_word = track1_title.split()[0].lower() assert len(Tracks.search_titles(session, title_first_word)) == 2 assert len(Tracks.search_titles(session, track1_title)) == 1 diff --git a/test_playlists.py b/test_playlists.py index 24bb7e0..57e63d3 100644 --- a/test_playlists.py +++ b/test_playlists.py @@ -3,7 +3,6 @@ from PyQt5.QtCore import Qt from app import playlists from app import models from app import musicmuster -from app import dbconfig def seed2tracks(session): @@ -78,7 +77,6 @@ def test_save_and_restore(qtbot, session): def test_meta_all_clear(qtbot, session): - # Create playlist playlist = models.Playlists(session, "my playlist") playlist_tab = playlists.PlaylistTab(None, session, playlist.id) @@ -107,7 +105,6 @@ def test_meta_all_clear(qtbot, session): def test_meta(qtbot, session): - # Create playlist playlist = playlists.Playlists(session, "my playlist") playlist_tab = playlists.PlaylistTab(None, session, playlist.id) @@ -141,7 +138,7 @@ def test_meta(qtbot, session): # Add a note note_text = "my note" - note_row = 7 # will be added as row 3 + note_row = 7 # will be added as row 3 note = models.Notes(session, playlist.id, note_row, note_text) playlist_tab._insert_note(session, note) @@ -211,7 +208,6 @@ def test_clear_next(qtbot, session): def test_get_selected_row(qtbot, monkeypatch, session): - monkeypatch.setattr(musicmuster, "Session", session) monkeypatch.setattr(playlists, "Session", session) @@ -236,15 +232,12 @@ def test_get_selected_row(qtbot, monkeypatch, session): row0_item0 = playlist_tab.item(0, 0) assert row0_item0 is not None rect = playlist_tab.visualItemRect(row0_item0) - qtbot.mouseClick( - playlist_tab.viewport(), Qt.LeftButton, pos=rect.center() - ) + qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()) row_number = playlist_tab.get_selected_row() assert row_number == 0 def test_set_next(qtbot, monkeypatch, session): - monkeypatch.setattr(musicmuster, "Session", session) monkeypatch.setattr(playlists, "Session", session) seed2tracks(session) @@ -274,14 +267,11 @@ def test_set_next(qtbot, monkeypatch, session): row0_item2 = playlist_tab.item(0, 2) assert row0_item2 is not None rect = playlist_tab.visualItemRect(row0_item2) - qtbot.mouseClick( - playlist_tab.viewport(), Qt.LeftButton, pos=rect.center() - ) + qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()) selected_title = playlist_tab.get_selected_title() assert selected_title == track1_title - qtbot.keyPress(playlist_tab.viewport(), "N", - modifier=Qt.ControlModifier) + qtbot.keyPress(playlist_tab.viewport(), "N", modifier=Qt.ControlModifier) qtbot.wait(1000) diff --git a/tree.py b/tree.py index 4cbba1b..19c51c7 100755 --- a/tree.py +++ b/tree.py @@ -9,9 +9,10 @@ datas = { ], "No Category": [ ("New Game", "Playnite", "", "", "Never", "Not Plated", ""), - ] + ], } + class GroupDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, parent=None): super(GroupDelegate, self).__init__(parent) @@ -25,6 +26,7 @@ class GroupDelegate(QtWidgets.QStyledItemDelegate): option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration option.icon = self._minus_icon if is_open else self._plus_icon + class GroupView(QtWidgets.QTreeView): def __init__(self, model, parent=None): super(GroupView, self).__init__(parent) @@ -48,7 +50,18 @@ class GroupModel(QtGui.QStandardItemModel): def __init__(self, parent=None): super(GroupModel, self).__init__(parent) self.setColumnCount(8) - self.setHorizontalHeaderLabels(["", "Name", "Library", "Release Date", "Genre(s)", "Last Played", "Time Played", ""]) + self.setHorizontalHeaderLabels( + [ + "", + "Name", + "Library", + "Release Date", + "Genre(s)", + "Last Played", + "Time Played", + "", + ] + ) for i in range(self.columnCount()): it = self.horizontalHeaderItem(i) it.setForeground(QtGui.QColor("#F2F2F2")) @@ -84,7 +97,7 @@ class GroupModel(QtGui.QStandardItemModel): item.setEditable(False) item.setBackground(QtGui.QColor("#0D1225")) item.setForeground(QtGui.QColor("#F2F2F2")) - group_item.setChild(j, i+1, item) + group_item.setChild(j, i + 1, item) class MainWindow(QtWidgets.QMainWindow): @@ -100,11 +113,12 @@ class MainWindow(QtWidgets.QMainWindow): for children in childrens: model.append_element_to_group(group_item, children) -if __name__ == '__main__': + +if __name__ == "__main__": import sys + app = QtWidgets.QApplication(sys.argv) w = MainWindow() w.resize(720, 240) w.show() sys.exit(app.exec_()) - diff --git a/web.py b/web.py index 5527215..ec0e365 100755 --- a/web.py +++ b/web.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import sys -from pprint import pprint from PyQt6.QtWidgets import QApplication, QLabel from PyQt6.QtGui import QColor, QPalette