Compare commits

...

5 Commits

Author SHA1 Message Date
Keith Edmunds
c7284c4397 Merge branch 'buttons' into dev 2023-07-09 16:15:29 +01:00
Keith Edmunds
986257bef6 Flake8 and Black run on all files 2023-07-09 16:12:21 +01:00
Keith Edmunds
fbc780b579 Put elapsed/total time below Preview button 2023-07-09 15:40:56 +01:00
Keith Edmunds
722043d049 Move Stop button away from other buttons
Fixes #177
2023-07-09 15:39:22 +01:00
Keith Edmunds
f44d6aa25e Documentation skeleton in place 2023-07-02 19:18:28 +01:00
79 changed files with 8049 additions and 1782 deletions

13
.flake8 Normal file
View File

@ -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

View File

@ -2,14 +2,12 @@
from PyQt6.QtCore import Qt, QEvent, QObject from PyQt6.QtCore import Qt, QEvent, QObject
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QAbstractItemDelegate,
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
QMainWindow, QMainWindow,
QMessageBox, QMessageBox,
QPlainTextEdit, QPlainTextEdit,
QStyledItemDelegate, QStyledItemDelegate,
QStyleOptionViewItem,
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
) )
@ -33,16 +31,15 @@ class EscapeDelegate(QStyledItemDelegate):
if event.type() == QEvent.Type.KeyPress: if event.type() == QEvent.Type.KeyPress:
key_event = cast(QKeyEvent, event) key_event = cast(QKeyEvent, event)
if key_event.key() == Qt.Key.Key_Return: if key_event.key() == Qt.Key.Key_Return:
if key_event.modifiers() == ( if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier):
Qt.KeyboardModifier.ControlModifier
):
print("save data") print("save data")
self.commitData.emit(editor) self.commitData.emit(editor)
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
return True return True
elif key_event.key() == Qt.Key.Key_Escape: elif key_event.key() == Qt.Key.Key_Escape:
discard_edits = QMessageBox.question( discard_edits = QMessageBox.question(
self.parent(), "Abandon edit", "Discard changes?") self.parent(), "Abandon edit", "Discard changes?"
)
if discard_edits == QMessageBox.StandardButton.Yes: if discard_edits == QMessageBox.StandardButton.Yes:
print("abandon edit") print("abandon edit")
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
@ -74,7 +71,7 @@ class MainWindow(QMainWindow):
self.table_widget.resizeRowsToContents() self.table_widget.resizeRowsToContents()
if __name__ == '__main__': if __name__ == "__main__":
app = QApplication([]) app = QApplication([])
window = MainWindow() window = MainWindow()
window.show() window.show()

View File

@ -2,7 +2,7 @@
import os import os
from pydub import AudioSegment, effects from pydub import AudioSegment
# DIR = "/home/kae/git/musicmuster/archive" # DIR = "/home/kae/git/musicmuster/archive"
DIR = "/home/kae/git/musicmuster" DIR = "/home/kae/git/musicmuster"

View File

@ -1,19 +1,18 @@
import inspect import inspect
import logging
import os import os
from config import Config from config import Config
from contextlib import contextmanager from contextlib import contextmanager
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import (sessionmaker, scoped_session) from sqlalchemy.orm import sessionmaker, scoped_session
from typing import Generator from typing import Generator
from log import log from log import log
MYSQL_CONNECT = os.environ.get('MM_DB') MYSQL_CONNECT = os.environ.get("MM_DB")
if MYSQL_CONNECT is None: if MYSQL_CONNECT is None:
raise ValueError("MYSQL_CONNECT is undefined") raise ValueError("MYSQL_CONNECT is undefined")
else: else:
dbname = MYSQL_CONNECT.split('/')[-1] dbname = MYSQL_CONNECT.split("/")[-1]
log.debug(f"Database: {dbname}") log.debug(f"Database: {dbname}")
# MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION') # MM_ENV = os.environ.get('MM_ENV', 'PRODUCTION')
@ -31,10 +30,10 @@ else:
engine = create_engine( engine = create_engine(
MYSQL_CONNECT, MYSQL_CONNECT,
encoding='utf-8', encoding="utf-8",
echo=Config.DISPLAY_SQL, echo=Config.DISPLAY_SQL,
pool_pre_ping=True, 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)) Session = scoped_session(sessionmaker(bind=engine, future=True))
log.debug(f"SqlA: session acquired [{hex(id(Session))}]") log.debug(f"SqlA: session acquired [{hex(id(Session))}]")
log.debug( log.debug(
f"Session acquisition: {file}:{function}:{lineno} " f"Session acquisition: {file}:{function}:{lineno} " f"[{hex(id(Session))}]"
f"[{hex(id(Session))}]"
) )
yield Session yield Session
log.debug(f" SqlA: session released [{hex(id(Session))}]") log.debug(f" SqlA: session released [{hex(id(Session))}]")

View File

@ -1,4 +1,3 @@
import numpy as np
import os import os
import psutil import psutil
import shutil import shutil
@ -16,7 +15,7 @@ from pydub import AudioSegment, effects
from pydub.utils import mediainfo from pydub.utils import mediainfo
from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore from PyQt6.QtWidgets import QMainWindow, QMessageBox # type: ignore
from tinytag import TinyTag # type: ignore from tinytag import TinyTag # type: ignore
from typing import Any, Dict, Optional, Union from typing import Any, Dict, Optional
def ask_yes_no(title: str, question: str, default_yes: bool = False) -> bool: 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.setWindowTitle(title)
dlg.setText(question) dlg.setText(question)
dlg.setStandardButtons( dlg.setStandardButtons(
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
dlg.setIcon(QMessageBox.Icon.Question) dlg.setIcon(QMessageBox.Icon.Question)
if default_yes: if default_yes:
dlg.setDefaultButton(QMessageBox.StandardButton.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( def fade_point(
audio_segment: AudioSegment, fade_threshold: float = 0.0, audio_segment: AudioSegment,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: 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 Returns the millisecond/index of the point where the volume drops below
the maximum and doesn't get louder again. the maximum and doesn't get louder again.
@ -55,8 +57,9 @@ def fade_point(
fade_threshold = max_vol fade_threshold = max_vol
while ( while (
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < fade_threshold audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold
and trim_ms > 0): # noqa W503 and trim_ms > 0
): # noqa W503
trim_ms -= chunk_size trim_ms -= chunk_size
# if there is no trailing silence, return lenght of track (it's less # if there is no trailing silence, return lenght of track (it's less
@ -77,9 +80,9 @@ def file_is_unreadable(path: Optional[str]) -> bool:
def get_audio_segment(path: str) -> Optional[AudioSegment]: def get_audio_segment(path: str) -> Optional[AudioSegment]:
try: try:
if path.endswith('.mp3'): if path.endswith(".mp3"):
return AudioSegment.from_mp3(path) return AudioSegment.from_mp3(path)
elif path.endswith('.flac'): elif path.endswith(".flac"):
return AudioSegment.from_file(path, "flac") # type: ignore return AudioSegment.from_file(path, "flac") # type: ignore
except AttributeError: except AttributeError:
return None return None
@ -99,12 +102,13 @@ def get_tags(path: str) -> Dict[str, Any]:
artist=tag.artist, artist=tag.artist,
bitrate=round(tag.bitrate), bitrate=round(tag.bitrate),
duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000), duration=int(round(tag.duration, Config.MILLISECOND_SIGFIGS) * 1000),
path=path path=path,
) )
def get_relative_date(past_date: Optional[datetime], def get_relative_date(
reference_date: Optional[datetime] = None) -> str: past_date: Optional[datetime], reference_date: Optional[datetime] = None
) -> str:
""" """
Return how long before reference_date past_date is as string. Return how long before reference_date past_date is as string.
@ -147,7 +151,8 @@ def get_relative_date(past_date: Optional[datetime],
def leading_silence( def leading_silence(
audio_segment: AudioSegment, audio_segment: AudioSegment,
silence_threshold: int = Config.DBFS_SILENCE, silence_threshold: int = Config.DBFS_SILENCE,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
) -> int:
""" """
Returns the millisecond/index that the leading silence ends. Returns the millisecond/index that the leading silence ends.
audio_segment - the segment to find silence in audio_segment - the segment to find silence in
@ -159,9 +164,11 @@ def leading_silence(
trim_ms: int = 0 # ms trim_ms: int = 0 # ms
assert chunk_size > 0 # to avoid infinite loop assert chunk_size > 0 # to avoid infinite loop
while ( while audio_segment[
audio_segment[trim_ms:trim_ms + chunk_size].dBFS < # noqa W504 trim_ms : trim_ms + chunk_size
silence_threshold and trim_ms < len(audio_segment)): ].dBFS < silence_threshold and trim_ms < len( # noqa W504
audio_segment
):
trim_ms += chunk_size trim_ms += chunk_size
# if there is no end it should return the length of the segment # 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 = EmailMessage()
msg.set_content(body) msg.set_content(body)
msg['Subject'] = subj msg["Subject"] = subj
msg['From'] = from_addr msg["From"] = from_addr
msg['To'] = to_addr msg["To"] = to_addr
# Send the message via SMTP server. # Send the message via SMTP server.
context = ssl.create_default_context() context = ssl.create_default_context()
@ -194,8 +201,7 @@ def send_mail(to_addr, from_addr, subj, body):
s.quit() s.quit()
def ms_to_mmss(ms: Optional[int], decimals: int = 0, def ms_to_mmss(ms: Optional[int], decimals: int = 0, negative: bool = False) -> str:
negative: bool = False) -> str:
"""Convert milliseconds to mm:ss""" """Convert milliseconds to mm:ss"""
minutes: int minutes: int
@ -227,13 +233,12 @@ def normalise_track(path):
# Check type # Check type
ftype = os.path.splitext(path)[1][1:] ftype = os.path.splitext(path)[1][1:]
if ftype not in ['mp3', 'flac']: if ftype not in ["mp3", "flac"]:
log.info( log.info(
f"helpers.normalise_track({path}): " f"helpers.normalise_track({path}): " f"File type {ftype} not implemented"
f"File type {ftype} not implemented"
) )
bitrate = mediainfo(path)['bit_rate'] bitrate = mediainfo(path)["bit_rate"]
audio = get_audio_segment(path) audio = get_audio_segment(path)
if not audio: if not audio:
return return
@ -245,23 +250,20 @@ def normalise_track(path):
_, temp_path = tempfile.mkstemp() _, temp_path = tempfile.mkstemp()
shutil.copyfile(path, temp_path) shutil.copyfile(path, temp_path)
except Exception as err: except Exception as err:
log.debug( log.debug(f"helpers.normalise_track({path}): err1: {repr(err)}")
f"helpers.normalise_track({path}): err1: {repr(err)}"
)
return return
# Overwrite original file with normalised output # Overwrite original file with normalised output
normalised = effects.normalize(audio) normalised = effects.normalize(audio)
try: try:
normalised.export(path, format=os.path.splitext(path)[1][1:], normalised.export(path, format=os.path.splitext(path)[1][1:], bitrate=bitrate)
bitrate=bitrate)
# Fix up permssions and ownership # Fix up permssions and ownership
os.chown(path, stats.st_uid, stats.st_gid) os.chown(path, stats.st_uid, stats.st_gid)
os.chmod(path, stats.st_mode) os.chmod(path, stats.st_mode)
# Copy tags # Copy tags
if ftype == 'flac': if ftype == "flac":
tag_handler = FLAC tag_handler = FLAC
elif ftype == 'mp3': elif ftype == "mp3":
tag_handler = MP3 tag_handler = MP3
else: else:
return return
@ -271,9 +273,7 @@ def normalise_track(path):
dst[tag] = src[tag] dst[tag] = src[tag]
dst.save() dst.save()
except Exception as err: except Exception as err:
log.debug( log.debug(f"helpers.normalise_track({path}): err2: {repr(err)}")
f"helpers.normalise_track({path}): err2: {repr(err)}"
)
# Restore original file # Restore original file
shutil.copyfile(path, temp_path) shutil.copyfile(path, temp_path)
finally: finally:
@ -296,9 +296,9 @@ def open_in_audacity(path: str) -> bool:
if not path: if not path:
return False return False
to_pipe: str = '/tmp/audacity_script_pipe.to.' + str(os.getuid()) to_pipe: str = "/tmp/audacity_script_pipe.to." + str(os.getuid())
from_pipe: str = '/tmp/audacity_script_pipe.from.' + str(os.getuid()) from_pipe: str = "/tmp/audacity_script_pipe.from." + str(os.getuid())
eol: str = '\n' eol: str = "\n"
def send_command(command: str) -> None: def send_command(command: str) -> None:
"""Send a single command.""" """Send a single command."""
@ -308,13 +308,13 @@ def open_in_audacity(path: str) -> bool:
def get_response() -> str: def get_response() -> str:
"""Return the command response.""" """Return the command response."""
result: str = '' result: str = ""
line: str = '' line: str = ""
while True: while True:
result += line result += line
line = from_audacity.readline() line = from_audacity.readline()
if line == '\n' and len(result) > 0: if line == "\n" and len(result) > 0:
break break
return result return result
@ -325,8 +325,7 @@ def open_in_audacity(path: str) -> bool:
response = get_response() response = get_response()
return response return response
with open(to_pipe, 'w') as to_audacity, open( with open(to_pipe, "w") as to_audacity, open(from_pipe, "rt") as from_audacity:
from_pipe, 'rt') as from_audacity:
do_command(f'Import2: Filename="{path}"') do_command(f'Import2: Filename="{path}"')
return True return True
@ -338,18 +337,18 @@ def set_track_metadata(session, track):
t = get_tags(track.path) t = get_tags(track.path)
audio = get_audio_segment(track.path) audio = get_audio_segment(track.path)
track.title = t['title'] track.title = t["title"]
track.artist = t['artist'] track.artist = t["artist"]
track.bitrate = t['bitrate'] track.bitrate = t["bitrate"]
if not audio: if not audio:
return return
track.duration = len(audio) track.duration = len(audio)
track.start_gap = leading_silence(audio) track.start_gap = leading_silence(audio)
track.fade_at = round(fade_point(audio) / 1000, track.fade_at = round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
Config.MILLISECOND_SIGFIGS) * 1000 track.silence_at = (
track.silence_at = round(trailing_silence(audio) / 1000, round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
Config.MILLISECOND_SIGFIGS) * 1000 )
track.mtime = os.path.getmtime(track.path) track.mtime = os.path.getmtime(track.path)
session.commit() session.commit()
@ -358,20 +357,20 @@ def set_track_metadata(session, track):
def show_OK(parent: QMainWindow, title: str, msg: str) -> None: def show_OK(parent: QMainWindow, title: str, msg: str) -> None:
"""Display a message to user""" """Display a message to user"""
QMessageBox.information(parent, title, msg, QMessageBox.information(parent, title, msg, buttons=QMessageBox.StandardButton.Ok)
buttons=QMessageBox.StandardButton.Ok)
def show_warning(parent: QMainWindow, title: str, msg: str) -> None: def show_warning(parent: QMainWindow, title: str, msg: str) -> None:
"""Display a warning to user""" """Display a warning to user"""
QMessageBox.warning(parent, title, msg, QMessageBox.warning(parent, title, msg, buttons=QMessageBox.StandardButton.Cancel)
buttons=QMessageBox.StandardButton.Cancel)
def trailing_silence( def trailing_silence(
audio_segment: AudioSegment, silence_threshold: int = -50, audio_segment: AudioSegment,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE) -> int: silence_threshold: int = -50,
chunk_size: int = Config.AUDIO_SEGMENT_CHUNK_SIZE,
) -> int:
"""Return fade point from start in milliseconds""" """Return fade point from start in milliseconds"""
return fade_point(audio_segment, silence_threshold, chunk_size) return fade_point(audio_segment, silence_threshold, chunk_size)

View File

@ -2,7 +2,7 @@ import urllib.parse
from datetime import datetime from datetime import datetime
from slugify import slugify # type: ignore from slugify import slugify # type: ignore
from typing import Dict, Optional from typing import Dict
from PyQt6.QtCore import QUrl # type: ignore from PyQt6.QtCore import QUrl # type: ignore
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QTabWidget from PyQt6.QtWidgets import QTabWidget
@ -47,13 +47,11 @@ class InfoTabs(QTabWidget):
if url in self.tabtitles.values(): if url in self.tabtitles.values():
self.setCurrentIndex( self.setCurrentIndex(
list(self.tabtitles.keys())[ list(self.tabtitles.keys())[list(self.tabtitles.values()).index(url)]
list(self.tabtitles.values()).index(url)
]
) )
return return
short_title = title[:Config.INFO_TAB_TITLE_LENGTH] short_title = title[: Config.INFO_TAB_TITLE_LENGTH]
if self.count() < Config.MAX_INFO_TABS: if self.count() < Config.MAX_INFO_TABS:
# Create a new tab # Create a new tab
@ -63,9 +61,7 @@ class InfoTabs(QTabWidget):
else: else:
# Reuse oldest widget # Reuse oldest widget
widget = min( widget = min(self.last_update, key=self.last_update.get) # type: ignore
self.last_update, key=self.last_update.get # type: ignore
)
tab_index = self.indexOf(widget) tab_index = self.indexOf(widget)
self.setTabText(tab_index, short_title) self.setTabText(tab_index, short_title)

View File

@ -1,13 +1,11 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os.path
import re import re
import stackprinter # type: ignore
from dbconfig import Session, scoped_session from dbconfig import scoped_session
from datetime import datetime from datetime import datetime
from typing import Iterable, List, Optional, Union, ValuesView from typing import List, Optional
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
@ -35,21 +33,14 @@ from sqlalchemy.orm.exc import (
from sqlalchemy.exc import ( from sqlalchemy.exc import (
IntegrityError, IntegrityError,
) )
from config import Config
from helpers import (
fade_point,
get_audio_segment,
get_tags,
leading_silence,
trailing_silence,
)
from log import log from log import log
Base = declarative_base() Base = declarative_base()
# Database classes # Database classes
class Carts(Base): class Carts(Base):
__tablename__ = 'carts' __tablename__ = "carts"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
cart_number: int = Column(Integer, nullable=False, unique=True) cart_number: int = Column(Integer, nullable=False, unique=True)
@ -64,10 +55,15 @@ class Carts(Base):
f"name={self.name}, path={self.path}>" f"name={self.name}, path={self.path}>"
) )
def __init__(self, session: scoped_session, cart_number: int, def __init__(
self,
session: scoped_session,
cart_number: int,
name: Optional[str] = None, name: Optional[str] = None,
duration: Optional[int] = None, path: Optional[str] = None, duration: Optional[int] = None,
enabled: bool = True) -> None: path: Optional[str] = None,
enabled: bool = True,
) -> None:
"""Create new cart""" """Create new cart"""
self.cart_number = cart_number self.cart_number = cart_number
@ -81,7 +77,7 @@ class Carts(Base):
class NoteColours(Base): class NoteColours(Base):
__tablename__ = 'notecolours' __tablename__ = "notecolours"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
substring = Column(String(256), index=False) substring = Column(String(256), index=False)
@ -106,11 +102,15 @@ class NoteColours(Base):
if not text: if not text:
return None return None
for rec in session.execute( for rec in (
session.execute(
select(NoteColours) select(NoteColours)
.filter(NoteColours.enabled.is_(True)) .filter(NoteColours.enabled.is_(True))
.order_by(NoteColours.order) .order_by(NoteColours.order)
).scalars().all(): )
.scalars()
.all()
):
if rec.is_regex: if rec.is_regex:
flags = re.UNICODE flags = re.UNICODE
if not rec.is_casesensitive: if not rec.is_casesensitive:
@ -130,11 +130,11 @@ class NoteColours(Base):
class Playdates(Base): class Playdates(Base):
__tablename__ = 'playdates' __tablename__ = "playdates"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
lastplayed = Column(DateTime, index=True, default=None) 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") track: "Tracks" = relationship("Tracks", back_populates="playdates")
def __repr__(self) -> str: def __repr__(self) -> str:
@ -152,8 +152,7 @@ class Playdates(Base):
session.commit() session.commit()
@staticmethod @staticmethod
def last_played(session: scoped_session, def last_played(session: scoped_session, track_id: int) -> Optional[datetime]:
track_id: int) -> Optional[datetime]:
"""Return datetime track last played or None""" """Return datetime track last played or None"""
last_played = session.execute( last_played = session.execute(
@ -169,8 +168,7 @@ class Playdates(Base):
return None return None
@staticmethod @staticmethod
def played_after(session: scoped_session, def played_after(session: scoped_session, since: datetime) -> List["Playdates"]:
since: datetime) -> List["Playdates"]:
"""Return a list of Playdates objects since passed time""" """Return a list of Playdates objects since passed time"""
return ( return (
@ -203,7 +201,7 @@ class Playlists(Base):
"PlaylistRows", "PlaylistRows",
back_populates="playlist", back_populates="playlist",
cascade="all, delete-orphan", cascade="all, delete-orphan",
order_by="PlaylistRows.plr_rownum" order_by="PlaylistRows.plr_rownum",
) )
def __repr__(self) -> str: def __repr__(self) -> str:
@ -232,11 +230,9 @@ class Playlists(Base):
) )
@classmethod @classmethod
def create_playlist_from_template(cls, def create_playlist_from_template(
session: scoped_session, cls, session: scoped_session, template: "Playlists", playlist_name: str
template: "Playlists", ) -> Optional["Playlists"]:
playlist_name: str) \
-> Optional["Playlists"]:
"""Create a new playlist from template""" """Create a new playlist from template"""
playlist = cls(session, playlist_name) playlist = cls(session, playlist_name)
@ -277,9 +273,7 @@ class Playlists(Base):
return ( return (
session.execute( session.execute(
select(cls) select(cls).filter(cls.is_template.is_(True)).order_by(cls.name)
.filter(cls.is_template.is_(True))
.order_by(cls.name)
) )
.scalars() .scalars()
.all() .all()
@ -295,7 +289,7 @@ class Playlists(Base):
.filter( .filter(
cls.tab.is_(None), cls.tab.is_(None),
cls.is_template.is_(False), cls.is_template.is_(False),
cls.deleted.is_(False) cls.deleted.is_(False),
) )
.order_by(cls.last_used.desc()) .order_by(cls.last_used.desc())
) )
@ -310,11 +304,7 @@ class Playlists(Base):
""" """
return ( return (
session.execute( session.execute(select(cls).where(cls.tab.is_not(None)).order_by(cls.tab))
select(cls)
.where(cls.tab.is_not(None))
.order_by(cls.tab)
)
.scalars() .scalars()
.all() .all()
) )
@ -329,15 +319,9 @@ class Playlists(Base):
def move_tab(session: scoped_session, frm: int, to: int) -> None: def move_tab(session: scoped_session, frm: int, to: int) -> None:
"""Move tabs""" """Move tabs"""
row_frm = session.execute( row_frm = session.execute(select(Playlists).filter_by(tab=frm)).scalar_one()
select(Playlists)
.filter_by(tab=frm)
).scalar_one()
row_to = session.execute( row_to = session.execute(select(Playlists).filter_by(tab=to)).scalar_one()
select(Playlists)
.filter_by(tab=to)
).scalar_one()
row_frm.tab = None row_frm.tab = None
row_to.tab = None row_to.tab = None
@ -354,8 +338,9 @@ class Playlists(Base):
session.flush() session.flush()
@staticmethod @staticmethod
def save_as_template(session: scoped_session, def save_as_template(
playlist_id: int, template_name: str) -> None: session: scoped_session, playlist_id: int, template_name: str
) -> None:
"""Save passed playlist as new template""" """Save passed playlist as new template"""
template = Playlists(session, template_name) template = Playlists(session, template_name)
@ -369,15 +354,14 @@ class Playlists(Base):
class PlaylistRows(Base): class PlaylistRows(Base):
__tablename__ = 'playlist_rows' __tablename__ = "playlist_rows"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
plr_rownum: int = Column(Integer, nullable=False) plr_rownum: int = Column(Integer, nullable=False)
note: str = Column(String(2048), index=False, default="", nullable=False) note: str = Column(String(2048), index=False, default="", nullable=False)
playlist_id: int = Column(Integer, ForeignKey('playlists.id'), playlist_id: int = Column(Integer, ForeignKey("playlists.id"), nullable=False)
nullable=False)
playlist: Playlists = relationship(Playlists, back_populates="rows") 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") track: "Tracks" = relationship("Tracks", back_populates="playlistrows")
played: bool = Column(Boolean, nullable=False, index=False, default=False) played: bool = Column(Boolean, nullable=False, index=False, default=False)
@ -388,12 +372,13 @@ class PlaylistRows(Base):
f"note={self.note}, plr_rownum={self.plr_rownum}>" f"note={self.note}, plr_rownum={self.plr_rownum}>"
) )
def __init__(self, def __init__(
self,
session: scoped_session, session: scoped_session,
playlist_id: int, playlist_id: int,
track_id: Optional[int], track_id: Optional[int],
row_number: int, row_number: int,
note: str = "" note: str = "",
) -> None: ) -> None:
"""Create PlaylistRows object""" """Create PlaylistRows object"""
@ -409,38 +394,38 @@ class PlaylistRows(Base):
current_note = self.note current_note = self.note
if current_note: if current_note:
self.note = current_note + '\n' + extra_note self.note = current_note + "\n" + extra_note
else: else:
self.note = extra_note self.note = extra_note
@staticmethod @staticmethod
def copy_playlist(session: scoped_session, def copy_playlist(session: scoped_session, src_id: int, dst_id: int) -> None:
src_id: int,
dst_id: int) -> None:
"""Copy playlist entries""" """Copy playlist entries"""
src_rows = session.execute( src_rows = (
select(PlaylistRows) session.execute(
.filter(PlaylistRows.playlist_id == src_id) select(PlaylistRows).filter(PlaylistRows.playlist_id == src_id)
).scalars().all() )
.scalars()
.all()
)
for plr in src_rows: for plr in src_rows:
PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum, PlaylistRows(session, dst_id, plr.track_id, plr.plr_rownum, plr.note)
plr.note)
@staticmethod @staticmethod
def delete_higher_rows( 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 Delete rows in given playlist that have a higher row number
than 'maxrow' than 'maxrow'
""" """
session.execute( session.execute(
delete(PlaylistRows) delete(PlaylistRows).where(
.where(
PlaylistRows.playlist_id == playlist_id, PlaylistRows.playlist_id == playlist_id,
PlaylistRows.plr_rownum > maxrow PlaylistRows.plr_rownum > maxrow,
) )
) )
session.flush() session.flush()
@ -451,11 +436,15 @@ class PlaylistRows(Base):
Ensure the row numbers for passed playlist have no gaps Ensure the row numbers for passed playlist have no gaps
""" """
plrs = session.execute( plrs = (
session.execute(
select(PlaylistRows) select(PlaylistRows)
.where(PlaylistRows.playlist_id == playlist_id) .where(PlaylistRows.playlist_id == playlist_id)
.order_by(PlaylistRows.plr_rownum) .order_by(PlaylistRows.plr_rownum)
).scalars().all() )
.scalars()
.all()
)
for i, plr in enumerate(plrs): for i, plr in enumerate(plrs):
plr.plr_rownum = i plr.plr_rownum = i
@ -464,113 +453,126 @@ class PlaylistRows(Base):
session.commit() session.commit()
@classmethod @classmethod
def get_from_id_list(cls, session: scoped_session, playlist_id: int, def get_from_id_list(
plr_ids: List[int]) -> List["PlaylistRows"]: 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 Take a list of PlaylistRows ids and return a list of corresponding
PlaylistRows objects PlaylistRows objects
""" """
plrs = session.execute( plrs = (
session.execute(
select(cls) select(cls)
.where( .where(cls.playlist_id == playlist_id, cls.id.in_(plr_ids))
cls.playlist_id == playlist_id, .order_by(cls.plr_rownum)
cls.id.in_(plr_ids) )
).order_by(cls.plr_rownum)).scalars().all() .scalars()
.all()
)
return plrs return plrs
@staticmethod @staticmethod
def get_last_used_row(session: scoped_session, def get_last_used_row(session: scoped_session, playlist_id: int) -> Optional[int]:
playlist_id: int) -> Optional[int]:
"""Return the last used row for playlist, or None if no rows""" """Return the last used row for playlist, or None if no rows"""
return session.execute( return session.execute(
select(func.max(PlaylistRows.plr_rownum)) select(func.max(PlaylistRows.plr_rownum)).where(
.where(PlaylistRows.playlist_id == playlist_id) PlaylistRows.playlist_id == playlist_id
)
).scalar_one() ).scalar_one()
@staticmethod @staticmethod
def get_track_plr(session: scoped_session, track_id: int, def get_track_plr(
playlist_id: int) -> Optional["PlaylistRows"]: session: scoped_session, track_id: int, playlist_id: int
) -> Optional["PlaylistRows"]:
"""Return first matching PlaylistRows object or None""" """Return first matching PlaylistRows object or None"""
return session.scalars( return session.scalars(
select(PlaylistRows) select(PlaylistRows)
.where( .where(
PlaylistRows.track_id == track_id, PlaylistRows.track_id == track_id,
PlaylistRows.playlist_id == playlist_id PlaylistRows.playlist_id == playlist_id,
) )
.limit(1) .limit(1)
).first() ).first()
@classmethod @classmethod
def get_played_rows(cls, session: scoped_session, def get_played_rows(
playlist_id: int) -> List["PlaylistRows"]: cls, session: scoped_session, playlist_id: int
) -> List["PlaylistRows"]:
""" """
For passed playlist, return a list of rows that For passed playlist, return a list of rows that
have been played. have been played.
""" """
plrs = session.execute( plrs = (
session.execute(
select(cls) select(cls)
.where( .where(cls.playlist_id == playlist_id, cls.played.is_(True))
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 return plrs
@classmethod @classmethod
def get_rows_with_tracks( def get_rows_with_tracks(
cls, session: scoped_session, playlist_id: int, cls,
session: scoped_session,
playlist_id: int,
from_row: Optional[int] = None, 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 For passed playlist, return a list of rows that
contain tracks contain tracks
""" """
query = select(cls).where( query = select(cls).where(
cls.playlist_id == playlist_id, cls.playlist_id == playlist_id, cls.track_id.is_not(None)
cls.track_id.is_not(None)
) )
if from_row is not None: if from_row is not None:
query = query.where(cls.plr_rownum >= from_row) query = query.where(cls.plr_rownum >= from_row)
if to_row is not None: if to_row is not None:
query = query.where(cls.plr_rownum <= to_row) query = query.where(cls.plr_rownum <= to_row)
plrs = ( plrs = session.execute((query).order_by(cls.plr_rownum)).scalars().all()
session.execute((query).order_by(cls.plr_rownum)).scalars().all()
)
return plrs return plrs
@classmethod @classmethod
def get_unplayed_rows(cls, session: scoped_session, def get_unplayed_rows(
playlist_id: int) -> List["PlaylistRows"]: cls, session: scoped_session, playlist_id: int
) -> List["PlaylistRows"]:
""" """
For passed playlist, return a list of playlist rows that For passed playlist, return a list of playlist rows that
have not been played. have not been played.
""" """
plrs = session.execute( plrs = (
session.execute(
select(cls) select(cls)
.where( .where(
cls.playlist_id == playlist_id, cls.playlist_id == playlist_id,
cls.track_id.is_not(None), cls.track_id.is_not(None),
cls.played.is_(False) cls.played.is_(False),
) )
.order_by(cls.plr_rownum) .order_by(cls.plr_rownum)
).scalars().all() )
.scalars()
.all()
)
return plrs return plrs
@staticmethod @staticmethod
def move_rows_down(session: scoped_session, playlist_id: int, def move_rows_down(
starting_row: int, move_by: int) -> None: session: scoped_session, playlist_id: int, starting_row: int, move_by: int
) -> None:
""" """
Create space to insert move_by additional rows by incremented row Create space to insert move_by additional rows by incremented row
number from starting_row to end of playlist number from starting_row to end of playlist
@ -580,7 +582,7 @@ class PlaylistRows(Base):
update(PlaylistRows) update(PlaylistRows)
.where( .where(
(PlaylistRows.playlist_id == playlist_id), (PlaylistRows.playlist_id == playlist_id),
(PlaylistRows.plr_rownum >= starting_row) (PlaylistRows.plr_rownum >= starting_row),
) )
.values(plr_rownum=PlaylistRows.plr_rownum + move_by) .values(plr_rownum=PlaylistRows.plr_rownum + move_by)
) )
@ -589,7 +591,7 @@ class PlaylistRows(Base):
class Settings(Base): class Settings(Base):
"""Manage settings""" """Manage settings"""
__tablename__ = 'settings' __tablename__ = "settings"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
name: str = Column(String(64), nullable=False, unique=True) name: str = Column(String(64), nullable=False, unique=True)
@ -602,21 +604,16 @@ class Settings(Base):
return f"<Settings(id={self.id}, name={self.name}, {value=}>" return f"<Settings(id={self.id}, name={self.name}, {value=}>"
def __init__(self, session: scoped_session, name: str): def __init__(self, session: scoped_session, name: str):
self.name = name self.name = name
session.add(self) session.add(self)
session.flush() session.flush()
@classmethod @classmethod
def get_int_settings(cls, session: scoped_session, def get_int_settings(cls, session: scoped_session, name: str) -> "Settings":
name: str) -> "Settings":
"""Get setting for an integer or return new setting record""" """Get setting for an integer or return new setting record"""
try: try:
return session.execute( return session.execute(select(cls).where(cls.name == name)).scalar_one()
select(cls)
.where(cls.name == name)
).scalar_one()
except NoResultFound: except NoResultFound:
return Settings(session, name) return Settings(session, name)
@ -629,7 +626,7 @@ class Settings(Base):
class Tracks(Base): class Tracks(Base):
__tablename__ = 'tracks' __tablename__ = "tracks"
id: int = Column(Integer, primary_key=True, autoincrement=True) id: int = Column(Integer, primary_key=True, autoincrement=True)
title = Column(String(256), index=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) path: str = Column(String(2048), index=False, nullable=False, unique=True)
mtime = Column(Float, index=True) mtime = Column(Float, index=True)
bitrate = Column(Integer, nullable=True, default=None) bitrate = Column(Integer, nullable=True, default=None)
playlistrows: PlaylistRows = relationship("PlaylistRows", playlistrows: PlaylistRows = relationship("PlaylistRows", back_populates="track")
back_populates="track")
playlists = association_proxy("playlistrows", "playlist") playlists = association_proxy("playlistrows", "playlist")
playdates: Playdates = relationship("Playdates", back_populates="track") playdates: Playdates = relationship("Playdates", back_populates="track")
@ -693,46 +689,36 @@ class Tracks(Base):
return session.execute(select(cls)).scalars().all() return session.execute(select(cls)).scalars().all()
@classmethod @classmethod
def get_by_path(cls, session: scoped_session, def get_by_path(cls, session: scoped_session, path: str) -> Optional["Tracks"]:
path: str) -> Optional["Tracks"]:
""" """
Return track with passed path, or None. Return track with passed path, or None.
""" """
try: try:
return ( return session.execute(
session.execute( select(Tracks).where(Tracks.path == path)
select(Tracks)
.where(Tracks.path == path)
).scalar_one() ).scalar_one()
)
except NoResultFound: except NoResultFound:
return None return None
@classmethod @classmethod
def search_artists(cls, session: scoped_session, def search_artists(cls, session: scoped_session, text: str) -> List["Tracks"]:
text: str) -> List["Tracks"]:
"""Search case-insenstively for artists containing str""" """Search case-insenstively for artists containing str"""
return ( return (
session.execute( session.execute(
select(cls) select(cls).where(cls.artist.ilike(f"%{text}%")).order_by(cls.title)
.where(cls.artist.ilike(f"%{text}%"))
.order_by(cls.title)
) )
.scalars() .scalars()
.all() .all()
) )
@classmethod @classmethod
def search_titles(cls, session: scoped_session, def search_titles(cls, session: scoped_session, text: str) -> List["Tracks"]:
text: str) -> List["Tracks"]:
"""Search case-insenstively for titles containing str""" """Search case-insenstively for titles containing str"""
return ( return (
session.execute( session.execute(
select(cls) select(cls).where(cls.title.like(f"{text}%")).order_by(cls.title)
.where(cls.title.like(f"{text}%"))
.order_by(cls.title)
) )
.scalars() .scalars()
.all() .all()

View File

@ -1,9 +1,9 @@
# import os # import os
import threading import threading
import vlc # type: ignore import vlc # type: ignore
# #
from config import Config from config import Config
from datetime import datetime
from helpers import file_is_unreadable from helpers import file_is_unreadable
from typing import Optional from typing import Optional
from time import sleep from time import sleep
@ -19,7 +19,6 @@ lock = threading.Lock()
class FadeTrack(QRunnable): class FadeTrack(QRunnable):
def __init__(self, player: vlc.MediaPlayer) -> None: def __init__(self, player: vlc.MediaPlayer) -> None:
super().__init__() super().__init__()
self.player = player self.player = player
@ -47,8 +46,7 @@ class FadeTrack(QRunnable):
for i in range(1, steps + 1): for i in range(1, steps + 1):
measures_to_reduce_by += i measures_to_reduce_by += i
volume_factor = 1 - ( volume_factor = 1 - (measures_to_reduce_by / total_measures_count)
measures_to_reduce_by / total_measures_count)
self.player.audio_set_volume(int(original_volume * volume_factor)) self.player.audio_set_volume(int(original_volume * volume_factor))
sleep(sleep_time) sleep(sleep_time)
@ -98,8 +96,7 @@ class Music:
return None return None
return self.player.get_position() return self.player.get_position()
def play(self, path: str, def play(self, path: str, position: Optional[float] = None) -> Optional[int]:
position: Optional[float] = None) -> Optional[int]:
""" """
Start playing the track at path. Start playing the track at path.

View File

@ -11,7 +11,6 @@ import subprocess
import sys import sys
import threading import threading
import icons_rc
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pygame import mixer from pygame import mixer
from time import sleep from time import sleep
@ -61,15 +60,7 @@ from dbconfig import (
) )
import helpers import helpers
import music import music
from models import ( from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
Base,
Carts,
Playdates,
PlaylistRows,
Playlists,
Settings,
Tracks
)
from config import Config from config import Config
from playlists import PlaylistTab from playlists import PlaylistTab
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
@ -93,7 +84,7 @@ class CartButton(QPushButton):
self.cart_id = cart.id self.cart_id = cart.id
if cart.path and cart.enabled and not cart.duration: if cart.path and cart.enabled and not cart.duration:
tags = helpers.get_tags(cart.path) tags = helpers.get_tags(cart.path)
cart.duration = tags['duration'] cart.duration = tags["duration"]
self.duration = cart.duration self.duration = cart.duration
self.path = cart.path self.path = cart.path
self.player = None self.player = None
@ -110,8 +101,9 @@ class CartButton(QPushButton):
self.pgb.setTextVisible(False) self.pgb.setTextVisible(False)
self.pgb.setVisible(False) self.pgb.setVisible(False)
palette = self.pgb.palette() palette = self.pgb.palette()
palette.setColor(QPalette.ColorRole.Highlight, palette.setColor(
QColor(Config.COLOUR_CART_PROGRESSBAR)) QPalette.ColorRole.Highlight, QColor(Config.COLOUR_CART_PROGRESSBAR)
)
self.pgb.setPalette(palette) self.pgb.setPalette(palette)
self.pgb.setGeometry(0, 0, self.width(), 10) self.pgb.setGeometry(0, 0, self.width(), 10)
self.pgb.setMinimum(0) self.pgb.setMinimum(0)
@ -157,16 +149,13 @@ class FadeCurve:
# Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE # Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE
# milliseconds before fade starts to silence # milliseconds before fade starts to silence
self.start_ms = max( self.start_ms = max(0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
self.end_ms = track.silence_at 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()) self.graph_array = np.array(self.audio_segment.get_array_of_samples())
# Calculate the factor to map milliseconds of track to array # Calculate the factor to map milliseconds of track to array
self.ms_to_array_factor = ( self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms)
len(self.graph_array) / (self.end_ms - self.start_ms)
)
self.region = None self.region = None
@ -192,10 +181,7 @@ class FadeCurve:
if self.region is None: if self.region is None:
# Create the region now that we're into fade # Create the region now that we're into fade
self.region = pg.LinearRegionItem( self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)])
[0, 0],
bounds=[0, len(self.graph_array)]
)
self.GraphWidget.addItem(self.region) self.GraphWidget.addItem(self.region)
# Update region position # Update region position
@ -286,8 +272,9 @@ class PlaylistTrack:
f"playlist_id={self.playlist_id}>" f"playlist_id={self.playlist_id}>"
) )
def set_plr(self, session: scoped_session, plr: PlaylistRows, def set_plr(
tab: PlaylistTab) -> None: self, session: scoped_session, plr: PlaylistRows, tab: PlaylistTab
) -> None:
""" """
Update with new plr information Update with new plr information
""" """
@ -323,8 +310,7 @@ class PlaylistTrack:
self.start_time = datetime.now() self.start_time = datetime.now()
if self.duration: if self.duration:
self.end_time = ( self.end_time = self.start_time + timedelta(milliseconds=self.duration)
self.start_time + timedelta(milliseconds=self.duration))
class Window(QMainWindow, Ui_MainWindow): class Window(QMainWindow, Ui_MainWindow):
@ -355,14 +341,15 @@ class Window(QMainWindow, Ui_MainWindow):
self.txtSearch.setHidden(True) self.txtSearch.setHidden(True)
self.hide_played_tracks = False self.hide_played_tracks = False
mixer.init() mixer.init()
self.widgetFadeVolume.hideAxis('bottom') self.widgetFadeVolume.hideAxis("bottom")
self.widgetFadeVolume.hideAxis('left') self.widgetFadeVolume.hideAxis("left")
self.widgetFadeVolume.setDefaultPadding(0) self.widgetFadeVolume.setDefaultPadding(0)
self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND) self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND)
FadeCurve.GraphWidget = self.widgetFadeVolume FadeCurve.GraphWidget = self.widgetFadeVolume
self.visible_playlist_tab: Callable[[], PlaylistTab] = \ self.visible_playlist_tab: Callable[
self.tabPlaylist.currentWidget [], PlaylistTab
] = self.tabPlaylist.currentWidget
self.load_last_playlists() self.load_last_playlists()
if Config.CARTS_HIDE: if Config.CARTS_HIDE:
@ -380,10 +367,8 @@ class Window(QMainWindow, Ui_MainWindow):
try: try:
git_tag = str( git_tag = str(
subprocess.check_output( subprocess.check_output(["git", "describe"], stderr=subprocess.STDOUT)
['git', 'describe'], stderr=subprocess.STDOUT ).strip("'b\\n")
)
).strip('\'b\\n')
except subprocess.CalledProcessError as exc_info: except subprocess.CalledProcessError as exc_info:
git_tag = str(exc_info.output) git_tag = str(exc_info.output)
@ -395,7 +380,7 @@ class Window(QMainWindow, Ui_MainWindow):
self, self,
"About", "About",
f"MusicMuster {git_tag}\n\nDatabase: {dbname}", f"MusicMuster {git_tag}\n\nDatabase: {dbname}",
QMessageBox.StandardButton.Ok QMessageBox.StandardButton.Ok,
) )
def cart_configure(self, cart: Carts, btn: CartButton) -> None: 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 # Don't allow clicks while we're playing
btn.setEnabled(False) btn.setEnabled(False)
if not btn.player: if not btn.player:
log.debug( log.debug(f"musicmuster.cart_click(): no player assigned ({btn=})")
f"musicmuster.cart_click(): no player assigned ({btn=})")
return return
btn.player.play() btn.player.play()
btn.is_playing = True btn.is_playing = True
colour = Config.COLOUR_CART_PLAYING colour = Config.COLOUR_CART_PLAYING
thread = threading.Thread(target=self.cart_progressbar, thread = threading.Thread(target=self.cart_progressbar, args=(btn,))
args=(btn,))
thread.start() thread.start()
else: else:
colour = Config.COLOUR_CART_ERROR colour = Config.COLOUR_CART_ERROR
@ -469,7 +452,7 @@ class Window(QMainWindow, Ui_MainWindow):
return return
if cart.path and not helpers.file_is_unreadable(cart.path): if cart.path and not helpers.file_is_unreadable(cart.path):
tags = helpers.get_tags(cart.path) tags = helpers.get_tags(cart.path)
cart.duration = tags['duration'] cart.duration = tags["duration"]
cart.enabled = dlg.ui.chkEnabled.isChecked() cart.enabled = dlg.ui.chkEnabled.isChecked()
cart.name = name cart.name = name
@ -488,8 +471,7 @@ class Window(QMainWindow, Ui_MainWindow):
for cart_number in range(1, Config.CARTS_COUNT + 1): for cart_number in range(1, Config.CARTS_COUNT + 1):
cart = session.query(Carts).get(cart_number) cart = session.query(Carts).get(cart_number)
if cart is None: if cart is None:
cart = Carts(session, cart_number, cart = Carts(session, cart_number, name=f"Cart #{cart_number}")
name=f"Cart #{cart_number}")
btn = CartButton(self, cart) btn = CartButton(self, cart)
btn.clicked.connect(self.cart_click) btn.clicked.connect(self.cart_click)
@ -540,7 +522,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.update_headers() self.update_headers()
def clear_selection(self) -> None: def clear_selection(self) -> None:
""" Clear selected row""" """Clear selected row"""
# Unselect any selected rows # Unselect any selected rows
if self.visible_playlist_tab(): if self.visible_playlist_tab():
@ -555,27 +537,25 @@ class Window(QMainWindow, Ui_MainWindow):
if self.playing: if self.playing:
event.ignore() event.ignore()
helpers.show_warning( helpers.show_warning(
self, self, "Track playing", "Can't close application while track is playing"
"Track playing", )
"Can't close application while track is playing")
else: else:
with Session() as session: with Session() as session:
record = Settings.get_int_settings( record = Settings.get_int_settings(session, "mainwindow_height")
session, "mainwindow_height")
if record.f_int != self.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") record = Settings.get_int_settings(session, "mainwindow_width")
if record.f_int != self.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") record = Settings.get_int_settings(session, "mainwindow_x")
if record.f_int != self.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") record = Settings.get_int_settings(session, "mainwindow_y")
if record.f_int != self.y(): if record.f_int != self.y():
record.update(session, {'f_int': self.y()}) record.update(session, {"f_int": self.y()})
# Save splitter settings # Save splitter settings
splitter_sizes = self.splitter.sizes() splitter_sizes = self.splitter.sizes()
@ -584,16 +564,15 @@ class Window(QMainWindow, Ui_MainWindow):
record = Settings.get_int_settings(session, "splitter_top") record = Settings.get_int_settings(session, "splitter_top")
if record.f_int != 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") record = Settings.get_int_settings(session, "splitter_bottom")
if record.f_int != 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 # Save current tab
record = Settings.get_int_settings(session, "active_tab") record = Settings.get_int_settings(session, "active_tab")
record.update(session, record.update(session, {"f_int": self.tabPlaylist.currentIndex()})
{'f_int': self.tabPlaylist.currentIndex()})
event.accept() event.accept()
@ -613,10 +592,8 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
# Don't close current track playlist # Don't close current track playlist
if self.tabPlaylist.widget(tab_index) == ( if self.tabPlaylist.widget(tab_index) == (self.current_track.playlist_tab):
self.current_track.playlist_tab): self.statusbar.showMessage("Can't close current track playlist", 5000)
self.statusbar.showMessage(
"Can't close current track playlist", 5000)
return False return False
# Attempt to close next track playlist # Attempt to close next track playlist
@ -643,15 +620,17 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionClosePlaylist.triggered.connect(self.close_playlist_tab) self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
self.actionDeletePlaylist.triggered.connect(self.delete_playlist) self.actionDeletePlaylist.triggered.connect(self.delete_playlist)
self.actionDownload_CSV_of_played_tracks.triggered.connect( self.actionDownload_CSV_of_played_tracks.triggered.connect(
self.download_played_tracks) self.download_played_tracks
self.actionEnable_controls.triggered.connect( )
self.enable_play_next_controls) self.actionEnable_controls.triggered.connect(self.enable_play_next_controls)
self.actionExport_playlist.triggered.connect(self.export_playlist_tab) self.actionExport_playlist.triggered.connect(self.export_playlist_tab)
self.actionFade.triggered.connect(self.fade) self.actionFade.triggered.connect(self.fade)
self.actionFind_next.triggered.connect( self.actionFind_next.triggered.connect(
lambda: self.tabPlaylist.currentWidget().search_next()) lambda: self.tabPlaylist.currentWidget().search_next()
)
self.actionFind_previous.triggered.connect( 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.actionImport.triggered.connect(self.import_track)
self.actionInsertSectionHeader.triggered.connect(self.insert_header) self.actionInsertSectionHeader.triggered.connect(self.insert_header)
self.actionInsertTrack.triggered.connect(self.insert_track) self.actionInsertTrack.triggered.connect(self.insert_track)
@ -666,13 +645,14 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionResume.triggered.connect(self.resume) self.actionResume.triggered.connect(self.resume)
self.actionSave_as_template.triggered.connect(self.save_as_template) self.actionSave_as_template.triggered.connect(self.save_as_template)
self.actionSearch_title_in_Songfacts.triggered.connect( 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( 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.actionSearch.triggered.connect(self.search_playlist)
self.actionSelect_next_track.triggered.connect(self.select_next_row) self.actionSelect_next_track.triggered.connect(self.select_next_row)
self.actionSelect_previous_track.triggered.connect( self.actionSelect_previous_track.triggered.connect(self.select_previous_row)
self.select_previous_row)
self.actionMoveUnplayed.triggered.connect(self.move_unplayed) self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
self.actionSetNext.triggered.connect(self.set_selected_track_next) self.actionSetNext.triggered.connect(self.set_selected_track_next)
self.actionSkipToNext.triggered.connect(self.play_next) self.actionSkipToNext.triggered.connect(self.play_next)
@ -691,10 +671,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick) self.timer.timeout.connect(self.tick)
def create_playlist(self, def create_playlist(
session: scoped_session, self, session: scoped_session, playlist_name: Optional[str] = None
playlist_name: Optional[str] = None) \ ) -> Optional[Playlists]:
-> Optional[Playlists]:
"""Create new playlist""" """Create new playlist"""
playlist_name = self.solicit_playlist_name() playlist_name = self.solicit_playlist_name()
@ -712,8 +691,7 @@ class Window(QMainWindow, Ui_MainWindow):
if playlist: if playlist:
self.create_playlist_tab(session, playlist) self.create_playlist_tab(session, playlist)
def create_playlist_tab(self, session: scoped_session, def create_playlist_tab(self, session: scoped_session, playlist: Playlists) -> int:
playlist: Playlists) -> int:
""" """
Take the passed playlist database object, create a playlist tab and Take the passed playlist database object, create a playlist tab and
add tab to display. Return index number of tab. add tab to display. Return index number of tab.
@ -722,8 +700,11 @@ class Window(QMainWindow, Ui_MainWindow):
assert playlist.id assert playlist.id
playlist_tab = PlaylistTab( playlist_tab = PlaylistTab(
musicmuster=self, session=session, playlist_id=playlist.id, musicmuster=self,
signals=self.signals) session=session,
playlist_id=playlist.id,
signals=self.signals,
)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx) self.tabPlaylist.setCurrentIndex(idx)
@ -737,8 +718,9 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
# Save the selected PlaylistRows items ready for a later # Save the selected PlaylistRows items ready for a later
# paste # paste
self.selected_plrs = ( self.selected_plrs = self.visible_playlist_tab().get_selected_playlistrows(
self.visible_playlist_tab().get_selected_playlistrows(session)) session
)
def debug(self): def debug(self):
"""Invoke debugger""" """Invoke debugger"""
@ -746,6 +728,7 @@ class Window(QMainWindow, Ui_MainWindow):
visible_playlist_id = self.visible_playlist_tab().playlist_id visible_playlist_id = self.visible_playlist_tab().playlist_id
print(f"Active playlist id={visible_playlist_id}") print(f"Active playlist id={visible_playlist_id}")
import ipdb # type: ignore import ipdb # type: ignore
ipdb.set_trace() ipdb.set_trace()
def delete_playlist(self) -> None: def delete_playlist(self) -> None:
@ -757,9 +740,9 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_id = self.visible_playlist_tab().playlist_id playlist_id = self.visible_playlist_tab().playlist_id
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
if playlist: if playlist:
if helpers.ask_yes_no("Delete playlist", if helpers.ask_yes_no(
f"Delete playlist '{playlist.name}': " "Delete playlist",
"Are you sure?" f"Delete playlist '{playlist.name}': " "Are you sure?",
): ):
if self.close_playlist_tab(): if self.close_playlist_tab():
playlist.delete(session) playlist.delete(session)
@ -780,9 +763,10 @@ class Window(QMainWindow, Ui_MainWindow):
start_dt = dlg.ui.dateTimeEdit.dateTime().toPyDateTime() start_dt = dlg.ui.dateTimeEdit.dateTime().toPyDateTime()
# Get output filename # Get output filename
pathspec = QFileDialog.getSaveFileName( pathspec = QFileDialog.getSaveFileName(
self, 'Save CSV of tracks played', self,
"Save CSV of tracks played",
directory="/tmp/playlist.csv", directory="/tmp/playlist.csv",
filter="CSV files (*.csv)" filter="CSV files (*.csv)",
) )
if not pathspec: if not pathspec:
return return
@ -794,9 +778,7 @@ class Window(QMainWindow, Ui_MainWindow):
with open(path, "w") as f: with open(path, "w") as f:
with Session() as session: with Session() as session:
for playdate in Playdates.played_after(session, start_dt): for playdate in Playdates.played_after(session, start_dt):
f.write( f.write(f"{playdate.track.artist},{playdate.track.title}\n")
f"{playdate.track.artist},{playdate.track.title}\n"
)
def drop3db(self) -> None: def drop3db(self) -> None:
"""Drop music level by 3db if button checked""" """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 playlist_id = self.visible_playlist_tab().playlist_id
with Session() as session: with Session() as session:
# Get output filename # Get output filename
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
if not playlist: if not playlist:
return return
pathspec = QFileDialog.getSaveFileName( pathspec = QFileDialog.getSaveFileName(
self, 'Save Playlist', self,
"Save Playlist",
directory=f"{playlist.name}.m3u", directory=f"{playlist.name}.m3u",
filter="M3U files (*.m3u);;All files (*.*)" filter="M3U files (*.m3u);;All files (*.*)",
) )
if not pathspec: if not pathspec:
return return
@ -923,10 +905,7 @@ class Window(QMainWindow, Ui_MainWindow):
times a second; this function has much better resolution. times a second; this function has much better resolution.
""" """
if ( if self.current_track.track_id is None or self.current_track.start_time is None:
self.current_track.track_id is None or
self.current_track.start_time is None
):
return 0 return 0
now = datetime.now() now = datetime.now()
@ -965,16 +944,16 @@ class Window(QMainWindow, Ui_MainWindow):
txt = "" txt = ""
tags = helpers.get_tags(fname) tags = helpers.get_tags(fname)
new_tracks.append(fname) new_tracks.append(fname)
title = tags['title'] title = tags["title"]
artist = tags['artist'] artist = tags["artist"]
count = 0 count = 0
possible_matches = Tracks.search_titles(session, title) possible_matches = Tracks.search_titles(session, title)
if possible_matches: if possible_matches:
txt += 'Similar to new track ' txt += "Similar to new track "
txt += f'"{title}" by "{artist} ({fname})":\n\n' txt += f'"{title}" by "{artist} ({fname})":\n\n'
for track in possible_matches: for track in possible_matches:
txt += f' "{track.title}" by {track.artist}' txt += f' "{track.title}" by {track.artist}'
txt += f' ({track.path})\n\n' txt += f" ({track.path})\n\n"
count += 1 count += 1
if count >= Config.MAX_IMPORT_MATCHES: if count >= Config.MAX_IMPORT_MATCHES:
txt += "\nThere are more similar-looking tracks" txt += "\nThere are more similar-looking tracks"
@ -987,7 +966,7 @@ class Window(QMainWindow, Ui_MainWindow):
"Possible duplicates", "Possible duplicates",
txt, txt,
QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.Ok,
QMessageBox.StandardButton.Cancel QMessageBox.StandardButton.Cancel,
) )
if result == QMessageBox.StandardButton.Cancel: if result == QMessageBox.StandardButton.Cancel:
return return
@ -1005,9 +984,7 @@ class Window(QMainWindow, Ui_MainWindow):
self, "Import error", "Error importing " + msg self, "Import error", "Error importing " + msg
) )
) )
self.worker.importing.connect( self.worker.importing.connect(lambda msg: self.statusbar.showMessage(msg, 5000))
lambda msg: self.statusbar.showMessage(msg, 5000)
)
self.worker.finished.connect(self.import_complete) self.worker.finished.connect(self.import_complete)
self.import_thread.start() self.import_thread.start()
@ -1057,8 +1034,9 @@ class Window(QMainWindow, Ui_MainWindow):
if record and record.f_int >= 0: if record and record.f_int >= 0:
self.tabPlaylist.setCurrentIndex(record.f_int) self.tabPlaylist.setCurrentIndex(record.f_int)
def move_playlist_rows(self, session: scoped_session, def move_playlist_rows(
playlistrows: List[PlaylistRows]) -> None: self, session: scoped_session, playlistrows: List[PlaylistRows]
) -> None:
""" """
Move passed playlist rows to another playlist Move passed playlist rows to another playlist
@ -1071,14 +1049,15 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
# Remove current/next rows from list # Remove current/next rows from list
plrs_to_move = [plr for plr in playlistrows if plrs_to_move = [
plr.id not in plr
[self.current_track.plr_id, for plr in playlistrows
self.next_track.plr_id] 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 rows_to_delete = [
if plr.plr_rownum is not None] plr.plr_rownum for plr in plrs_to_move if plr.plr_rownum is not None
]
if not rows_to_delete: if not rows_to_delete:
return return
@ -1100,8 +1079,7 @@ class Window(QMainWindow, Ui_MainWindow):
destination_playlist_id = dlg.playlist.id destination_playlist_id = dlg.playlist.id
# Update destination playlist in the database # Update destination playlist in the database
last_row = PlaylistRows.get_last_used_row(session, last_row = PlaylistRows.get_last_used_row(session, destination_playlist_id)
destination_playlist_id)
if last_row is not None: if last_row is not None:
next_row = last_row + 1 next_row = last_row + 1
else: else:
@ -1135,8 +1113,9 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
with Session() as session: with Session() as session:
selected_plrs = ( selected_plrs = self.visible_playlist_tab().get_selected_playlistrows(
self.visible_playlist_tab().get_selected_playlistrows(session)) session
)
if not selected_plrs: if not selected_plrs:
return return
@ -1155,11 +1134,9 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_id = self.visible_playlist_tab().playlist_id playlist_id = self.visible_playlist_tab().playlist_id
with Session() as session: with Session() as session:
unplayed_plrs = PlaylistRows.get_unplayed_rows( unplayed_plrs = PlaylistRows.get_unplayed_rows(session, playlist_id)
session, playlist_id) if helpers.ask_yes_no(
if helpers.ask_yes_no("Move tracks", "Move tracks", f"Move {len(unplayed_plrs)} tracks:" " Are you sure?"
f"Move {len(unplayed_plrs)} tracks:"
" Are you sure?"
): ):
self.move_playlist_rows(session, unplayed_plrs) self.move_playlist_rows(session, unplayed_plrs)
@ -1168,8 +1145,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
templates = Playlists.get_all_templates(session) templates = Playlists.get_all_templates(session)
dlg = SelectPlaylistDialog(self, playlists=templates, dlg = SelectPlaylistDialog(self, playlists=templates, session=session)
session=session)
dlg.exec() dlg.exec()
template = dlg.playlist template = dlg.playlist
if template: if template:
@ -1177,7 +1153,8 @@ class Window(QMainWindow, Ui_MainWindow):
if not playlist_name: if not playlist_name:
return return
playlist = Playlists.create_playlist_from_template( playlist = Playlists.create_playlist_from_template(
session, template, playlist_name) session, template, playlist_name
)
if not playlist: if not playlist:
return return
tab_index = self.create_playlist_tab(session, playlist) tab_index = self.create_playlist_tab(session, playlist)
@ -1188,8 +1165,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
playlists = Playlists.get_closed(session) playlists = Playlists.get_closed(session)
dlg = SelectPlaylistDialog(self, playlists=playlists, dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
session=session)
dlg.exec() dlg.exec()
playlist = dlg.playlist playlist = dlg.playlist
if playlist: if playlist:
@ -1217,8 +1193,9 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
# Create space in destination playlist # Create space in destination playlist
PlaylistRows.move_rows_down(session, dst_playlist_id, PlaylistRows.move_rows_down(
dst_row, len(self.selected_plrs)) session, dst_playlist_id, dst_row, len(self.selected_plrs)
)
session.commit() session.commit()
# Update plrs # Update plrs
@ -1240,7 +1217,8 @@ class Window(QMainWindow, Ui_MainWindow):
# Update display # Update display
self.visible_playlist_tab().populate_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 # If source playlist is not destination playlist, fixup row
# numbers and update display # numbers and update display
@ -1250,13 +1228,13 @@ class Window(QMainWindow, Ui_MainWindow):
# will be re-populated when it is opened) # will be re-populated when it is opened)
source_playlist_tab = None source_playlist_tab = None
for tab in range(self.tabPlaylist.count()): for tab in range(self.tabPlaylist.count()):
if self.tabPlaylist.widget(tab).playlist_id == \ if self.tabPlaylist.widget(tab).playlist_id == src_playlist_id:
src_playlist_id:
source_playlist_tab = self.tabPlaylist.widget(tab) source_playlist_tab = self.tabPlaylist.widget(tab)
break break
if source_playlist_tab: if source_playlist_tab:
source_playlist_tab.populate_display( 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 # Reset so rows can't be repasted
self.selected_plrs = None self.selected_plrs = None
@ -1305,8 +1283,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Set current track playlist_tab colour # Set current track playlist_tab colour
current_tab = self.current_track.playlist_tab current_tab = self.current_track.playlist_tab
if current_tab: if current_tab:
self.set_tab_colour( self.set_tab_colour(current_tab, QColor(Config.COLOUR_CURRENT_TAB))
current_tab, QColor(Config.COLOUR_CURRENT_TAB))
# Restore volume if -3dB active # Restore volume if -3dB active
if self.btnDrop3db.isChecked(): if self.btnDrop3db.isChecked():
@ -1356,8 +1333,7 @@ class Window(QMainWindow, Ui_MainWindow):
if self.btnPreview.isChecked(): if self.btnPreview.isChecked():
# Get track path for first selected track if there is one # Get track path for first selected track if there is one
track_path = ( track_path = self.visible_playlist_tab().get_selected_row_track_path()
self.visible_playlist_tab().get_selected_row_track_path())
if not track_path: if not track_path:
# Otherwise get path to next track to play # Otherwise get path to next track to play
track_path = self.next_track.path track_path = self.next_track.path
@ -1405,7 +1381,6 @@ class Window(QMainWindow, Ui_MainWindow):
if self.current_track.track_id: if self.current_track.track_id:
playing_track = self.current_track playing_track = self.current_track
with Session() as session:
# Set next plr to be track to resume # Set next plr to be track to resume
if not self.previous_track.plr_id: if not self.previous_track.plr_id:
return return
@ -1413,8 +1388,9 @@ class Window(QMainWindow, Ui_MainWindow):
return return
# Resume last track # Resume last track
self.set_next_plr_id(self.previous_track.plr_id, self.set_next_plr_id(
self.previous_track.playlist_tab) self.previous_track.plr_id, self.previous_track.playlist_tab
)
self.play_next(self.previous_track_position) self.play_next(self.previous_track_position)
# If a track was playing when we were called, get details to # If a track was playing when we were called, get details to
@ -1424,16 +1400,13 @@ class Window(QMainWindow, Ui_MainWindow):
return return
if not playing_track.playlist_tab: if not playing_track.playlist_tab:
return return
self.set_next_plr_id(playing_track.plr_id, self.set_next_plr_id(playing_track.plr_id, playing_track.playlist_tab)
playing_track.playlist_tab)
def save_as_template(self) -> None: def save_as_template(self) -> None:
"""Save current playlist as template""" """Save current playlist as template"""
with Session() as session: with Session() as session:
template_names = [ template_names = [a.name for a in Playlists.get_all_templates(session)]
a.name for a in Playlists.get_all_templates(session)
]
while True: while True:
# Get name for new template # Get name for new template
@ -1448,13 +1421,12 @@ class Window(QMainWindow, Ui_MainWindow):
template_name = dlg.textValue() template_name = dlg.textValue()
if template_name not in template_names: if template_name not in template_names:
break break
helpers.show_warning(self, helpers.show_warning(
"Duplicate template", self, "Duplicate template", "Template name already in use"
"Template name already in use" )
Playlists.save_as_template(
session, self.visible_playlist_tab().playlist_id, template_name
) )
Playlists.save_as_template(session,
self.visible_playlist_tab().playlist_id,
template_name)
helpers.show_OK(self, "Template", "Template saved") helpers.show_OK(self, "Template", "Template saved")
def search_playlist(self) -> None: def search_playlist(self) -> None:
@ -1535,8 +1507,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab) self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab)
self.tabPlaylist.currentWidget().scroll_next_to_top() self.tabPlaylist.currentWidget().scroll_next_to_top()
def solicit_playlist_name(self, def solicit_playlist_name(self, default: Optional[str] = "") -> Optional[str]:
default: Optional[str] = "") -> Optional[str]:
"""Get name of playlist from user""" """Get name of playlist from user"""
dlg = QInputDialog(self) dlg = QInputDialog(self)
@ -1581,11 +1552,13 @@ class Window(QMainWindow, Ui_MainWindow):
# Reset playlist_tab colour # Reset playlist_tab colour
if self.current_track.playlist_tab: if self.current_track.playlist_tab:
if self.current_track.playlist_tab == self.next_track.playlist_tab: if self.current_track.playlist_tab == self.next_track.playlist_tab:
self.set_tab_colour(self.current_track.playlist_tab, self.set_tab_colour(
QColor(Config.COLOUR_NEXT_TAB)) self.current_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
)
else: else:
self.set_tab_colour(self.current_track.playlist_tab, self.set_tab_colour(
QColor(Config.COLOUR_NORMAL_TAB)) self.current_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
)
# Run end-of-track actions # Run end-of-track actions
self.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) self.set_next_plr_id(selected_plr_ids[0], playlist_tab)
def set_next_plr_id(self, next_plr_id: Optional[int], def set_next_plr_id(
playlist_tab: PlaylistTab) -> None: 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 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 # Tell playlist tabs to update their 'next track' highlighting
# Args must both be ints, so use zero for no next track # Args must both be ints, so use zero for no next track
self.signals.set_next_track_signal.emit(old_next_track.plr_id, self.signals.set_next_track_signal.emit(
next_plr_id or 0) old_next_track.plr_id, next_plr_id or 0
)
# Update headers # Update headers
self.update_headers() self.update_headers()
@ -1650,12 +1625,15 @@ class Window(QMainWindow, Ui_MainWindow):
# because it isn't quick # because it isn't quick
if self.next_track.title: if self.next_track.title:
QTimer.singleShot( QTimer.singleShot(
1, lambda: self.tabInfolist.open_in_wikipedia( 1,
self.next_track.title) lambda: self.tabInfolist.open_in_wikipedia(
self.next_track.title
),
) )
def _set_next_track_playlist_tab_colours( 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 Set playlist tab colour for next track. self.next_track needs
to be set before calling. 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 # If the original next playlist tab isn't the same as the
# new one or the current track, it needs its colour reset. # new one or the current track, it needs its colour reset.
if ( if (
old_next_track and old_next_track
old_next_track.playlist_tab and and old_next_track.playlist_tab
old_next_track.playlist_tab not in [ and old_next_track.playlist_tab
self.next_track.playlist_tab, not in [self.next_track.playlist_tab, self.current_track.playlist_tab]
self.current_track.playlist_tab ):
]): self.set_tab_colour(
self.set_tab_colour(old_next_track.playlist_tab, old_next_track.playlist_tab, QColor(Config.COLOUR_NORMAL_TAB)
QColor(Config.COLOUR_NORMAL_TAB)) )
# If the new next playlist tab isn't the same as the # If the new next playlist tab isn't the same as the
# old one or the current track, it needs its colour set. # old one or the current track, it needs its colour set.
if old_next_track: if old_next_track:
@ -1679,14 +1657,14 @@ class Window(QMainWindow, Ui_MainWindow):
else: else:
old_tab = None old_tab = None
if ( if (
self.next_track and self.next_track
self.next_track.playlist_tab and and self.next_track.playlist_tab
self.next_track.playlist_tab not in [ and self.next_track.playlist_tab
old_tab, not in [old_tab, self.current_track.playlist_tab]
self.current_track.playlist_tab ):
]): self.set_tab_colour(
self.set_tab_colour(self.next_track.playlist_tab, self.next_track.playlist_tab, QColor(Config.COLOUR_NEXT_TAB)
QColor(Config.COLOUR_NEXT_TAB)) )
def tick(self) -> None: def tick(self) -> None:
""" """
@ -1713,9 +1691,9 @@ class Window(QMainWindow, Ui_MainWindow):
# Update volume fade curve # Update volume fade curve
if ( if (
self.current_track.track_id and self.current_track.track_id
self.current_track.fade_graph and and self.current_track.fade_graph
self.current_track.start_time and self.current_track.start_time
): ):
play_time = ( play_time = (
datetime.now() - self.current_track.start_time datetime.now() - self.current_track.start_time
@ -1727,8 +1705,7 @@ class Window(QMainWindow, Ui_MainWindow):
Called every 500ms Called every 500ms
""" """
self.lblTOD.setText(datetime.now().strftime( self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT))
Config.TOD_TIME_FORMAT))
# Update carts # Update carts
self.cart_tick() self.cart_tick()
@ -1748,18 +1725,26 @@ class Window(QMainWindow, Ui_MainWindow):
# player.is_playing() returning True, so assume playing if less # player.is_playing() returning True, so assume playing if less
# than Config.PLAY_SETTLE microseconds have passed since # than Config.PLAY_SETTLE microseconds have passed since
# starting play. # starting play.
if self.music.player and self.current_track.start_time and ( if (
self.music.player.is_playing() or self.music.player
(datetime.now() - self.current_track.start_time) and self.current_track.start_time
< timedelta(microseconds=Config.PLAY_SETTLE)): and (
self.music.player.is_playing()
or (datetime.now() - self.current_track.start_time)
< timedelta(microseconds=Config.PLAY_SETTLE)
)
):
playtime = self.get_playtime() playtime = self.get_playtime()
time_to_fade = (self.current_track.fade_at - playtime) time_to_fade = self.current_track.fade_at - playtime
time_to_silence = ( time_to_silence = self.current_track.silence_at - playtime
self.current_track.silence_at - playtime) time_to_end = self.current_track.duration - playtime
time_to_end = (self.current_track.duration - playtime)
# Elapsed time # Elapsed time
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) self.label_elapsed_timer.setText(
helpers.ms_to_mmss(playtime)
+ " / "
+ helpers.ms_to_mmss(self.current_track.duration)
)
# Time to fade # Time to fade
self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade)) self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))
@ -1787,12 +1772,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.frame_silent.setStyleSheet("") self.frame_silent.setStyleSheet("")
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
self.label_silent_timer.setText( self.label_silent_timer.setText(helpers.ms_to_mmss(time_to_silence))
helpers.ms_to_mmss(time_to_silence)
)
# Time to end
self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
# Autoplay next track # Autoplay next track
# if time_to_silence <= 1500: # if time_to_silence <= 1500:
@ -1834,8 +1814,9 @@ class Window(QMainWindow, Ui_MainWindow):
class CartDialog(QDialog): class CartDialog(QDialog):
"""Edit cart details""" """Edit cart details"""
def __init__(self, musicmuster: Window, session: scoped_session, def __init__(
cart: Carts, *args, **kwargs) -> None: self, musicmuster: Window, session: scoped_session, cart: Carts, *args, **kwargs
) -> None:
""" """
Manage carts Manage carts
""" """
@ -1871,8 +1852,14 @@ class CartDialog(QDialog):
class DbDialog(QDialog): class DbDialog(QDialog):
"""Select track from database""" """Select track from database"""
def __init__(self, musicmuster: Window, session: scoped_session, def __init__(
get_one_track: bool = False, *args, **kwargs) -> None: self,
musicmuster: Window,
session: scoped_session,
get_one_track: bool = False,
*args,
**kwargs,
) -> None:
""" """
Subclassed QDialog to manage track selection Subclassed QDialog to manage track selection
@ -1911,17 +1898,16 @@ class DbDialog(QDialog):
record = Settings.get_int_settings(self.session, "dbdialog_height") record = Settings.get_int_settings(self.session, "dbdialog_height")
if record.f_int != self.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") record = Settings.get_int_settings(self.session, "dbdialog_width")
if record.f_int != self.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: def add_selected(self) -> None:
"""Handle Add button""" """Handle Add button"""
if (not self.ui.matchList.selectedItems() and if not self.ui.matchList.selectedItems() and not self.ui.txtNote.text():
not self.ui.txtNote.text()):
return return
track = None track = None
@ -1946,10 +1932,12 @@ class DbDialog(QDialog):
if track: if track:
self.musicmuster.visible_playlist_tab().insert_track( self.musicmuster.visible_playlist_tab().insert_track(
self.session, track, note=self.ui.txtNote.text()) self.session, track, note=self.ui.txtNote.text()
)
else: else:
self.musicmuster.visible_playlist_tab().insert_header( self.musicmuster.visible_playlist_tab().insert_header(
self.session, note=self.ui.txtNote.text()) self.session, note=self.ui.txtNote.text()
)
# TODO: this shouldn't be needed as insert_track() saves # TODO: this shouldn't be needed as insert_track() saves
# playlist # playlist
@ -2041,11 +2029,11 @@ class SelectPlaylistDialog(QDialog):
self.session = session self.session = session
self.playlist = None self.playlist = None
record = Settings.get_int_settings( record = Settings.get_int_settings(self.session, "select_playlist_dialog_width")
self.session, "select_playlist_dialog_width")
width = record.f_int or 800 width = record.f_int or 800
record = Settings.get_int_settings( record = Settings.get_int_settings(
self.session, "select_playlist_dialog_height") self.session, "select_playlist_dialog_height"
)
height = record.f_int or 600 height = record.f_int or 600
self.resize(width, height) self.resize(width, height)
@ -2057,14 +2045,14 @@ class SelectPlaylistDialog(QDialog):
def __del__(self): # review def __del__(self): # review
record = Settings.get_int_settings( record = Settings.get_int_settings(
self.session, "select_playlist_dialog_height") self.session, "select_playlist_dialog_height"
)
if record.f_int != self.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( record = Settings.get_int_settings(self.session, "select_playlist_dialog_width")
self.session, "select_playlist_dialog_width")
if record.f_int != self.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 def list_doubleclick(self, entry): # review
self.playlist = entry.data(Qt.ItemDataRole.UserRole) self.playlist = entry.data(Qt.ItemDataRole.UserRole)
@ -2086,12 +2074,22 @@ if __name__ == "__main__":
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
# Only allow at most one option to be specified # Only allow at most one option to be specified
group = p.add_mutually_exclusive_group() group = p.add_mutually_exclusive_group()
group.add_argument('-b', '--bitrates', group.add_argument(
action="store_true", dest="update_bitrates", "-b",
default=False, help="Update bitrates in database") "--bitrates",
group.add_argument('-c', '--check-database', action="store_true",
action="store_true", dest="check_db", dest="update_bitrates",
default=False, help="Check and report on database") 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() args = p.parse_args()
# Run as required # Run as required
@ -2111,13 +2109,16 @@ if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
# PyQt6 defaults to a grey for labels # PyQt6 defaults to a grey for labels
palette = app.palette() palette = app.palette()
palette.setColor(QPalette.ColorRole.WindowText, palette.setColor(
QColor(Config.COLOUR_LABEL_TEXT)) QPalette.ColorRole.WindowText, QColor(Config.COLOUR_LABEL_TEXT)
)
# Set colours that will be used by playlist row stripes # Set colours that will be used by playlist row stripes
palette.setColor(QPalette.ColorRole.Base, palette.setColor(
QColor(Config.COLOUR_EVEN_PLAYLIST)) QPalette.ColorRole.Base, QColor(Config.COLOUR_EVEN_PLAYLIST)
palette.setColor(QPalette.ColorRole.AlternateBase, )
QColor(Config.COLOUR_ODD_PLAYLIST)) palette.setColor(
QPalette.ColorRole.AlternateBase, QColor(Config.COLOUR_ODD_PLAYLIST)
)
app.setPalette(palette) app.setPalette(palette)
win = Window() win = Window()
win.show() win.show()
@ -2129,8 +2130,12 @@ if __name__ == "__main__":
from helpers import send_mail from helpers import send_mail
msg = stackprinter.format(exc) msg = stackprinter.format(exc)
send_mail(Config.ERRORS_TO, Config.ERRORS_FROM, send_mail(
"Exception from musicmuster", msg) Config.ERRORS_TO,
Config.ERRORS_FROM,
"Exception from musicmuster",
msg,
)
print("\033[1;31;47mUnhandled exception starts") print("\033[1;31;47mUnhandled exception starts")
stackprinter.show(style="darkbg") stackprinter.show(style="darkbg")

File diff suppressed because it is too large Load Diff

View File

@ -10,11 +10,7 @@ import shutil
import sys import sys
from helpers import ( from helpers import (
fade_point,
get_audio_segment,
get_tags, get_tags,
leading_silence,
trailing_silence,
set_track_metadata, set_track_metadata,
) )
@ -29,7 +25,7 @@ process_tag_matches = True
do_processing = True do_processing = True
process_no_matches = 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) 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 only want to run this against the production database because
# we will affect files in the common pool of tracks used by all # we will affect files in the common pool of tracks used by all
# databases # 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: ") response = input("Not on production database - c to continue: ")
if response != "c": if response != "c":
sys.exit(0) sys.exit(0)
@ -68,8 +64,8 @@ def main():
artists_to_path = {} artists_to_path = {}
for k, v in parents.items(): for k, v in parents.items():
try: try:
titles_to_path[v['title'].lower()] = k titles_to_path[v["title"].lower()] = k
artists_to_path[v['artist'].lower()] = k artists_to_path[v["artist"].lower()] = k
except AttributeError: except AttributeError:
continue continue
@ -78,33 +74,31 @@ def main():
if not os.path.isfile(new_path): if not os.path.isfile(new_path):
continue continue
new_tags = get_tags(new_path) new_tags = get_tags(new_path)
new_title = new_tags['title'] new_title = new_tags["title"]
new_artist = new_tags['artist'] new_artist = new_tags["artist"]
bitrate = new_tags['bitrate'] bitrate = new_tags["bitrate"]
# If same filename exists in parent direcory, check tags # If same filename exists in parent direcory, check tags
parent_path = os.path.join(parent_dir, new_fname) parent_path = os.path.join(parent_dir, new_fname)
if os.path.exists(parent_path): if os.path.exists(parent_path):
parent_tags = get_tags(parent_path) parent_tags = get_tags(parent_path)
parent_title = parent_tags['title'] parent_title = parent_tags["title"]
parent_artist = parent_tags['artist'] parent_artist = parent_tags["artist"]
if ( if (str(parent_title).lower() == str(new_title).lower()) and (
(str(parent_title).lower() == str(new_title).lower()) and str(parent_artist).lower() == str(new_artist).lower()
(str(parent_artist).lower() == str(new_artist).lower())
): ):
name_and_tags.append( name_and_tags.append(
f" {new_fname=}, {parent_title}{new_title}, " f" {new_fname=}, {parent_title}{new_title}, "
f" {parent_artist}{new_artist}" f" {parent_artist}{new_artist}"
) )
if process_name_and_tags_matches: if process_name_and_tags_matches:
process_track(new_path, parent_path, new_title, process_track(new_path, parent_path, new_title, new_artist, bitrate)
new_artist, bitrate)
continue continue
# Check for matching tags although filename is different # Check for matching tags although filename is different
if new_title.lower() in titles_to_path: if new_title.lower() in titles_to_path:
possible_path = titles_to_path[new_title.lower()] 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( # print(
# f"title={new_title}, artist={new_artist}:\n" # f"title={new_title}, artist={new_artist}:\n"
# f" {new_path} → {parent_path}" # f" {new_path} → {parent_path}"
@ -114,8 +108,9 @@ def main():
f" {new_path}{parent_path}" f" {new_path}{parent_path}"
) )
if process_tag_matches: if process_tag_matches:
process_track(new_path, possible_path, new_title, process_track(
new_artist, bitrate) new_path, possible_path, new_title, new_artist, bitrate
)
continue continue
else: else:
no_match += 1 no_match += 1
@ -132,8 +127,8 @@ def main():
if choice: if choice:
old_file = os.path.join(parent_dir, choice[0]) old_file = os.path.join(parent_dir, choice[0])
oldtags = get_tags(old_file) oldtags = get_tags(old_file)
old_title = oldtags['title'] old_title = oldtags["title"]
old_artist = oldtags['artist'] old_artist = oldtags["artist"]
print() print()
print(f" File name will change {choice[0]}") print(f" File name will change {choice[0]}")
print(f"{new_fname}") print(f"{new_fname}")
@ -232,11 +227,8 @@ def main():
def process_track(src, dst, title, artist, bitrate): def process_track(src, dst, title, artist, bitrate):
new_path = os.path.join(os.path.dirname(dst), os.path.basename(src)) new_path = os.path.join(os.path.dirname(dst), os.path.basename(src))
print( print(f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n")
f"process_track:\n {src=}\n {dst=}\n {title=}, {artist=}\n"
)
if not do_processing: if not do_processing:
return return

View File

@ -29,7 +29,7 @@
<widget class="QWidget" name="centralwidget"> <widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout_4"> <layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0"> <item row="0" column="0">
<layout class="QHBoxLayout" name="TrackHeaderLayout"> <layout class="QHBoxLayout" name="horizontalLayout_3">
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_3">
<item> <item>
@ -290,6 +290,25 @@ padding-left: 8px;</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0">
<widget class="QWidget" name="cartsWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_Carts">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QFrame" name="frame_6"> <widget class="QFrame" name="frame_6">
<property name="minimumSize"> <property name="minimumSize">
@ -364,8 +383,6 @@ padding-left: 8px;</string>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Raised</enum> <enum>QFrame::Raised</enum>
</property> </property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QFrame" name="FadeStopInfoFrame"> <widget class="QFrame" name="FadeStopInfoFrame">
@ -387,42 +404,13 @@ padding-left: 8px;</string>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Raised</enum> <enum>QFrame::Raised</enum>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QVBoxLayout" name="verticalLayout_4">
<item row="0" column="0" colspan="2"> <item>
<widget class="QPushButton" name="btnFade">
<property name="minimumSize">
<size>
<width>132</width>
<height>36</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>164</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string> Fade</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/fade</normaloff>:/icons/fade</iconset>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QPushButton" name="btnPreview"> <widget class="QPushButton" name="btnPreview">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>132</width> <width>132</width>
<height>36</height> <height>41</height>
</size> </size>
</property> </property>
<property name="text"> <property name="text">
@ -443,43 +431,12 @@ padding-left: 8px;</string>
</property> </property>
</widget> </widget>
</item> </item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_elapsed">
<property name="minimumSize">
<size>
<width>152</width>
<height>112</height>
</size>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Elapsed time</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item> <item>
<widget class="QLabel" name="label_elapsed_timer"> <widget class="QLabel" name="label_elapsed_timer">
<property name="font"> <property name="font">
<font> <font>
<family>FreeSans</family> <family>FreeSans</family>
<pointsize>40</pointsize> <pointsize>18</pointsize>
<weight>50</weight> <weight>50</weight>
<bold>false</bold> <bold>false</bold>
</font> </font>
@ -488,7 +445,7 @@ padding-left: 8px;</string>
<string notr="true">color: black;</string> <string notr="true">color: black;</string>
</property> </property>
<property name="text"> <property name="text">
<string>00:00</string> <string>00:00 / 00:00</string>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignCenter</set> <set>Qt::AlignCenter</set>
@ -498,6 +455,74 @@ padding-left: 8px;</string>
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QFrame" name="frame_toggleplayed_3db">
<property name="minimumSize">
<size>
<width>152</width>
<height>112</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>184</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QPushButton" name="btnDrop3db">
<property name="minimumSize">
<size>
<width>132</width>
<height>41</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>164</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>-3dB to talk</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnHidePlayed">
<property name="minimumSize">
<size>
<width>132</width>
<height>41</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>164</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Hide played</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QFrame" name="frame_fade"> <widget class="QFrame" name="frame_fade">
<property name="minimumSize"> <property name="minimumSize">
@ -547,22 +572,6 @@ padding-left: 8px;</string>
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="PlotWidget" name="widgetFadeVolume" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item> <item>
<widget class="QFrame" name="frame_silent"> <widget class="QFrame" name="frame_silent">
<property name="minimumSize"> <property name="minimumSize">
@ -580,9 +589,15 @@ padding-left: 8px;</string>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Raised</enum> <enum>QFrame::Raised</enum>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>45</width>
<height>24</height>
</rect>
</property>
<property name="text"> <property name="text">
<string>Silent</string> <string>Silent</string>
</property> </property>
@ -590,9 +605,15 @@ padding-left: 8px;</string>
<set>Qt::AlignCenter</set> <set>Qt::AlignCenter</set>
</property> </property>
</widget> </widget>
</item>
<item>
<widget class="QLabel" name="label_silent_timer"> <widget class="QLabel" name="label_silent_timer">
<property name="geometry">
<rect>
<x>10</x>
<y>48</y>
<width>132</width>
<height>54</height>
</rect>
</property>
<property name="font"> <property name="font">
<font> <font>
<family>FreeSans</family> <family>FreeSans</family>
@ -608,70 +629,35 @@ padding-left: 8px;</string>
<set>Qt::AlignCenter</set> <set>Qt::AlignCenter</set>
</property> </property>
</widget> </widget>
</item>
</layout>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QFrame" name="frame_end"> <widget class="PlotWidget" name="widgetFadeVolume" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>152</width> <width>0</width>
<height>112</height> <height>0</height>
</size> </size>
</property> </property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>End</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_end_timer">
<property name="font">
<font>
<family>FreeSans</family>
<pointsize>40</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>00:00</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QFrame" name="frame_toggleplayed_3db"> <widget class="QFrame" name="frame">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>152</width> <width>151</width>
<height>112</height> <height>0</height>
</size> </size>
</property> </property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>184</width> <width>151</width>
<height>16777215</height> <height>16777215</height>
</size> </size>
</property> </property>
@ -681,74 +667,55 @@ padding-left: 8px;</string>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Raised</enum> <enum>QFrame::Raised</enum>
</property> </property>
<layout class="QGridLayout" name="gridLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_5">
<item row="0" column="0">
<widget class="QPushButton" name="btnDrop3db">
<property name="minimumSize">
<size>
<width>132</width>
<height>36</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>164</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>-3dB to talk</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="btnHidePlayed">
<property name="minimumSize">
<size>
<width>132</width>
<height>36</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>164</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Hide played</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QWidget" name="cartsWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_Carts">
<item> <item>
<spacer name="horizontalSpacer"> <widget class="QPushButton" name="btnFade">
<property name="orientation"> <property name="minimumSize">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>132</width>
<height>20</height> <height>32</height>
</size> </size>
</property> </property>
</spacer> <property name="maximumSize">
<size>
<width>164</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string> Fade</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/fade</normaloff>:/icons/fade</iconset>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnStop">
<property name="minimumSize">
<size>
<width>0</width>
<height>36</height>
</size>
</property>
<property name="text">
<string> Stop</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/stopsign</normaloff>:/icons/stopsign</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>

View File

@ -1,6 +1,6 @@
# Form implementation generated from reading ui file 'app/ui/main_window.ui' # Form implementation generated from reading ui file 'app/ui/main_window.ui'
# #
# Created by: PyQt6 UI code generator 6.5.0 # Created by: PyQt6 UI code generator 6.5.1
# #
# WARNING: Any manual changes made to this file will be lost when pyuic6 is # WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing. # run again. Do not edit this file unless you know what you are doing.
@ -22,8 +22,8 @@ class Ui_MainWindow(object):
self.centralwidget.setObjectName("centralwidget") self.centralwidget.setObjectName("centralwidget")
self.gridLayout_4 = QtWidgets.QGridLayout(self.centralwidget) self.gridLayout_4 = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout_4.setObjectName("gridLayout_4") self.gridLayout_4.setObjectName("gridLayout_4")
self.TrackHeaderLayout = QtWidgets.QHBoxLayout() self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.TrackHeaderLayout.setObjectName("TrackHeaderLayout") self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.verticalLayout_3 = QtWidgets.QVBoxLayout() self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3") self.verticalLayout_3.setObjectName("verticalLayout_3")
self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget) self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
@ -74,7 +74,7 @@ class Ui_MainWindow(object):
self.next_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) self.next_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.next_track_2.setObjectName("next_track_2") self.next_track_2.setObjectName("next_track_2")
self.verticalLayout_3.addWidget(self.next_track_2) self.verticalLayout_3.addWidget(self.next_track_2)
self.TrackHeaderLayout.addLayout(self.verticalLayout_3) self.horizontalLayout_3.addLayout(self.verticalLayout_3)
self.verticalLayout = QtWidgets.QVBoxLayout() self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setObjectName("verticalLayout")
self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget) self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget)
@ -130,7 +130,7 @@ class Ui_MainWindow(object):
self.hdrNextTrack.setFlat(True) self.hdrNextTrack.setFlat(True)
self.hdrNextTrack.setObjectName("hdrNextTrack") self.hdrNextTrack.setObjectName("hdrNextTrack")
self.verticalLayout.addWidget(self.hdrNextTrack) self.verticalLayout.addWidget(self.hdrNextTrack)
self.TrackHeaderLayout.addLayout(self.verticalLayout) self.horizontalLayout_3.addLayout(self.verticalLayout)
self.frame_2 = QtWidgets.QFrame(parent=self.centralwidget) self.frame_2 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_2.setMaximumSize(QtCore.QSize(230, 16777215)) self.frame_2.setMaximumSize(QtCore.QSize(230, 16777215))
self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
@ -146,8 +146,8 @@ class Ui_MainWindow(object):
self.lblTOD.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.lblTOD.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.lblTOD.setObjectName("lblTOD") self.lblTOD.setObjectName("lblTOD")
self.gridLayout_2.addWidget(self.lblTOD, 0, 0, 1, 1) self.gridLayout_2.addWidget(self.lblTOD, 0, 0, 1, 1)
self.TrackHeaderLayout.addWidget(self.frame_2) self.horizontalLayout_3.addWidget(self.frame_2)
self.gridLayout_4.addLayout(self.TrackHeaderLayout, 0, 0, 1, 1) self.gridLayout_4.addLayout(self.horizontalLayout_3, 0, 0, 1, 1)
self.frame_4 = QtWidgets.QFrame(parent=self.centralwidget) self.frame_4 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_4.setMinimumSize(QtCore.QSize(0, 16)) self.frame_4.setMinimumSize(QtCore.QSize(0, 16))
self.frame_4.setAutoFillBackground(False) self.frame_4.setAutoFillBackground(False)
@ -156,6 +156,13 @@ class Ui_MainWindow(object):
self.frame_4.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) self.frame_4.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_4.setObjectName("frame_4") self.frame_4.setObjectName("frame_4")
self.gridLayout_4.addWidget(self.frame_4, 1, 0, 1, 1) self.gridLayout_4.addWidget(self.frame_4, 1, 0, 1, 1)
self.cartsWidget = QtWidgets.QWidget(parent=self.centralwidget)
self.cartsWidget.setObjectName("cartsWidget")
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_Carts.addItem(spacerItem)
self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget) self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_6.setMinimumSize(QtCore.QSize(0, 16)) self.frame_6.setMinimumSize(QtCore.QSize(0, 16))
self.frame_6.setAutoFillBackground(False) self.frame_6.setAutoFillBackground(False)
@ -184,9 +191,7 @@ class Ui_MainWindow(object):
self.InfoFooterFrame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.InfoFooterFrame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.InfoFooterFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) self.InfoFooterFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.InfoFooterFrame.setObjectName("InfoFooterFrame") self.InfoFooterFrame.setObjectName("InfoFooterFrame")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.InfoFooterFrame) self.horizontalLayout = QtWidgets.QHBoxLayout(self.InfoFooterFrame)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout") self.horizontalLayout.setObjectName("horizontalLayout")
self.FadeStopInfoFrame = QtWidgets.QFrame(parent=self.InfoFooterFrame) self.FadeStopInfoFrame = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.FadeStopInfoFrame.setMinimumSize(QtCore.QSize(152, 112)) self.FadeStopInfoFrame.setMinimumSize(QtCore.QSize(152, 112))
@ -194,43 +199,21 @@ class Ui_MainWindow(object):
self.FadeStopInfoFrame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.FadeStopInfoFrame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.FadeStopInfoFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) self.FadeStopInfoFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.FadeStopInfoFrame.setObjectName("FadeStopInfoFrame") self.FadeStopInfoFrame.setObjectName("FadeStopInfoFrame")
self.gridLayout = QtWidgets.QGridLayout(self.FadeStopInfoFrame) self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.FadeStopInfoFrame)
self.gridLayout.setObjectName("gridLayout") self.verticalLayout_4.setObjectName("verticalLayout_4")
self.btnFade = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
self.btnFade.setMinimumSize(QtCore.QSize(132, 36))
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
icon1 = QtGui.QIcon()
icon1.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnFade.setIcon(icon1)
self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade")
self.gridLayout.addWidget(self.btnFade, 0, 0, 1, 2)
self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame) self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
self.btnPreview.setMinimumSize(QtCore.QSize(132, 36)) self.btnPreview.setMinimumSize(QtCore.QSize(132, 41))
icon2 = QtGui.QIcon() icon1 = QtGui.QIcon()
icon2.addPixmap(QtGui.QPixmap(":/icons/headphones"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon1.addPixmap(QtGui.QPixmap(":/icons/headphones"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnPreview.setIcon(icon2) self.btnPreview.setIcon(icon1)
self.btnPreview.setIconSize(QtCore.QSize(30, 30)) self.btnPreview.setIconSize(QtCore.QSize(30, 30))
self.btnPreview.setCheckable(True) self.btnPreview.setCheckable(True)
self.btnPreview.setObjectName("btnPreview") self.btnPreview.setObjectName("btnPreview")
self.gridLayout.addWidget(self.btnPreview, 1, 0, 1, 2) self.verticalLayout_4.addWidget(self.btnPreview)
self.horizontalLayout.addWidget(self.FadeStopInfoFrame) self.label_elapsed_timer = QtWidgets.QLabel(parent=self.FadeStopInfoFrame)
self.frame_elapsed = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_elapsed.setMinimumSize(QtCore.QSize(152, 112))
self.frame_elapsed.setStyleSheet("")
self.frame_elapsed.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_elapsed.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_elapsed.setObjectName("frame_elapsed")
self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.frame_elapsed)
self.verticalLayout_4.setObjectName("verticalLayout_4")
self.label = QtWidgets.QLabel(parent=self.frame_elapsed)
self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label.setObjectName("label")
self.verticalLayout_4.addWidget(self.label)
self.label_elapsed_timer = QtWidgets.QLabel(parent=self.frame_elapsed)
font = QtGui.QFont() font = QtGui.QFont()
font.setFamily("FreeSans") font.setFamily("FreeSans")
font.setPointSize(40) font.setPointSize(18)
font.setBold(False) font.setBold(False)
font.setWeight(50) font.setWeight(50)
self.label_elapsed_timer.setFont(font) self.label_elapsed_timer.setFont(font)
@ -238,7 +221,28 @@ class Ui_MainWindow(object):
self.label_elapsed_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.label_elapsed_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_elapsed_timer.setObjectName("label_elapsed_timer") self.label_elapsed_timer.setObjectName("label_elapsed_timer")
self.verticalLayout_4.addWidget(self.label_elapsed_timer) self.verticalLayout_4.addWidget(self.label_elapsed_timer)
self.horizontalLayout.addWidget(self.frame_elapsed) self.horizontalLayout.addWidget(self.FadeStopInfoFrame)
self.frame_toggleplayed_3db = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_toggleplayed_3db.setMinimumSize(QtCore.QSize(152, 112))
self.frame_toggleplayed_3db.setMaximumSize(QtCore.QSize(184, 16777215))
self.frame_toggleplayed_3db.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_toggleplayed_3db.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_toggleplayed_3db.setObjectName("frame_toggleplayed_3db")
self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.frame_toggleplayed_3db)
self.verticalLayout_6.setObjectName("verticalLayout_6")
self.btnDrop3db = QtWidgets.QPushButton(parent=self.frame_toggleplayed_3db)
self.btnDrop3db.setMinimumSize(QtCore.QSize(132, 41))
self.btnDrop3db.setMaximumSize(QtCore.QSize(164, 16777215))
self.btnDrop3db.setCheckable(True)
self.btnDrop3db.setObjectName("btnDrop3db")
self.verticalLayout_6.addWidget(self.btnDrop3db)
self.btnHidePlayed = QtWidgets.QPushButton(parent=self.frame_toggleplayed_3db)
self.btnHidePlayed.setMinimumSize(QtCore.QSize(132, 41))
self.btnHidePlayed.setMaximumSize(QtCore.QSize(164, 16777215))
self.btnHidePlayed.setCheckable(True)
self.btnHidePlayed.setObjectName("btnHidePlayed")
self.verticalLayout_6.addWidget(self.btnHidePlayed)
self.horizontalLayout.addWidget(self.frame_toggleplayed_3db)
self.frame_fade = QtWidgets.QFrame(parent=self.InfoFooterFrame) self.frame_fade = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_fade.setMinimumSize(QtCore.QSize(152, 112)) self.frame_fade.setMinimumSize(QtCore.QSize(152, 112))
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
@ -262,6 +266,27 @@ class Ui_MainWindow(object):
self.label_fade_timer.setObjectName("label_fade_timer") self.label_fade_timer.setObjectName("label_fade_timer")
self.verticalLayout_2.addWidget(self.label_fade_timer) self.verticalLayout_2.addWidget(self.label_fade_timer)
self.horizontalLayout.addWidget(self.frame_fade) self.horizontalLayout.addWidget(self.frame_fade)
self.frame_silent = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_silent.setMinimumSize(QtCore.QSize(152, 112))
self.frame_silent.setStyleSheet("")
self.frame_silent.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_silent.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_silent.setObjectName("frame_silent")
self.label_5 = QtWidgets.QLabel(parent=self.frame_silent)
self.label_5.setGeometry(QtCore.QRect(10, 10, 45, 24))
self.label_5.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_5.setObjectName("label_5")
self.label_silent_timer = QtWidgets.QLabel(parent=self.frame_silent)
self.label_silent_timer.setGeometry(QtCore.QRect(10, 48, 132, 54))
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(40)
font.setBold(False)
font.setWeight(50)
self.label_silent_timer.setFont(font)
self.label_silent_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_silent_timer.setObjectName("label_silent_timer")
self.horizontalLayout.addWidget(self.frame_silent)
self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame) self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(1) sizePolicy.setHorizontalStretch(1)
@ -271,82 +296,32 @@ class Ui_MainWindow(object):
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0)) self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
self.widgetFadeVolume.setObjectName("widgetFadeVolume") self.widgetFadeVolume.setObjectName("widgetFadeVolume")
self.horizontalLayout.addWidget(self.widgetFadeVolume) self.horizontalLayout.addWidget(self.widgetFadeVolume)
self.frame_silent = QtWidgets.QFrame(parent=self.InfoFooterFrame) self.frame = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_silent.setMinimumSize(QtCore.QSize(152, 112)) self.frame.setMinimumSize(QtCore.QSize(151, 0))
self.frame_silent.setStyleSheet("") self.frame.setMaximumSize(QtCore.QSize(151, 16777215))
self.frame_silent.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_silent.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_silent.setObjectName("frame_silent") self.frame.setObjectName("frame")
self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.frame_silent) self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.frame)
self.verticalLayout_5.setObjectName("verticalLayout_5") self.verticalLayout_5.setObjectName("verticalLayout_5")
self.label_5 = QtWidgets.QLabel(parent=self.frame_silent) self.btnFade = QtWidgets.QPushButton(parent=self.frame)
self.label_5.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
self.label_5.setObjectName("label_5") self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
self.verticalLayout_5.addWidget(self.label_5) icon2 = QtGui.QIcon()
self.label_silent_timer = QtWidgets.QLabel(parent=self.frame_silent) icon2.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
font = QtGui.QFont() self.btnFade.setIcon(icon2)
font.setFamily("FreeSans") self.btnFade.setIconSize(QtCore.QSize(30, 30))
font.setPointSize(40) self.btnFade.setObjectName("btnFade")
font.setBold(False) self.verticalLayout_5.addWidget(self.btnFade)
font.setWeight(50) self.btnStop = QtWidgets.QPushButton(parent=self.frame)
self.label_silent_timer.setFont(font) self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
self.label_silent_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) icon3 = QtGui.QIcon()
self.label_silent_timer.setObjectName("label_silent_timer") icon3.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.verticalLayout_5.addWidget(self.label_silent_timer) self.btnStop.setIcon(icon3)
self.horizontalLayout.addWidget(self.frame_silent) self.btnStop.setObjectName("btnStop")
self.frame_end = QtWidgets.QFrame(parent=self.InfoFooterFrame) self.verticalLayout_5.addWidget(self.btnStop)
self.frame_end.setMinimumSize(QtCore.QSize(152, 112)) self.horizontalLayout.addWidget(self.frame)
self.frame_end.setStyleSheet("")
self.frame_end.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_end.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_end.setObjectName("frame_end")
self.gridLayout_5 = QtWidgets.QGridLayout(self.frame_end)
self.gridLayout_5.setObjectName("gridLayout_5")
self.label_6 = QtWidgets.QLabel(parent=self.frame_end)
self.label_6.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_6.setObjectName("label_6")
self.gridLayout_5.addWidget(self.label_6, 0, 0, 1, 1)
self.label_end_timer = QtWidgets.QLabel(parent=self.frame_end)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(40)
font.setBold(False)
font.setWeight(50)
self.label_end_timer.setFont(font)
self.label_end_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_end_timer.setObjectName("label_end_timer")
self.gridLayout_5.addWidget(self.label_end_timer, 1, 0, 1, 1)
self.horizontalLayout.addWidget(self.frame_end)
self.frame_toggleplayed_3db = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_toggleplayed_3db.setMinimumSize(QtCore.QSize(152, 112))
self.frame_toggleplayed_3db.setMaximumSize(QtCore.QSize(184, 16777215))
self.frame_toggleplayed_3db.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_toggleplayed_3db.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_toggleplayed_3db.setObjectName("frame_toggleplayed_3db")
self.gridLayout_3 = QtWidgets.QGridLayout(self.frame_toggleplayed_3db)
self.gridLayout_3.setObjectName("gridLayout_3")
self.btnDrop3db = QtWidgets.QPushButton(parent=self.frame_toggleplayed_3db)
self.btnDrop3db.setMinimumSize(QtCore.QSize(132, 36))
self.btnDrop3db.setMaximumSize(QtCore.QSize(164, 16777215))
self.btnDrop3db.setCheckable(True)
self.btnDrop3db.setObjectName("btnDrop3db")
self.gridLayout_3.addWidget(self.btnDrop3db, 0, 0, 1, 1)
self.btnHidePlayed = QtWidgets.QPushButton(parent=self.frame_toggleplayed_3db)
self.btnHidePlayed.setMinimumSize(QtCore.QSize(132, 36))
self.btnHidePlayed.setMaximumSize(QtCore.QSize(164, 16777215))
self.btnHidePlayed.setCheckable(True)
self.btnHidePlayed.setObjectName("btnHidePlayed")
self.gridLayout_3.addWidget(self.btnHidePlayed, 1, 0, 1, 1)
self.horizontalLayout.addWidget(self.frame_toggleplayed_3db)
self.horizontalLayout_2.addLayout(self.horizontalLayout)
self.gridLayout_4.addWidget(self.InfoFooterFrame, 5, 0, 1, 1) self.gridLayout_4.addWidget(self.InfoFooterFrame, 5, 0, 1, 1)
self.cartsWidget = QtWidgets.QWidget(parent=self.centralwidget)
self.cartsWidget.setObjectName("cartsWidget")
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_Carts.addItem(spacerItem)
self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow) self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 29)) self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 29))
@ -366,41 +341,41 @@ class Ui_MainWindow(object):
self.statusbar.setObjectName("statusbar") self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar) MainWindow.setStatusBar(self.statusbar)
self.actionPlay_next = QtGui.QAction(parent=MainWindow) self.actionPlay_next = QtGui.QAction(parent=MainWindow)
icon3 = QtGui.QIcon() icon4 = QtGui.QIcon()
icon3.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon4.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionPlay_next.setIcon(icon3) self.actionPlay_next.setIcon(icon4)
self.actionPlay_next.setObjectName("actionPlay_next") self.actionPlay_next.setObjectName("actionPlay_next")
self.actionSkipToNext = QtGui.QAction(parent=MainWindow) self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
icon4 = QtGui.QIcon() icon5 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon5.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionSkipToNext.setIcon(icon4) self.actionSkipToNext.setIcon(icon5)
self.actionSkipToNext.setObjectName("actionSkipToNext") self.actionSkipToNext.setObjectName("actionSkipToNext")
self.actionInsertTrack = QtGui.QAction(parent=MainWindow) self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
icon5 = QtGui.QIcon() icon6 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionInsertTrack.setIcon(icon5) self.actionInsertTrack.setIcon(icon6)
self.actionInsertTrack.setObjectName("actionInsertTrack") self.actionInsertTrack.setObjectName("actionInsertTrack")
self.actionAdd_file = QtGui.QAction(parent=MainWindow) self.actionAdd_file = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon() icon7 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon7.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionAdd_file.setIcon(icon6) self.actionAdd_file.setIcon(icon7)
self.actionAdd_file.setObjectName("actionAdd_file") self.actionAdd_file.setObjectName("actionAdd_file")
self.actionFade = QtGui.QAction(parent=MainWindow) self.actionFade = QtGui.QAction(parent=MainWindow)
icon7 = QtGui.QIcon() icon8 = QtGui.QIcon()
icon7.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionFade.setIcon(icon7) self.actionFade.setIcon(icon8)
self.actionFade.setObjectName("actionFade") self.actionFade.setObjectName("actionFade")
self.actionStop = QtGui.QAction(parent=MainWindow) self.actionStop = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon() icon9 = QtGui.QIcon()
icon8.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon9.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionStop.setIcon(icon8) self.actionStop.setIcon(icon9)
self.actionStop.setObjectName("actionStop") self.actionStop.setObjectName("actionStop")
self.action_Clear_selection = QtGui.QAction(parent=MainWindow) self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
self.action_Clear_selection.setObjectName("action_Clear_selection") self.action_Clear_selection.setObjectName("action_Clear_selection")
self.action_Resume_previous = QtGui.QAction(parent=MainWindow) self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
icon9 = QtGui.QIcon() icon10 = QtGui.QIcon()
icon9.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon10.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.action_Resume_previous.setIcon(icon9) self.action_Resume_previous.setIcon(icon10)
self.action_Resume_previous.setObjectName("action_Resume_previous") self.action_Resume_previous.setObjectName("action_Resume_previous")
self.actionE_xit = QtGui.QAction(parent=MainWindow) self.actionE_xit = QtGui.QAction(parent=MainWindow)
self.actionE_xit.setObjectName("actionE_xit") self.actionE_xit.setObjectName("actionE_xit")
@ -540,18 +515,16 @@ class Ui_MainWindow(object):
self.current_track_2.setText(_translate("MainWindow", "Current track:")) self.current_track_2.setText(_translate("MainWindow", "Current track:"))
self.next_track_2.setText(_translate("MainWindow", "Next track:")) self.next_track_2.setText(_translate("MainWindow", "Next track:"))
self.lblTOD.setText(_translate("MainWindow", "00:00:00")) self.lblTOD.setText(_translate("MainWindow", "00:00:00"))
self.btnFade.setText(_translate("MainWindow", " Fade"))
self.btnPreview.setText(_translate("MainWindow", " Preview")) self.btnPreview.setText(_translate("MainWindow", " Preview"))
self.label.setText(_translate("MainWindow", "Elapsed time")) self.label_elapsed_timer.setText(_translate("MainWindow", "00:00 / 00:00"))
self.label_elapsed_timer.setText(_translate("MainWindow", "00:00")) self.btnDrop3db.setText(_translate("MainWindow", "-3dB to talk"))
self.btnHidePlayed.setText(_translate("MainWindow", "Hide played"))
self.label_4.setText(_translate("MainWindow", "Fade")) self.label_4.setText(_translate("MainWindow", "Fade"))
self.label_fade_timer.setText(_translate("MainWindow", "00:00")) self.label_fade_timer.setText(_translate("MainWindow", "00:00"))
self.label_5.setText(_translate("MainWindow", "Silent")) self.label_5.setText(_translate("MainWindow", "Silent"))
self.label_silent_timer.setText(_translate("MainWindow", "00:00")) self.label_silent_timer.setText(_translate("MainWindow", "00:00"))
self.label_6.setText(_translate("MainWindow", "End")) self.btnFade.setText(_translate("MainWindow", " Fade"))
self.label_end_timer.setText(_translate("MainWindow", "00:00")) self.btnStop.setText(_translate("MainWindow", " Stop"))
self.btnDrop3db.setText(_translate("MainWindow", "-3dB to talk"))
self.btnHidePlayed.setText(_translate("MainWindow", "Hide played"))
self.menuFile.setTitle(_translate("MainWindow", "&Playlists")) self.menuFile.setTitle(_translate("MainWindow", "&Playlists"))
self.menuPlaylist.setTitle(_translate("MainWindow", "Sho&wtime")) self.menuPlaylist.setTitle(_translate("MainWindow", "Sho&wtime"))
self.menuSearc_h.setTitle(_translate("MainWindow", "&Search")) self.menuSearc_h.setTitle(_translate("MainWindow", "&Search"))

View File

@ -4,13 +4,7 @@ import os
from config import Config from config import Config
from helpers import ( from helpers import (
fade_point,
get_audio_segment,
get_tags, get_tags,
leading_silence,
normalise_track,
set_track_metadata,
trailing_silence,
) )
from log import log from log import log
from models import Tracks from models import Tracks
@ -72,12 +66,14 @@ def check_db(session):
print("Invalid paths in database") print("Invalid paths in database")
print("-------------------------") print("-------------------------")
for t in paths_not_found: for t in paths_not_found:
print(f""" print(
f"""
Track ID: {t.id} Track ID: {t.id}
Path: {t.path} Path: {t.path}
Title: {t.title} Title: {t.title}
Artist: {t.artist} Artist: {t.artist}
""") """
)
if more_files_to_report: if more_files_to_report:
print("There were more paths than listed that were not found") print("There were more paths than listed that were not found")

View File

@ -1,15 +1,15 @@
# https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629 # https://itnext.io/setting-up-transactional-tests-with-pytest-and-sqlalchemy-b2d726347629
import pytest import pytest
import sys
sys.path.append("app") # Flake8 doesn't like the sys.append within imports
import models # import sys
# sys.path.append("app")
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.orm import scoped_session, sessionmaker
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def connection(): def connection():
engine = create_engine( engine = create_engine(
@ -22,6 +22,7 @@ def connection():
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def setup_database(connection): def setup_database(connection):
from app.models import Base # noqa E402 from app.models import Base # noqa E402
Base.metadata.bind = connection Base.metadata.bind = connection
Base.metadata.create_all() Base.metadata.create_all()
# seed_database() # seed_database()

1
devnotes.txt Normal file
View File

@ -0,0 +1 @@
Run Flake8 and Black

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

BIN
docs/build/doctrees/development.doctree vendored Normal file

Binary file not shown.

BIN
docs/build/doctrees/environment.pickle vendored Normal file

Binary file not shown.

BIN
docs/build/doctrees/index.doctree vendored Normal file

Binary file not shown.

BIN
docs/build/doctrees/installation.doctree vendored Normal file

Binary file not shown.

BIN
docs/build/doctrees/introduction.doctree vendored Normal file

Binary file not shown.

BIN
docs/build/doctrees/reference.doctree vendored Normal file

Binary file not shown.

BIN
docs/build/doctrees/tutorial.doctree vendored Normal file

Binary file not shown.

4
docs/build/html/.buildinfo vendored Normal file
View File

@ -0,0 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: dcac9a6ec03f5a33392ad4b5bcf42637
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@ -0,0 +1,2 @@
Development
===========

25
docs/build/html/_sources/index.rst.txt vendored Normal file
View File

@ -0,0 +1,25 @@
.. MusicMuster documentation master file, created by
sphinx-quickstart on Sun Jul 2 17:58:44 2023.
Welcome to MusicMuster's documentation!
=======================================
**MusicMuster** is a music player targeted at the production of live
internet radio shows.
Contents
--------
.. toctree::
introduction
installation
tutorial
reference
development
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,2 @@
Installation
============

View File

@ -0,0 +1,87 @@
Introduction
============
Why MusicMuster?
----------------
In January 2022 I started my show on `Mixcloud
<https://www.mixcloud.com/KeithsMusicBox/>`. Until then, my show had
been on an internet radio station which required me to use a Windows
playout system. As I only use Linux, I had to set up a Windows PC
specifically for that purpose. The system I had to use had what I felt
were shortcomings in various areas.
Once I moved to Mixcloud I searched for a Linux equivalent that didn't
have the same shortcomings but was unable to find one that met my
criteria. I decided to see how practical it would be to write my own,
and MusicMuster was born.
What is MusicMuster?
--------------------
It is a Linux-based music player. Whilst it could be used as a general
home music player, there are much better applications for that role.
**MusicMuster** has been specifically designed to support the
production of live internet radio shows.
Features
--------
* Database backed
* Can be almost entirely keyboard driven
* Open multiple playlists in tabs
* Play tracks from any playlist
* Add notes/comments to tracks on playlist
* Automatatic colour-coding of notes/comments according to content
* Preview tracks before playing to audience
* Time of day clock
* Elapsed track time counter
* Time to run until track starts to fade
* Time to run until track is silent
* Graphic of volume from 5 seconds (configurable) before fade until
track is silent
* Optionally hide played tracks in playlist
* Button to drop playout volume by 3dB for talkover
* Playlist displays:
* Title
* Artist
* Length of track (mm:ss)
* Estimated start time of track
* Estimated end time of track
* When track was last played
* 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 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 and next track in Wikipedia
* Optional search of any track in Wikipedia
* Optional search of any track in Songfacts
Requirements
------------
.. note:: MusicMuster has only been tested on Debian 12, "Bookworm";
however, it should run on most contemporary Linux systems.
The :doc:`installation` page explains how to build MusicMuster in its
own environment which will automatcally install all requirements
except the database. The current version of MusicMuster uses MariaDB
version 10.11; however, any recent version of MariaDB should suffice.
MusicMuster is a Python 3 application and requires Python 3.8 or
later.
Feedback, bugs, etc
-------------------
Please send to keith@midnighthax.com
Keith Edmunds,
July 2023

View File

@ -0,0 +1,2 @@
Reference
=========

View File

@ -0,0 +1,2 @@
Tutorial
========

703
docs/build/html/_static/alabaster.css vendored Normal file
View File

@ -0,0 +1,703 @@
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: Georgia, serif;
font-size: 17px;
background-color: #fff;
color: #000;
margin: 0;
padding: 0;
}
div.document {
width: 940px;
margin: 30px auto 0 auto;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 220px;
}
div.sphinxsidebar {
width: 220px;
font-size: 14px;
line-height: 1.5;
}
hr {
border: 1px solid #B1B4B6;
}
div.body {
background-color: #fff;
color: #3E4349;
padding: 0 30px 0 30px;
}
div.body > .section {
text-align: left;
}
div.footer {
width: 940px;
margin: 20px auto 30px auto;
font-size: 14px;
color: #888;
text-align: right;
}
div.footer a {
color: #888;
}
p.caption {
font-family: inherit;
font-size: inherit;
}
div.relations {
display: none;
}
div.sphinxsidebar a {
color: #444;
text-decoration: none;
border-bottom: 1px dotted #999;
}
div.sphinxsidebar a:hover {
border-bottom: 1px solid #999;
}
div.sphinxsidebarwrapper {
padding: 18px 10px;
}
div.sphinxsidebarwrapper p.logo {
padding: 0;
margin: -10px 0 0 0px;
text-align: center;
}
div.sphinxsidebarwrapper h1.logo {
margin-top: -10px;
text-align: center;
margin-bottom: 5px;
text-align: left;
}
div.sphinxsidebarwrapper h1.logo-name {
margin-top: 0px;
}
div.sphinxsidebarwrapper p.blurb {
margin-top: 0;
font-style: normal;
}
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: Georgia, serif;
color: #444;
font-size: 24px;
font-weight: normal;
margin: 0 0 5px 0;
padding: 0;
}
div.sphinxsidebar h4 {
font-size: 20px;
}
div.sphinxsidebar h3 a {
color: #444;
}
div.sphinxsidebar p.logo a,
div.sphinxsidebar h3 a,
div.sphinxsidebar p.logo a:hover,
div.sphinxsidebar h3 a:hover {
border: none;
}
div.sphinxsidebar p {
color: #555;
margin: 10px 0;
}
div.sphinxsidebar ul {
margin: 10px 0;
padding: 0;
color: #000;
}
div.sphinxsidebar ul li.toctree-l1 > a {
font-size: 120%;
}
div.sphinxsidebar ul li.toctree-l2 > a {
font-size: 110%;
}
div.sphinxsidebar input {
border: 1px solid #CCC;
font-family: Georgia, serif;
font-size: 1em;
}
div.sphinxsidebar hr {
border: none;
height: 1px;
color: #AAA;
background: #AAA;
text-align: left;
margin-left: 0;
width: 50%;
}
div.sphinxsidebar .badge {
border-bottom: none;
}
div.sphinxsidebar .badge:hover {
border-bottom: none;
}
/* To address an issue with donation coming after search */
div.sphinxsidebar h3.donation {
margin-top: 10px;
}
/* -- body styles ----------------------------------------------------------- */
a {
color: #004B6B;
text-decoration: underline;
}
a:hover {
color: #6D4100;
text-decoration: underline;
}
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: Georgia, serif;
font-weight: normal;
margin: 30px 0px 10px 0px;
padding: 0;
}
div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
div.body h2 { font-size: 180%; }
div.body h3 { font-size: 150%; }
div.body h4 { font-size: 130%; }
div.body h5 { font-size: 100%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: #DDD;
padding: 0 4px;
text-decoration: none;
}
a.headerlink:hover {
color: #444;
background: #EAEAEA;
}
div.body p, div.body dd, div.body li {
line-height: 1.4em;
}
div.admonition {
margin: 20px 0px;
padding: 10px 30px;
background-color: #EEE;
border: 1px solid #CCC;
}
div.admonition tt.xref, div.admonition code.xref, div.admonition a tt {
background-color: #FBFBFB;
border-bottom: 1px solid #fafafa;
}
div.admonition p.admonition-title {
font-family: Georgia, serif;
font-weight: normal;
font-size: 24px;
margin: 0 0 10px 0;
padding: 0;
line-height: 1;
}
div.admonition p.last {
margin-bottom: 0;
}
div.highlight {
background-color: #fff;
}
dt:target, .highlight {
background: #FAF3E8;
}
div.warning {
background-color: #FCC;
border: 1px solid #FAA;
}
div.danger {
background-color: #FCC;
border: 1px solid #FAA;
-moz-box-shadow: 2px 2px 4px #D52C2C;
-webkit-box-shadow: 2px 2px 4px #D52C2C;
box-shadow: 2px 2px 4px #D52C2C;
}
div.error {
background-color: #FCC;
border: 1px solid #FAA;
-moz-box-shadow: 2px 2px 4px #D52C2C;
-webkit-box-shadow: 2px 2px 4px #D52C2C;
box-shadow: 2px 2px 4px #D52C2C;
}
div.caution {
background-color: #FCC;
border: 1px solid #FAA;
}
div.attention {
background-color: #FCC;
border: 1px solid #FAA;
}
div.important {
background-color: #EEE;
border: 1px solid #CCC;
}
div.note {
background-color: #EEE;
border: 1px solid #CCC;
}
div.tip {
background-color: #EEE;
border: 1px solid #CCC;
}
div.hint {
background-color: #EEE;
border: 1px solid #CCC;
}
div.seealso {
background-color: #EEE;
border: 1px solid #CCC;
}
div.topic {
background-color: #EEE;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre, tt, code {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
font-size: 0.9em;
}
.hll {
background-color: #FFC;
margin: 0 -12px;
padding: 0 12px;
display: block;
}
img.screenshot {
}
tt.descname, tt.descclassname, code.descname, code.descclassname {
font-size: 0.95em;
}
tt.descname, code.descname {
padding-right: 0.08em;
}
img.screenshot {
-moz-box-shadow: 2px 2px 4px #EEE;
-webkit-box-shadow: 2px 2px 4px #EEE;
box-shadow: 2px 2px 4px #EEE;
}
table.docutils {
border: 1px solid #888;
-moz-box-shadow: 2px 2px 4px #EEE;
-webkit-box-shadow: 2px 2px 4px #EEE;
box-shadow: 2px 2px 4px #EEE;
}
table.docutils td, table.docutils th {
border: 1px solid #888;
padding: 0.25em 0.7em;
}
table.field-list, table.footnote {
border: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
table.footnote {
margin: 15px 0;
width: 100%;
border: 1px solid #EEE;
background: #FDFDFD;
font-size: 0.9em;
}
table.footnote + table.footnote {
margin-top: -15px;
border-top: none;
}
table.field-list th {
padding: 0 0.8em 0 0;
}
table.field-list td {
padding: 0;
}
table.field-list p {
margin-bottom: 0.8em;
}
/* Cloned from
* https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68
*/
.field-name {
-moz-hyphens: manual;
-ms-hyphens: manual;
-webkit-hyphens: manual;
hyphens: manual;
}
table.footnote td.label {
width: .1px;
padding: 0.3em 0 0.3em 0.5em;
}
table.footnote td {
padding: 0.3em 0.5em;
}
dl {
margin-left: 0;
margin-right: 0;
margin-top: 0;
padding: 0;
}
dl dd {
margin-left: 30px;
}
blockquote {
margin: 0 0 0 30px;
padding: 0;
}
ul, ol {
/* Matches the 30px from the narrow-screen "li > ul" selector below */
margin: 10px 0 10px 30px;
padding: 0;
}
pre {
background: #EEE;
padding: 7px 30px;
margin: 15px 0px;
line-height: 1.3em;
}
div.viewcode-block:target {
background: #ffd;
}
dl pre, blockquote pre, li pre {
margin-left: 0;
padding-left: 30px;
}
tt, code {
background-color: #ecf0f3;
color: #222;
/* padding: 1px 2px; */
}
tt.xref, code.xref, a tt {
background-color: #FBFBFB;
border-bottom: 1px solid #fff;
}
a.reference {
text-decoration: none;
border-bottom: 1px dotted #004B6B;
}
/* Don't put an underline on images */
a.image-reference, a.image-reference:hover {
border-bottom: none;
}
a.reference:hover {
border-bottom: 1px solid #6D4100;
}
a.footnote-reference {
text-decoration: none;
font-size: 0.7em;
vertical-align: top;
border-bottom: 1px dotted #004B6B;
}
a.footnote-reference:hover {
border-bottom: 1px solid #6D4100;
}
a:hover tt, a:hover code {
background: #EEE;
}
@media screen and (max-width: 870px) {
div.sphinxsidebar {
display: none;
}
div.document {
width: 100%;
}
div.documentwrapper {
margin-left: 0;
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
}
div.bodywrapper {
margin-top: 0;
margin-right: 0;
margin-bottom: 0;
margin-left: 0;
}
ul {
margin-left: 0;
}
li > ul {
/* Matches the 30px from the "ul, ol" selector above */
margin-left: 30px;
}
.document {
width: auto;
}
.footer {
width: auto;
}
.bodywrapper {
margin: 0;
}
.footer {
width: auto;
}
.github {
display: none;
}
}
@media screen and (max-width: 875px) {
body {
margin: 0;
padding: 20px 30px;
}
div.documentwrapper {
float: none;
background: #fff;
}
div.sphinxsidebar {
display: block;
float: none;
width: 102.5%;
margin: 50px -30px -20px -30px;
padding: 10px 20px;
background: #333;
color: #FFF;
}
div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
div.sphinxsidebar h3 a {
color: #fff;
}
div.sphinxsidebar a {
color: #AAA;
}
div.sphinxsidebar p.logo {
display: none;
}
div.document {
width: 100%;
margin: 0;
}
div.footer {
display: none;
}
div.bodywrapper {
margin: 0;
}
div.body {
min-height: 0;
padding: 0;
}
.rtd_doc_footer {
display: none;
}
.document {
width: auto;
}
.footer {
width: auto;
}
.footer {
width: auto;
}
.github {
display: none;
}
}
/* misc. */
.revsys-inline {
display: none!important;
}
/* Make nested-list/multi-paragraph items look better in Releases changelog
* pages. Without this, docutils' magical list fuckery causes inconsistent
* formatting between different release sub-lists.
*/
div#changelog > div.section > ul > li > p:only-child {
margin-bottom: 0;
}
/* Hide fugly table cell borders in ..bibliography:: directive output */
table.docutils.citation, table.docutils.citation td, table.docutils.citation th {
border: none;
/* Below needed in some edge cases; if not applied, bottom shadows appear */
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
/* relbar */
.related {
line-height: 30px;
width: 100%;
font-size: 0.9rem;
}
.related.top {
border-bottom: 1px solid #EEE;
margin-bottom: 20px;
}
.related.bottom {
border-top: 1px solid #EEE;
}
.related ul {
padding: 0;
margin: 0;
list-style: none;
}
.related li {
display: inline;
}
nav#rellinks {
float: right;
}
nav#rellinks li+li:before {
content: "|";
}
nav#breadcrumbs li+li:before {
content: "\00BB";
}
/* Hide certain items when printing */
@media print {
div.related {
display: none;
}
}

903
docs/build/html/_static/basic.css vendored Normal file
View File

@ -0,0 +1,903 @@
/*
* basic.css
* ~~~~~~~~~
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
div.section::after {
display: block;
content: '';
clear: left;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
word-wrap: break-word;
overflow-wrap : break-word;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
div.sphinxsidebar #searchbox form.search {
overflow: hidden;
}
div.sphinxsidebar #searchbox input[type="text"] {
float: left;
width: 80%;
padding: 0.25em;
box-sizing: border-box;
}
div.sphinxsidebar #searchbox input[type="submit"] {
float: left;
width: 20%;
border-left: none;
padding: 0.25em;
box-sizing: border-box;
}
img {
border: 0;
max-width: 100%;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li p.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
margin-left: auto;
margin-right: auto;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable {
width: 100%;
}
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable ul {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
table.indextable > tbody > tr > td > ul {
padding-left: 0em;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
div.modindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
div.genindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
/* -- domain module index --------------------------------------------------- */
table.modindextable td {
padding: 2px;
border-collapse: collapse;
}
/* -- general body styles --------------------------------------------------- */
div.body {
min-width: 360px;
max-width: 800px;
}
div.body p, div.body dd, div.body li, div.body blockquote {
-moz-hyphens: auto;
-ms-hyphens: auto;
-webkit-hyphens: auto;
hyphens: auto;
}
a.headerlink {
visibility: hidden;
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink,
caption:hover > a.headerlink,
p.caption:hover > a.headerlink,
div.code-block-caption:hover > a.headerlink {
visibility: visible;
}
div.body p.caption {
text-align: inherit;
}
div.body td {
text-align: left;
}
.first {
margin-top: 0 !important;
}
p.rubric {
margin-top: 30px;
font-weight: bold;
}
img.align-left, figure.align-left, .figure.align-left, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, figure.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, figure.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
img.align-default, figure.align-default, .figure.align-default {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-default {
text-align: center;
}
.align-right {
text-align: right;
}
/* -- sidebars -------------------------------------------------------------- */
div.sidebar,
aside.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px;
background-color: #ffe;
width: 40%;
float: right;
clear: right;
overflow-x: auto;
}
p.sidebar-title {
font-weight: bold;
}
nav.contents,
aside.topic,
div.admonition, div.topic, blockquote {
clear: left;
}
/* -- topics ---------------------------------------------------------------- */
nav.contents,
aside.topic,
div.topic {
border: 1px solid #ccc;
padding: 7px;
margin: 10px 0 10px 0;
}
p.topic-title {
font-size: 1.1em;
font-weight: bold;
margin-top: 10px;
}
/* -- admonitions ----------------------------------------------------------- */
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 7px;
}
div.admonition dt {
font-weight: bold;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
div.body p.centered {
text-align: center;
margin-top: 25px;
}
/* -- content of sidebars/topics/admonitions -------------------------------- */
div.sidebar > :last-child,
aside.sidebar > :last-child,
nav.contents > :last-child,
aside.topic > :last-child,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
}
div.sidebar::after,
aside.sidebar::after,
nav.contents::after,
aside.topic::after,
div.topic::after,
div.admonition::after,
blockquote::after {
display: block;
content: '';
clear: both;
}
/* -- tables ---------------------------------------------------------------- */
table.docutils {
margin-top: 10px;
margin-bottom: 10px;
border: 0;
border-collapse: collapse;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
table.align-default {
margin-left: auto;
margin-right: auto;
}
table caption span.caption-number {
font-style: italic;
}
table caption span.caption-text {
}
table.docutils td, table.docutils th {
padding: 1px 8px 1px 5px;
border-top: 0;
border-left: 0;
border-right: 0;
border-bottom: 1px solid #aaa;
}
th {
text-align: left;
padding-right: 5px;
}
table.citation {
border-left: solid 1px gray;
margin-left: 1px;
}
table.citation td {
border-bottom: none;
}
th > :first-child,
td > :first-child {
margin-top: 0px;
}
th > :last-child,
td > :last-child {
margin-bottom: 0px;
}
/* -- figures --------------------------------------------------------------- */
div.figure, figure {
margin: 0.5em;
padding: 0.5em;
}
div.figure p.caption, figcaption {
padding: 0.3em;
}
div.figure p.caption span.caption-number,
figcaption span.caption-number {
font-style: italic;
}
div.figure p.caption span.caption-text,
figcaption span.caption-text {
}
/* -- field list styles ----------------------------------------------------- */
table.field-list td, table.field-list th {
border: 0 !important;
}
.field-list ul {
margin: 0;
padding-left: 1em;
}
.field-list p {
margin: 0;
}
.field-name {
-moz-hyphens: manual;
-ms-hyphens: manual;
-webkit-hyphens: manual;
hyphens: manual;
}
/* -- hlist styles ---------------------------------------------------------- */
table.hlist {
margin: 1em 0;
}
table.hlist td {
vertical-align: top;
}
/* -- object description styles --------------------------------------------- */
.sig {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
}
.sig-name, code.descname {
background-color: transparent;
font-weight: bold;
}
.sig-name {
font-size: 1.1em;
}
code.descname {
font-size: 1.2em;
}
.sig-prename, code.descclassname {
background-color: transparent;
}
.optional {
font-size: 1.3em;
}
.sig-paren {
font-size: larger;
}
.sig-param.n {
font-style: italic;
}
/* C++ specific styling */
.sig-inline.c-texpr,
.sig-inline.cpp-texpr {
font-family: unset;
}
.sig.c .k, .sig.c .kt,
.sig.cpp .k, .sig.cpp .kt {
color: #0033B3;
}
.sig.c .m,
.sig.cpp .m {
color: #1750EB;
}
.sig.c .s, .sig.c .sc,
.sig.cpp .s, .sig.cpp .sc {
color: #067D17;
}
/* -- other body styles ----------------------------------------------------- */
ol.arabic {
list-style: decimal;
}
ol.loweralpha {
list-style: lower-alpha;
}
ol.upperalpha {
list-style: upper-alpha;
}
ol.lowerroman {
list-style: lower-roman;
}
ol.upperroman {
list-style: upper-roman;
}
:not(li) > ol > li:first-child > :first-child,
:not(li) > ul > li:first-child > :first-child {
margin-top: 0px;
}
:not(li) > ol > li:last-child > :last-child,
:not(li) > ul > li:last-child > :last-child {
margin-bottom: 0px;
}
ol.simple ol p,
ol.simple ul p,
ul.simple ol p,
ul.simple ul p {
margin-top: 0;
}
ol.simple > li:not(:first-child) > p,
ul.simple > li:not(:first-child) > p {
margin-top: 0;
}
ol.simple p,
ul.simple p {
margin-bottom: 0;
}
aside.footnote > span,
div.citation > span {
float: left;
}
aside.footnote > span:last-of-type,
div.citation > span:last-of-type {
padding-right: 0.5em;
}
aside.footnote > p {
margin-left: 2em;
}
div.citation > p {
margin-left: 4em;
}
aside.footnote > p:last-of-type,
div.citation > p:last-of-type {
margin-bottom: 0em;
}
aside.footnote > p:last-of-type:after,
div.citation > p:last-of-type:after {
content: "";
clear: both;
}
dl.field-list {
display: grid;
grid-template-columns: fit-content(30%) auto;
}
dl.field-list > dt {
font-weight: bold;
word-break: break-word;
padding-left: 0.5em;
padding-right: 5px;
}
dl.field-list > dd {
padding-left: 0.5em;
margin-top: 0em;
margin-left: 0em;
margin-bottom: 0em;
}
dl {
margin-bottom: 15px;
}
dd > :first-child {
margin-top: 0px;
}
dd ul, dd table {
margin-bottom: 10px;
}
dd {
margin-top: 3px;
margin-bottom: 10px;
margin-left: 30px;
}
dl > dd:last-child,
dl > dd:last-child > :last-child {
margin-bottom: 0;
}
dt:target, span.highlighted {
background-color: #fbe54e;
}
rect.highlighted {
fill: #fbe54e;
}
dl.glossary dt {
font-weight: bold;
font-size: 1.1em;
}
.versionmodified {
font-style: italic;
}
.system-message {
background-color: #fda;
padding: 5px;
border: 3px solid red;
}
.footnote:target {
background-color: #ffa;
}
.line-block {
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
.line-block .line-block {
margin-top: 0;
margin-bottom: 0;
margin-left: 1.5em;
}
.guilabel, .menuselection {
font-family: sans-serif;
}
.accelerator {
text-decoration: underline;
}
.classifier {
font-style: oblique;
}
.classifier:before {
font-style: normal;
margin: 0 0.5em;
content: ":";
display: inline-block;
}
abbr, acronym {
border-bottom: dotted 1px;
cursor: help;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
overflow-y: hidden; /* fixes display issues on Chrome browsers */
}
pre, div[class*="highlight-"] {
clear: both;
}
span.pre {
-moz-hyphens: none;
-ms-hyphens: none;
-webkit-hyphens: none;
hyphens: none;
white-space: nowrap;
}
div[class*="highlight-"] {
margin: 1em 0;
}
td.linenos pre {
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
display: block;
}
table.highlighttable tbody {
display: block;
}
table.highlighttable tr {
display: flex;
}
table.highlighttable td {
margin: 0;
padding: 0;
}
table.highlighttable td.linenos {
padding-right: 0.5em;
}
table.highlighttable td.code {
flex: 1;
overflow: hidden;
}
.highlight .hll {
display: block;
}
div.highlight pre,
table.highlighttable pre {
margin: 0;
}
div.code-block-caption + div {
margin-top: 0;
}
div.code-block-caption {
margin-top: 1em;
padding: 2px 5px;
font-size: small;
}
div.code-block-caption code {
background-color: transparent;
}
table.highlighttable td.linenos,
span.linenos,
div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
-webkit-user-select: text; /* Safari fallback only */
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+ */
}
div.code-block-caption span.caption-number {
padding: 0.1em 0.3em;
font-style: italic;
}
div.code-block-caption span.caption-text {
}
div.literal-block-wrapper {
margin: 1em 0;
}
code.xref, a code {
background-color: transparent;
font-weight: bold;
}
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
background-color: transparent;
}
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family: sans-serif;
}
div.viewcode-block:target {
margin: -1px -10px;
padding: 0 10px;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
span.eqno a.headerlink {
position: absolute;
z-index: 1;
}
div.math:hover a.headerlink {
visibility: visible;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}

1
docs/build/html/_static/custom.css vendored Normal file
View File

@ -0,0 +1 @@
/* This file intentionally left blank. */

69
docs/build/html/_static/debug.css vendored Normal file
View File

@ -0,0 +1,69 @@
/*
This CSS file should be overridden by the theme authors. It's
meant for debugging and developing the skeleton that this theme provides.
*/
body {
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
background: lavender;
}
.sb-announcement {
background: rgb(131, 131, 131);
}
.sb-announcement__inner {
background: black;
color: white;
}
.sb-header {
background: lightskyblue;
}
.sb-header__inner {
background: royalblue;
color: white;
}
.sb-header-secondary {
background: lightcyan;
}
.sb-header-secondary__inner {
background: cornflowerblue;
color: white;
}
.sb-sidebar-primary {
background: lightgreen;
}
.sb-main {
background: blanchedalmond;
}
.sb-main__inner {
background: antiquewhite;
}
.sb-header-article {
background: lightsteelblue;
}
.sb-article-container {
background: snow;
}
.sb-article-main {
background: white;
}
.sb-footer-article {
background: lightpink;
}
.sb-sidebar-secondary {
background: lightgoldenrodyellow;
}
.sb-footer-content {
background: plum;
}
.sb-footer-content__inner {
background: palevioletred;
}
.sb-footer {
background: pink;
}
.sb-footer__inner {
background: salmon;
}
.sb-article {
background: white;
}

156
docs/build/html/_static/doctools.js vendored Normal file
View File

@ -0,0 +1,156 @@
/*
* doctools.js
* ~~~~~~~~~~~
*
* Base JavaScript utilities for all Sphinx HTML documentation.
*
* :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
"use strict";
const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([
"TEXTAREA",
"INPUT",
"SELECT",
"BUTTON",
]);
const _ready = (callback) => {
if (document.readyState !== "loading") {
callback();
} else {
document.addEventListener("DOMContentLoaded", callback);
}
};
/**
* Small JavaScript module for the documentation.
*/
const Documentation = {
init: () => {
Documentation.initDomainIndexTable();
Documentation.initOnKeyListeners();
},
/**
* i18n support
*/
TRANSLATIONS: {},
PLURAL_EXPR: (n) => (n === 1 ? 0 : 1),
LOCALE: "unknown",
// gettext and ngettext don't access this so that the functions
// can safely bound to a different name (_ = Documentation.gettext)
gettext: (string) => {
const translated = Documentation.TRANSLATIONS[string];
switch (typeof translated) {
case "undefined":
return string; // no translation
case "string":
return translated; // translation exists
default:
return translated[0]; // (singular, plural) translation tuple exists
}
},
ngettext: (singular, plural, n) => {
const translated = Documentation.TRANSLATIONS[singular];
if (typeof translated !== "undefined")
return translated[Documentation.PLURAL_EXPR(n)];
return n === 1 ? singular : plural;
},
addTranslations: (catalog) => {
Object.assign(Documentation.TRANSLATIONS, catalog.messages);
Documentation.PLURAL_EXPR = new Function(
"n",
`return (${catalog.plural_expr})`
);
Documentation.LOCALE = catalog.locale;
},
/**
* helper function to focus on search bar
*/
focusSearchBar: () => {
document.querySelectorAll("input[name=q]")[0]?.focus();
},
/**
* Initialise the domain index toggle buttons
*/
initDomainIndexTable: () => {
const toggler = (el) => {
const idNumber = el.id.substr(7);
const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`);
if (el.src.substr(-9) === "minus.png") {
el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`;
toggledRows.forEach((el) => (el.style.display = "none"));
} else {
el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`;
toggledRows.forEach((el) => (el.style.display = ""));
}
};
const togglerElements = document.querySelectorAll("img.toggler");
togglerElements.forEach((el) =>
el.addEventListener("click", (event) => toggler(event.currentTarget))
);
togglerElements.forEach((el) => (el.style.display = ""));
if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler);
},
initOnKeyListeners: () => {
// only install a listener if it is really needed
if (
!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS &&
!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS
)
return;
document.addEventListener("keydown", (event) => {
// bail for input elements
if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
// bail with special keys
if (event.altKey || event.ctrlKey || event.metaKey) return;
if (!event.shiftKey) {
switch (event.key) {
case "ArrowLeft":
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
const prevLink = document.querySelector('link[rel="prev"]');
if (prevLink && prevLink.href) {
window.location.href = prevLink.href;
event.preventDefault();
}
break;
case "ArrowRight":
if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
const nextLink = document.querySelector('link[rel="next"]');
if (nextLink && nextLink.href) {
window.location.href = nextLink.href;
event.preventDefault();
}
break;
}
}
// some keyboard layouts may need Shift to get /
switch (event.key) {
case "/":
if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;
Documentation.focusSearchBar();
event.preventDefault();
}
});
},
};
// quick alias for translations
const _ = Documentation.gettext;
_ready(Documentation.init);

View File

@ -0,0 +1,14 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '2.11.2',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
FILE_SUFFIX: '.html',
LINK_SUFFIX: '.html',
HAS_SOURCE: true,
SOURCELINK_SUFFIX: '.txt',
NAVIGATION_WITH_KEYS: false,
SHOW_SEARCH_SUMMARY: true,
ENABLE_SEARCH_SHORTCUTS: true,
};

BIN
docs/build/html/_static/file.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

199
docs/build/html/_static/language_data.js vendored Normal file
View File

@ -0,0 +1,199 @@
/*
* language_data.js
* ~~~~~~~~~~~~~~~~
*
* This script contains the language-specific data used by searchtools.js,
* namely the list of stopwords, stemmer, scorer and splitter.
*
* :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"];
/* Non-minified version is copied as a separate JS file, is available */
/**
* Porter Stemmer
*/
var Stemmer = function() {
var step2list = {
ational: 'ate',
tional: 'tion',
enci: 'ence',
anci: 'ance',
izer: 'ize',
bli: 'ble',
alli: 'al',
entli: 'ent',
eli: 'e',
ousli: 'ous',
ization: 'ize',
ation: 'ate',
ator: 'ate',
alism: 'al',
iveness: 'ive',
fulness: 'ful',
ousness: 'ous',
aliti: 'al',
iviti: 'ive',
biliti: 'ble',
logi: 'log'
};
var step3list = {
icate: 'ic',
ative: '',
alize: 'al',
iciti: 'ic',
ical: 'ic',
ful: '',
ness: ''
};
var c = "[^aeiou]"; // consonant
var v = "[aeiouy]"; // vowel
var C = c + "[^aeiouy]*"; // consonant sequence
var V = v + "[aeiou]*"; // vowel sequence
var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
var s_v = "^(" + C + ")?" + v; // vowel in stem
this.stemWord = function (w) {
var stem;
var suffix;
var firstch;
var origword = w;
if (w.length < 3)
return w;
var re;
var re2;
var re3;
var re4;
firstch = w.substr(0,1);
if (firstch == "y")
w = firstch.toUpperCase() + w.substr(1);
// Step 1a
re = /^(.+?)(ss|i)es$/;
re2 = /^(.+?)([^s])s$/;
if (re.test(w))
w = w.replace(re,"$1$2");
else if (re2.test(w))
w = w.replace(re2,"$1$2");
// Step 1b
re = /^(.+?)eed$/;
re2 = /^(.+?)(ed|ing)$/;
if (re.test(w)) {
var fp = re.exec(w);
re = new RegExp(mgr0);
if (re.test(fp[1])) {
re = /.$/;
w = w.replace(re,"");
}
}
else if (re2.test(w)) {
var fp = re2.exec(w);
stem = fp[1];
re2 = new RegExp(s_v);
if (re2.test(stem)) {
w = stem;
re2 = /(at|bl|iz)$/;
re3 = new RegExp("([^aeiouylsz])\\1$");
re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
if (re2.test(w))
w = w + "e";
else if (re3.test(w)) {
re = /.$/;
w = w.replace(re,"");
}
else if (re4.test(w))
w = w + "e";
}
}
// Step 1c
re = /^(.+?)y$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
re = new RegExp(s_v);
if (re.test(stem))
w = stem + "i";
}
// Step 2
re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
suffix = fp[2];
re = new RegExp(mgr0);
if (re.test(stem))
w = stem + step2list[suffix];
}
// Step 3
re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
suffix = fp[2];
re = new RegExp(mgr0);
if (re.test(stem))
w = stem + step3list[suffix];
}
// Step 4
re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
re2 = /^(.+?)(s|t)(ion)$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
re = new RegExp(mgr1);
if (re.test(stem))
w = stem;
}
else if (re2.test(w)) {
var fp = re2.exec(w);
stem = fp[1] + fp[2];
re2 = new RegExp(mgr1);
if (re2.test(stem))
w = stem;
}
// Step 5
re = /^(.+?)e$/;
if (re.test(w)) {
var fp = re.exec(w);
stem = fp[1];
re = new RegExp(mgr1);
re2 = new RegExp(meq1);
re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
w = stem;
}
re = /ll$/;
re2 = new RegExp(mgr1);
if (re.test(w) && re2.test(w)) {
re = /.$/;
w = w.replace(re,"");
}
// and turn initial Y back to y
if (firstch == "y")
w = firstch.toLowerCase() + w.substr(1);
return w;
}
}

BIN
docs/build/html/_static/minus.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

BIN
docs/build/html/_static/plus.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

255
docs/build/html/_static/pygments.css vendored Normal file
View File

@ -0,0 +1,255 @@
.highlight pre { line-height: 125%; }
.highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
.highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
.highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight { background: #f8f8f8; }
.highlight .c { color: #8f5902; font-style: italic } /* Comment */
.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */
.highlight .g { color: #000000 } /* Generic */
.highlight .k { color: #204a87; font-weight: bold } /* Keyword */
.highlight .l { color: #000000 } /* Literal */
.highlight .n { color: #000000 } /* Name */
.highlight .o { color: #ce5c00; font-weight: bold } /* Operator */
.highlight .x { color: #000000 } /* Other */
.highlight .p { color: #000000; font-weight: bold } /* Punctuation */
.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */
.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */
.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */
.highlight .gd { color: #a40000 } /* Generic.Deleted */
.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */
.highlight .gr { color: #ef2929 } /* Generic.Error */
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.highlight .gi { color: #00A000 } /* Generic.Inserted */
.highlight .go { color: #000000; font-style: italic } /* Generic.Output */
.highlight .gp { color: #8f5902 } /* Generic.Prompt */
.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */
.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */
.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */
.highlight .ld { color: #000000 } /* Literal.Date */
.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */
.highlight .s { color: #4e9a06 } /* Literal.String */
.highlight .na { color: #c4a000 } /* Name.Attribute */
.highlight .nb { color: #204a87 } /* Name.Builtin */
.highlight .nc { color: #000000 } /* Name.Class */
.highlight .no { color: #000000 } /* Name.Constant */
.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */
.highlight .ni { color: #ce5c00 } /* Name.Entity */
.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #000000 } /* Name.Function */
.highlight .nl { color: #f57900 } /* Name.Label */
.highlight .nn { color: #000000 } /* Name.Namespace */
.highlight .nx { color: #000000 } /* Name.Other */
.highlight .py { color: #000000 } /* Name.Property */
.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #000000 } /* Name.Variable */
.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */
.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */
.highlight .w { color: #f8f8f8 } /* Text.Whitespace */
.highlight .mb { color: #0000cf; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */
.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */
.highlight .sc { color: #4e9a06 } /* Literal.String.Char */
.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */
.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */
.highlight .se { color: #4e9a06 } /* Literal.String.Escape */
.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */
.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */
.highlight .sx { color: #4e9a06 } /* Literal.String.Other */
.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */
.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */
.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */
.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #000000 } /* Name.Function.Magic */
.highlight .vc { color: #000000 } /* Name.Variable.Class */
.highlight .vg { color: #000000 } /* Name.Variable.Global */
.highlight .vi { color: #000000 } /* Name.Variable.Instance */
.highlight .vm { color: #000000 } /* Name.Variable.Magic */
.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */
@media not print {
body[data-theme="dark"] .highlight pre { line-height: 125%; }
body[data-theme="dark"] .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
body[data-theme="dark"] .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
body[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
body[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
body[data-theme="dark"] .highlight .hll { background-color: #404040 }
body[data-theme="dark"] .highlight { background: #202020; color: #d0d0d0 }
body[data-theme="dark"] .highlight .c { color: #ababab; font-style: italic } /* Comment */
body[data-theme="dark"] .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
body[data-theme="dark"] .highlight .esc { color: #d0d0d0 } /* Escape */
body[data-theme="dark"] .highlight .g { color: #d0d0d0 } /* Generic */
body[data-theme="dark"] .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */
body[data-theme="dark"] .highlight .l { color: #d0d0d0 } /* Literal */
body[data-theme="dark"] .highlight .n { color: #d0d0d0 } /* Name */
body[data-theme="dark"] .highlight .o { color: #d0d0d0 } /* Operator */
body[data-theme="dark"] .highlight .x { color: #d0d0d0 } /* Other */
body[data-theme="dark"] .highlight .p { color: #d0d0d0 } /* Punctuation */
body[data-theme="dark"] .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
body[data-theme="dark"] .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
body[data-theme="dark"] .highlight .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
body[data-theme="dark"] .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
body[data-theme="dark"] .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */
body[data-theme="dark"] .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
body[data-theme="dark"] .highlight .gd { color: #d22323 } /* Generic.Deleted */
body[data-theme="dark"] .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
body[data-theme="dark"] .highlight .gr { color: #d22323 } /* Generic.Error */
body[data-theme="dark"] .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
body[data-theme="dark"] .highlight .gi { color: #589819 } /* Generic.Inserted */
body[data-theme="dark"] .highlight .go { color: #cccccc } /* Generic.Output */
body[data-theme="dark"] .highlight .gp { color: #aaaaaa } /* Generic.Prompt */
body[data-theme="dark"] .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
body[data-theme="dark"] .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
body[data-theme="dark"] .highlight .gt { color: #d22323 } /* Generic.Traceback */
body[data-theme="dark"] .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
body[data-theme="dark"] .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
body[data-theme="dark"] .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
body[data-theme="dark"] .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */
body[data-theme="dark"] .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
body[data-theme="dark"] .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
body[data-theme="dark"] .highlight .ld { color: #d0d0d0 } /* Literal.Date */
body[data-theme="dark"] .highlight .m { color: #51b2fd } /* Literal.Number */
body[data-theme="dark"] .highlight .s { color: #ed9d13 } /* Literal.String */
body[data-theme="dark"] .highlight .na { color: #bbbbbb } /* Name.Attribute */
body[data-theme="dark"] .highlight .nb { color: #2fbccd } /* Name.Builtin */
body[data-theme="dark"] .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */
body[data-theme="dark"] .highlight .no { color: #40ffff } /* Name.Constant */
body[data-theme="dark"] .highlight .nd { color: #ffa500 } /* Name.Decorator */
body[data-theme="dark"] .highlight .ni { color: #d0d0d0 } /* Name.Entity */
body[data-theme="dark"] .highlight .ne { color: #bbbbbb } /* Name.Exception */
body[data-theme="dark"] .highlight .nf { color: #71adff } /* Name.Function */
body[data-theme="dark"] .highlight .nl { color: #d0d0d0 } /* Name.Label */
body[data-theme="dark"] .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
body[data-theme="dark"] .highlight .nx { color: #d0d0d0 } /* Name.Other */
body[data-theme="dark"] .highlight .py { color: #d0d0d0 } /* Name.Property */
body[data-theme="dark"] .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
body[data-theme="dark"] .highlight .nv { color: #40ffff } /* Name.Variable */
body[data-theme="dark"] .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
body[data-theme="dark"] .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */
body[data-theme="dark"] .highlight .w { color: #666666 } /* Text.Whitespace */
body[data-theme="dark"] .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */
body[data-theme="dark"] .highlight .mf { color: #51b2fd } /* Literal.Number.Float */
body[data-theme="dark"] .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */
body[data-theme="dark"] .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */
body[data-theme="dark"] .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */
body[data-theme="dark"] .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */
body[data-theme="dark"] .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */
body[data-theme="dark"] .highlight .sc { color: #ed9d13 } /* Literal.String.Char */
body[data-theme="dark"] .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */
body[data-theme="dark"] .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */
body[data-theme="dark"] .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */
body[data-theme="dark"] .highlight .se { color: #ed9d13 } /* Literal.String.Escape */
body[data-theme="dark"] .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */
body[data-theme="dark"] .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */
body[data-theme="dark"] .highlight .sx { color: #ffa500 } /* Literal.String.Other */
body[data-theme="dark"] .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */
body[data-theme="dark"] .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */
body[data-theme="dark"] .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */
body[data-theme="dark"] .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
body[data-theme="dark"] .highlight .fm { color: #71adff } /* Name.Function.Magic */
body[data-theme="dark"] .highlight .vc { color: #40ffff } /* Name.Variable.Class */
body[data-theme="dark"] .highlight .vg { color: #40ffff } /* Name.Variable.Global */
body[data-theme="dark"] .highlight .vi { color: #40ffff } /* Name.Variable.Instance */
body[data-theme="dark"] .highlight .vm { color: #40ffff } /* Name.Variable.Magic */
body[data-theme="dark"] .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) .highlight pre { line-height: 125%; }
body:not([data-theme="light"]) .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
body:not([data-theme="light"]) .highlight span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
body:not([data-theme="light"]) .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
body:not([data-theme="light"]) .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
body:not([data-theme="light"]) .highlight .hll { background-color: #404040 }
body:not([data-theme="light"]) .highlight { background: #202020; color: #d0d0d0 }
body:not([data-theme="light"]) .highlight .c { color: #ababab; font-style: italic } /* Comment */
body:not([data-theme="light"]) .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
body:not([data-theme="light"]) .highlight .esc { color: #d0d0d0 } /* Escape */
body:not([data-theme="light"]) .highlight .g { color: #d0d0d0 } /* Generic */
body:not([data-theme="light"]) .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */
body:not([data-theme="light"]) .highlight .l { color: #d0d0d0 } /* Literal */
body:not([data-theme="light"]) .highlight .n { color: #d0d0d0 } /* Name */
body:not([data-theme="light"]) .highlight .o { color: #d0d0d0 } /* Operator */
body:not([data-theme="light"]) .highlight .x { color: #d0d0d0 } /* Other */
body:not([data-theme="light"]) .highlight .p { color: #d0d0d0 } /* Punctuation */
body:not([data-theme="light"]) .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
body:not([data-theme="light"]) .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
body:not([data-theme="light"]) .highlight .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
body:not([data-theme="light"]) .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
body:not([data-theme="light"]) .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */
body:not([data-theme="light"]) .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
body:not([data-theme="light"]) .highlight .gd { color: #d22323 } /* Generic.Deleted */
body:not([data-theme="light"]) .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
body:not([data-theme="light"]) .highlight .gr { color: #d22323 } /* Generic.Error */
body:not([data-theme="light"]) .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
body:not([data-theme="light"]) .highlight .gi { color: #589819 } /* Generic.Inserted */
body:not([data-theme="light"]) .highlight .go { color: #cccccc } /* Generic.Output */
body:not([data-theme="light"]) .highlight .gp { color: #aaaaaa } /* Generic.Prompt */
body:not([data-theme="light"]) .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
body:not([data-theme="light"]) .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
body:not([data-theme="light"]) .highlight .gt { color: #d22323 } /* Generic.Traceback */
body:not([data-theme="light"]) .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
body:not([data-theme="light"]) .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
body:not([data-theme="light"]) .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
body:not([data-theme="light"]) .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */
body:not([data-theme="light"]) .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
body:not([data-theme="light"]) .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
body:not([data-theme="light"]) .highlight .ld { color: #d0d0d0 } /* Literal.Date */
body:not([data-theme="light"]) .highlight .m { color: #51b2fd } /* Literal.Number */
body:not([data-theme="light"]) .highlight .s { color: #ed9d13 } /* Literal.String */
body:not([data-theme="light"]) .highlight .na { color: #bbbbbb } /* Name.Attribute */
body:not([data-theme="light"]) .highlight .nb { color: #2fbccd } /* Name.Builtin */
body:not([data-theme="light"]) .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */
body:not([data-theme="light"]) .highlight .no { color: #40ffff } /* Name.Constant */
body:not([data-theme="light"]) .highlight .nd { color: #ffa500 } /* Name.Decorator */
body:not([data-theme="light"]) .highlight .ni { color: #d0d0d0 } /* Name.Entity */
body:not([data-theme="light"]) .highlight .ne { color: #bbbbbb } /* Name.Exception */
body:not([data-theme="light"]) .highlight .nf { color: #71adff } /* Name.Function */
body:not([data-theme="light"]) .highlight .nl { color: #d0d0d0 } /* Name.Label */
body:not([data-theme="light"]) .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
body:not([data-theme="light"]) .highlight .nx { color: #d0d0d0 } /* Name.Other */
body:not([data-theme="light"]) .highlight .py { color: #d0d0d0 } /* Name.Property */
body:not([data-theme="light"]) .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
body:not([data-theme="light"]) .highlight .nv { color: #40ffff } /* Name.Variable */
body:not([data-theme="light"]) .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
body:not([data-theme="light"]) .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */
body:not([data-theme="light"]) .highlight .w { color: #666666 } /* Text.Whitespace */
body:not([data-theme="light"]) .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */
body:not([data-theme="light"]) .highlight .mf { color: #51b2fd } /* Literal.Number.Float */
body:not([data-theme="light"]) .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */
body:not([data-theme="light"]) .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */
body:not([data-theme="light"]) .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */
body:not([data-theme="light"]) .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */
body:not([data-theme="light"]) .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */
body:not([data-theme="light"]) .highlight .sc { color: #ed9d13 } /* Literal.String.Char */
body:not([data-theme="light"]) .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */
body:not([data-theme="light"]) .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */
body:not([data-theme="light"]) .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */
body:not([data-theme="light"]) .highlight .se { color: #ed9d13 } /* Literal.String.Escape */
body:not([data-theme="light"]) .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */
body:not([data-theme="light"]) .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */
body:not([data-theme="light"]) .highlight .sx { color: #ffa500 } /* Literal.String.Other */
body:not([data-theme="light"]) .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */
body:not([data-theme="light"]) .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */
body:not([data-theme="light"]) .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */
body:not([data-theme="light"]) .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
body:not([data-theme="light"]) .highlight .fm { color: #71adff } /* Name.Function.Magic */
body:not([data-theme="light"]) .highlight .vc { color: #40ffff } /* Name.Variable.Class */
body:not([data-theme="light"]) .highlight .vg { color: #40ffff } /* Name.Variable.Global */
body:not([data-theme="light"]) .highlight .vi { color: #40ffff } /* Name.Variable.Instance */
body:not([data-theme="light"]) .highlight .vm { color: #40ffff } /* Name.Variable.Magic */
body:not([data-theme="light"]) .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */
}
}

View File

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
/*!
* gumshoejs v5.1.2 (patched by @pradyunsg)
* A simple, framework-agnostic scrollspy script.
* (c) 2019 Chris Ferdinandi
* MIT License
* http://github.com/cferdinandi/gumshoe
*/

File diff suppressed because one or more lines are too long

566
docs/build/html/_static/searchtools.js vendored Normal file
View File

@ -0,0 +1,566 @@
/*
* searchtools.js
* ~~~~~~~~~~~~~~~~
*
* Sphinx JavaScript utilities for the full-text search.
*
* :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
"use strict";
/**
* Simple result scoring code.
*/
if (typeof Scorer === "undefined") {
var Scorer = {
// Implement the following function to further tweak the score for each result
// The function takes a result array [docname, title, anchor, descr, score, filename]
// and returns the new score.
/*
score: result => {
const [docname, title, anchor, descr, score, filename] = result
return score
},
*/
// query matches the full name of an object
objNameMatch: 11,
// or matches in the last dotted part of the object name
objPartialMatch: 6,
// Additive scores depending on the priority of the object
objPrio: {
0: 15, // used to be importantResults
1: 5, // used to be objectResults
2: -5, // used to be unimportantResults
},
// Used when the priority is not in the mapping.
objPrioDefault: 0,
// query found in title
title: 15,
partialTitle: 7,
// query found in terms
term: 5,
partialTerm: 2,
};
}
const _removeChildren = (element) => {
while (element && element.lastChild) element.removeChild(element.lastChild);
};
/**
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
*/
const _escapeRegExp = (string) =>
string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
const _displayItem = (item, searchTerms) => {
const docBuilder = DOCUMENTATION_OPTIONS.BUILDER;
const docUrlRoot = DOCUMENTATION_OPTIONS.URL_ROOT;
const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX;
const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX;
const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY;
const [docName, title, anchor, descr, score, _filename] = item;
let listItem = document.createElement("li");
let requestUrl;
let linkUrl;
if (docBuilder === "dirhtml") {
// dirhtml builder
let dirname = docName + "/";
if (dirname.match(/\/index\/$/))
dirname = dirname.substring(0, dirname.length - 6);
else if (dirname === "index/") dirname = "";
requestUrl = docUrlRoot + dirname;
linkUrl = requestUrl;
} else {
// normal html builders
requestUrl = docUrlRoot + docName + docFileSuffix;
linkUrl = docName + docLinkSuffix;
}
let linkEl = listItem.appendChild(document.createElement("a"));
linkEl.href = linkUrl + anchor;
linkEl.dataset.score = score;
linkEl.innerHTML = title;
if (descr)
listItem.appendChild(document.createElement("span")).innerHTML =
" (" + descr + ")";
else if (showSearchSummary)
fetch(requestUrl)
.then((responseData) => responseData.text())
.then((data) => {
if (data)
listItem.appendChild(
Search.makeSearchSummary(data, searchTerms)
);
});
Search.output.appendChild(listItem);
};
const _finishSearch = (resultCount) => {
Search.stopPulse();
Search.title.innerText = _("Search Results");
if (!resultCount)
Search.status.innerText = Documentation.gettext(
"Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories."
);
else
Search.status.innerText = _(
`Search finished, found ${resultCount} page(s) matching the search query.`
);
};
const _displayNextItem = (
results,
resultCount,
searchTerms
) => {
// results left, load the summary and display it
// this is intended to be dynamic (don't sub resultsCount)
if (results.length) {
_displayItem(results.pop(), searchTerms);
setTimeout(
() => _displayNextItem(results, resultCount, searchTerms),
5
);
}
// search finished, update title and status message
else _finishSearch(resultCount);
};
/**
* Default splitQuery function. Can be overridden in ``sphinx.search`` with a
* custom function per language.
*
* The regular expression works by splitting the string on consecutive characters
* that are not Unicode letters, numbers, underscores, or emoji characters.
* This is the same as ``\W+`` in Python, preserving the surrogate pair area.
*/
if (typeof splitQuery === "undefined") {
var splitQuery = (query) => query
.split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu)
.filter(term => term) // remove remaining empty strings
}
/**
* Search Module
*/
const Search = {
_index: null,
_queued_query: null,
_pulse_status: -1,
htmlToText: (htmlString) => {
const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html');
htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() });
const docContent = htmlElement.querySelector('[role="main"]');
if (docContent !== undefined) return docContent.textContent;
console.warn(
"Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template."
);
return "";
},
init: () => {
const query = new URLSearchParams(window.location.search).get("q");
document
.querySelectorAll('input[name="q"]')
.forEach((el) => (el.value = query));
if (query) Search.performSearch(query);
},
loadIndex: (url) =>
(document.body.appendChild(document.createElement("script")).src = url),
setIndex: (index) => {
Search._index = index;
if (Search._queued_query !== null) {
const query = Search._queued_query;
Search._queued_query = null;
Search.query(query);
}
},
hasIndex: () => Search._index !== null,
deferQuery: (query) => (Search._queued_query = query),
stopPulse: () => (Search._pulse_status = -1),
startPulse: () => {
if (Search._pulse_status >= 0) return;
const pulse = () => {
Search._pulse_status = (Search._pulse_status + 1) % 4;
Search.dots.innerText = ".".repeat(Search._pulse_status);
if (Search._pulse_status >= 0) window.setTimeout(pulse, 500);
};
pulse();
},
/**
* perform a search for something (or wait until index is loaded)
*/
performSearch: (query) => {
// create the required interface elements
const searchText = document.createElement("h2");
searchText.textContent = _("Searching");
const searchSummary = document.createElement("p");
searchSummary.classList.add("search-summary");
searchSummary.innerText = "";
const searchList = document.createElement("ul");
searchList.classList.add("search");
const out = document.getElementById("search-results");
Search.title = out.appendChild(searchText);
Search.dots = Search.title.appendChild(document.createElement("span"));
Search.status = out.appendChild(searchSummary);
Search.output = out.appendChild(searchList);
const searchProgress = document.getElementById("search-progress");
// Some themes don't use the search progress node
if (searchProgress) {
searchProgress.innerText = _("Preparing search...");
}
Search.startPulse();
// index already loaded, the browser was quick!
if (Search.hasIndex()) Search.query(query);
else Search.deferQuery(query);
},
/**
* execute search (requires search index to be loaded)
*/
query: (query) => {
const filenames = Search._index.filenames;
const docNames = Search._index.docnames;
const titles = Search._index.titles;
const allTitles = Search._index.alltitles;
const indexEntries = Search._index.indexentries;
// stem the search terms and add them to the correct list
const stemmer = new Stemmer();
const searchTerms = new Set();
const excludedTerms = new Set();
const highlightTerms = new Set();
const objectTerms = new Set(splitQuery(query.toLowerCase().trim()));
splitQuery(query.trim()).forEach((queryTerm) => {
const queryTermLower = queryTerm.toLowerCase();
// maybe skip this "word"
// stopwords array is from language_data.js
if (
stopwords.indexOf(queryTermLower) !== -1 ||
queryTerm.match(/^\d+$/)
)
return;
// stem the word
let word = stemmer.stemWord(queryTermLower);
// select the correct list
if (word[0] === "-") excludedTerms.add(word.substr(1));
else {
searchTerms.add(word);
highlightTerms.add(queryTermLower);
}
});
if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js
localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" "))
}
// console.debug("SEARCH: searching for:");
// console.info("required: ", [...searchTerms]);
// console.info("excluded: ", [...excludedTerms]);
// array of [docname, title, anchor, descr, score, filename]
let results = [];
_removeChildren(document.getElementById("search-progress"));
const queryLower = query.toLowerCase();
for (const [title, foundTitles] of Object.entries(allTitles)) {
if (title.toLowerCase().includes(queryLower) && (queryLower.length >= title.length/2)) {
for (const [file, id] of foundTitles) {
let score = Math.round(100 * queryLower.length / title.length)
results.push([
docNames[file],
titles[file] !== title ? `${titles[file]} > ${title}` : title,
id !== null ? "#" + id : "",
null,
score,
filenames[file],
]);
}
}
}
// search for explicit entries in index directives
for (const [entry, foundEntries] of Object.entries(indexEntries)) {
if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) {
for (const [file, id] of foundEntries) {
let score = Math.round(100 * queryLower.length / entry.length)
results.push([
docNames[file],
titles[file],
id ? "#" + id : "",
null,
score,
filenames[file],
]);
}
}
}
// lookup as object
objectTerms.forEach((term) =>
results.push(...Search.performObjectSearch(term, objectTerms))
);
// lookup as search terms in fulltext
results.push(...Search.performTermsSearch(searchTerms, excludedTerms));
// let the scorer override scores with a custom scoring function
if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item)));
// now sort the results by score (in opposite order of appearance, since the
// display function below uses pop() to retrieve items) and then
// alphabetically
results.sort((a, b) => {
const leftScore = a[4];
const rightScore = b[4];
if (leftScore === rightScore) {
// same score: sort alphabetically
const leftTitle = a[1].toLowerCase();
const rightTitle = b[1].toLowerCase();
if (leftTitle === rightTitle) return 0;
return leftTitle > rightTitle ? -1 : 1; // inverted is intentional
}
return leftScore > rightScore ? 1 : -1;
});
// remove duplicate search results
// note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept
let seen = new Set();
results = results.reverse().reduce((acc, result) => {
let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(',');
if (!seen.has(resultStr)) {
acc.push(result);
seen.add(resultStr);
}
return acc;
}, []);
results = results.reverse();
// for debugging
//Search.lastresults = results.slice(); // a copy
// console.info("search results:", Search.lastresults);
// print the results
_displayNextItem(results, results.length, searchTerms);
},
/**
* search for object names
*/
performObjectSearch: (object, objectTerms) => {
const filenames = Search._index.filenames;
const docNames = Search._index.docnames;
const objects = Search._index.objects;
const objNames = Search._index.objnames;
const titles = Search._index.titles;
const results = [];
const objectSearchCallback = (prefix, match) => {
const name = match[4]
const fullname = (prefix ? prefix + "." : "") + name;
const fullnameLower = fullname.toLowerCase();
if (fullnameLower.indexOf(object) < 0) return;
let score = 0;
const parts = fullnameLower.split(".");
// check for different match types: exact matches of full name or
// "last name" (i.e. last dotted part)
if (fullnameLower === object || parts.slice(-1)[0] === object)
score += Scorer.objNameMatch;
else if (parts.slice(-1)[0].indexOf(object) > -1)
score += Scorer.objPartialMatch; // matches in last name
const objName = objNames[match[1]][2];
const title = titles[match[0]];
// If more than one term searched for, we require other words to be
// found in the name/title/description
const otherTerms = new Set(objectTerms);
otherTerms.delete(object);
if (otherTerms.size > 0) {
const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase();
if (
[...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0)
)
return;
}
let anchor = match[3];
if (anchor === "") anchor = fullname;
else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname;
const descr = objName + _(", in ") + title;
// add custom score for some objects according to scorer
if (Scorer.objPrio.hasOwnProperty(match[2]))
score += Scorer.objPrio[match[2]];
else score += Scorer.objPrioDefault;
results.push([
docNames[match[0]],
fullname,
"#" + anchor,
descr,
score,
filenames[match[0]],
]);
};
Object.keys(objects).forEach((prefix) =>
objects[prefix].forEach((array) =>
objectSearchCallback(prefix, array)
)
);
return results;
},
/**
* search for full-text terms in the index
*/
performTermsSearch: (searchTerms, excludedTerms) => {
// prepare search
const terms = Search._index.terms;
const titleTerms = Search._index.titleterms;
const filenames = Search._index.filenames;
const docNames = Search._index.docnames;
const titles = Search._index.titles;
const scoreMap = new Map();
const fileMap = new Map();
// perform the search on the required terms
searchTerms.forEach((word) => {
const files = [];
const arr = [
{ files: terms[word], score: Scorer.term },
{ files: titleTerms[word], score: Scorer.title },
];
// add support for partial matches
if (word.length > 2) {
const escapedWord = _escapeRegExp(word);
Object.keys(terms).forEach((term) => {
if (term.match(escapedWord) && !terms[word])
arr.push({ files: terms[term], score: Scorer.partialTerm });
});
Object.keys(titleTerms).forEach((term) => {
if (term.match(escapedWord) && !titleTerms[word])
arr.push({ files: titleTerms[word], score: Scorer.partialTitle });
});
}
// no match but word was a required one
if (arr.every((record) => record.files === undefined)) return;
// found search word in contents
arr.forEach((record) => {
if (record.files === undefined) return;
let recordFiles = record.files;
if (recordFiles.length === undefined) recordFiles = [recordFiles];
files.push(...recordFiles);
// set score for the word in each file
recordFiles.forEach((file) => {
if (!scoreMap.has(file)) scoreMap.set(file, {});
scoreMap.get(file)[word] = record.score;
});
});
// create the mapping
files.forEach((file) => {
if (fileMap.has(file) && fileMap.get(file).indexOf(word) === -1)
fileMap.get(file).push(word);
else fileMap.set(file, [word]);
});
});
// now check if the files don't contain excluded terms
const results = [];
for (const [file, wordList] of fileMap) {
// check if all requirements are matched
// as search terms with length < 3 are discarded
const filteredTermCount = [...searchTerms].filter(
(term) => term.length > 2
).length;
if (
wordList.length !== searchTerms.size &&
wordList.length !== filteredTermCount
)
continue;
// ensure that none of the excluded terms is in the search result
if (
[...excludedTerms].some(
(term) =>
terms[term] === file ||
titleTerms[term] === file ||
(terms[term] || []).includes(file) ||
(titleTerms[term] || []).includes(file)
)
)
break;
// select one (max) score for the file.
const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w]));
// add result to the result list
results.push([
docNames[file],
titles[file],
"",
null,
score,
filenames[file],
]);
}
return results;
},
/**
* helper function to return a node containing the
* search summary for a given text. keywords is a list
* of stemmed words.
*/
makeSearchSummary: (htmlText, keywords) => {
const text = Search.htmlToText(htmlText);
if (text === "") return null;
const textLower = text.toLowerCase();
const actualStartPosition = [...keywords]
.map((k) => textLower.indexOf(k.toLowerCase()))
.filter((i) => i > -1)
.slice(-1)[0];
const startWithContext = Math.max(actualStartPosition - 120, 0);
const top = startWithContext === 0 ? "" : "...";
const tail = startWithContext + 240 < text.length ? "..." : "";
let summary = document.createElement("p");
summary.classList.add("context");
summary.textContent = top + text.substr(startWithContext, 240).trim() + tail;
return summary;
},
};
_ready(Search.init);

296
docs/build/html/_static/skeleton.css vendored Normal file
View File

@ -0,0 +1,296 @@
/* Some sane resets. */
html {
height: 100%;
}
body {
margin: 0;
min-height: 100%;
}
/* All the flexbox magic! */
body,
.sb-announcement,
.sb-content,
.sb-main,
.sb-container,
.sb-container__inner,
.sb-article-container,
.sb-footer-content,
.sb-header,
.sb-header-secondary,
.sb-footer {
display: flex;
}
/* These order things vertically */
body,
.sb-main,
.sb-article-container {
flex-direction: column;
}
/* Put elements in the center */
.sb-header,
.sb-header-secondary,
.sb-container,
.sb-content,
.sb-footer,
.sb-footer-content {
justify-content: center;
}
/* Put elements at the ends */
.sb-article-container {
justify-content: space-between;
}
/* These elements grow. */
.sb-main,
.sb-content,
.sb-container,
article {
flex-grow: 1;
}
/* Because padding making this wider is not fun */
article {
box-sizing: border-box;
}
/* The announcements element should never be wider than the page. */
.sb-announcement {
max-width: 100%;
}
.sb-sidebar-primary,
.sb-sidebar-secondary {
flex-shrink: 0;
width: 17rem;
}
.sb-announcement__inner {
justify-content: center;
box-sizing: border-box;
height: 3rem;
overflow-x: auto;
white-space: nowrap;
}
/* Sidebars, with checkbox-based toggle */
.sb-sidebar-primary,
.sb-sidebar-secondary {
position: fixed;
height: 100%;
top: 0;
}
.sb-sidebar-primary {
left: -17rem;
transition: left 250ms ease-in-out;
}
.sb-sidebar-secondary {
right: -17rem;
transition: right 250ms ease-in-out;
}
.sb-sidebar-toggle {
display: none;
}
.sb-sidebar-overlay {
position: fixed;
top: 0;
width: 0;
height: 0;
transition: width 0ms ease 250ms, height 0ms ease 250ms, opacity 250ms ease;
opacity: 0;
background-color: rgba(0, 0, 0, 0.54);
}
#sb-sidebar-toggle--primary:checked
~ .sb-sidebar-overlay[for="sb-sidebar-toggle--primary"],
#sb-sidebar-toggle--secondary:checked
~ .sb-sidebar-overlay[for="sb-sidebar-toggle--secondary"] {
width: 100%;
height: 100%;
opacity: 1;
transition: width 0ms ease, height 0ms ease, opacity 250ms ease;
}
#sb-sidebar-toggle--primary:checked ~ .sb-container .sb-sidebar-primary {
left: 0;
}
#sb-sidebar-toggle--secondary:checked ~ .sb-container .sb-sidebar-secondary {
right: 0;
}
/* Full-width mode */
.drop-secondary-sidebar-for-full-width-content
.hide-when-secondary-sidebar-shown {
display: none !important;
}
.drop-secondary-sidebar-for-full-width-content .sb-sidebar-secondary {
display: none !important;
}
/* Mobile views */
.sb-page-width {
width: 100%;
}
.sb-article-container,
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 100vw;
}
.sb-article,
.match-content-width {
padding: 0 1rem;
box-sizing: border-box;
}
@media (min-width: 32rem) {
.sb-article,
.match-content-width {
padding: 0 2rem;
}
}
/* Tablet views */
@media (min-width: 42rem) {
.sb-article-container {
width: auto;
}
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 42rem;
}
.sb-article,
.match-content-width {
width: 42rem;
}
}
@media (min-width: 46rem) {
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 46rem;
}
.sb-article,
.match-content-width {
width: 46rem;
}
}
@media (min-width: 50rem) {
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 50rem;
}
.sb-article,
.match-content-width {
width: 50rem;
}
}
/* Tablet views */
@media (min-width: 59rem) {
.sb-sidebar-secondary {
position: static;
}
.hide-when-secondary-sidebar-shown {
display: none !important;
}
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 59rem;
}
.sb-article,
.match-content-width {
width: 42rem;
}
}
@media (min-width: 63rem) {
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 63rem;
}
.sb-article,
.match-content-width {
width: 46rem;
}
}
@media (min-width: 67rem) {
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 67rem;
}
.sb-article,
.match-content-width {
width: 50rem;
}
}
/* Desktop views */
@media (min-width: 76rem) {
.sb-sidebar-primary {
position: static;
}
.hide-when-primary-sidebar-shown {
display: none !important;
}
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 59rem;
}
.sb-article,
.match-content-width {
width: 42rem;
}
}
/* Full desktop views */
@media (min-width: 80rem) {
.sb-article,
.match-content-width {
width: 46rem;
}
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 63rem;
}
}
@media (min-width: 84rem) {
.sb-article,
.match-content-width {
width: 50rem;
}
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 67rem;
}
}
@media (min-width: 88rem) {
.sb-footer-content__inner,
.drop-secondary-sidebar-for-full-width-content .sb-article,
.drop-secondary-sidebar-for-full-width-content .match-content-width {
width: 67rem;
}
.sb-page-width {
width: 88rem;
}
}

View File

@ -0,0 +1,144 @@
/* Highlighting utilities for Sphinx HTML documentation. */
"use strict";
const SPHINX_HIGHLIGHT_ENABLED = true
/**
* highlight a given string on a node by wrapping it in
* span elements with the given class name.
*/
const _highlight = (node, addItems, text, className) => {
if (node.nodeType === Node.TEXT_NODE) {
const val = node.nodeValue;
const parent = node.parentNode;
const pos = val.toLowerCase().indexOf(text);
if (
pos >= 0 &&
!parent.classList.contains(className) &&
!parent.classList.contains("nohighlight")
) {
let span;
const closestNode = parent.closest("body, svg, foreignObject");
const isInSVG = closestNode && closestNode.matches("svg");
if (isInSVG) {
span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
} else {
span = document.createElement("span");
span.classList.add(className);
}
span.appendChild(document.createTextNode(val.substr(pos, text.length)));
parent.insertBefore(
span,
parent.insertBefore(
document.createTextNode(val.substr(pos + text.length)),
node.nextSibling
)
);
node.nodeValue = val.substr(0, pos);
if (isInSVG) {
const rect = document.createElementNS(
"http://www.w3.org/2000/svg",
"rect"
);
const bbox = parent.getBBox();
rect.x.baseVal.value = bbox.x;
rect.y.baseVal.value = bbox.y;
rect.width.baseVal.value = bbox.width;
rect.height.baseVal.value = bbox.height;
rect.setAttribute("class", className);
addItems.push({ parent: parent, target: rect });
}
}
} else if (node.matches && !node.matches("button, select, textarea")) {
node.childNodes.forEach((el) => _highlight(el, addItems, text, className));
}
};
const _highlightText = (thisNode, text, className) => {
let addItems = [];
_highlight(thisNode, addItems, text, className);
addItems.forEach((obj) =>
obj.parent.insertAdjacentElement("beforebegin", obj.target)
);
};
/**
* Small JavaScript module for the documentation.
*/
const SphinxHighlight = {
/**
* highlight the search words provided in localstorage in the text
*/
highlightSearchWords: () => {
if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight
// get and clear terms from localstorage
const url = new URL(window.location);
const highlight =
localStorage.getItem("sphinx_highlight_terms")
|| url.searchParams.get("highlight")
|| "";
localStorage.removeItem("sphinx_highlight_terms")
url.searchParams.delete("highlight");
window.history.replaceState({}, "", url);
// get individual terms from highlight string
const terms = highlight.toLowerCase().split(/\s+/).filter(x => x);
if (terms.length === 0) return; // nothing to do
// There should never be more than one element matching "div.body"
const divBody = document.querySelectorAll("div.body");
const body = divBody.length ? divBody[0] : document.querySelector("body");
window.setTimeout(() => {
terms.forEach((term) => _highlightText(body, term, "highlighted"));
}, 10);
const searchBox = document.getElementById("searchbox");
if (searchBox === null) return;
searchBox.appendChild(
document
.createRange()
.createContextualFragment(
'<p class="highlight-link">' +
'<a href="javascript:SphinxHighlight.hideSearchWords()">' +
_("Hide Search Matches") +
"</a></p>"
)
);
},
/**
* helper function to hide the search marks again
*/
hideSearchWords: () => {
document
.querySelectorAll("#searchbox .highlight-link")
.forEach((el) => el.remove());
document
.querySelectorAll("span.highlighted")
.forEach((el) => el.classList.remove("highlighted"));
localStorage.removeItem("sphinx_highlight_terms")
},
initEscapeListener: () => {
// only install a listener if it is really needed
if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return;
document.addEventListener("keydown", (event) => {
// bail for input elements
if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
// bail with special keys
if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) {
SphinxHighlight.hideSearchWords();
event.preventDefault();
}
});
},
};
_ready(SphinxHighlight.highlightSearchWords);
_ready(SphinxHighlight.initEscapeListener);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

247
docs/build/html/development.html vendored Normal file
View File

@ -0,0 +1,247 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="prev" title="Reference" href="reference.html" />
<!-- Generated with Sphinx 7.0.1 and Furo 2023.05.20 -->
<title>Development - MusicMuster 2.11.2 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=e6660623a769aa55fea372102b9bf3151b292993" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
}
}
</style></head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-toc" viewBox="0 0 24 24">
<title>Contents</title>
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">MusicMuster 2.11.2 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">MusicMuster 2.11.2 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
<input type="hidden" name="check_keywords" value="yes">
<input type="hidden" name="area" value="default">
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="introduction.html">Introduction</a></li>
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">Reference</a></li>
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Development</a></li>
</ul>
</div>
</div>
</div>
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>Back to top</span>
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main">
<section id="development">
<h1>Development<a class="headerlink" href="#development" title="Permalink to this heading">#</a></h1>
</section>
</article>
</div>
<footer>
<div class="related-pages">
<a class="prev-page" href="reference.html">
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
<div class="page-info">
<div class="context">
<span>Previous</span>
</div>
<div class="title">Reference</div>
</div>
</a>
</div>
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Keith Edmunds
</div>
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
</div>
</div>
</footer>
</div>
<aside class="toc-drawer no-toc">
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
</body>
</html>

236
docs/build/html/genindex.html vendored Normal file
View File

@ -0,0 +1,236 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#" /><link rel="search" title="Search" href="search.html" />
<!-- Generated with Sphinx 7.0.1 and Furo 2023.05.20 --><title>Index - MusicMuster 2.11.2 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=e6660623a769aa55fea372102b9bf3151b292993" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
}
}
</style></head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-toc" viewBox="0 0 24 24">
<title>Contents</title>
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">MusicMuster 2.11.2 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">MusicMuster 2.11.2 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
<input type="hidden" name="check_keywords" value="yes">
<input type="hidden" name="area" value="default">
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul>
<li class="toctree-l1"><a class="reference internal" href="introduction.html">Introduction</a></li>
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">Reference</a></li>
<li class="toctree-l1"><a class="reference internal" href="development.html">Development</a></li>
</ul>
</div>
</div>
</div>
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>Back to top</span>
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main">
<section class="genindex-section">
<h1 id="index">Index</h1>
<div class="genindex-jumpbox"></div>
</section>
</article>
</div>
<footer>
<div class="related-pages">
</div>
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Keith Edmunds
</div>
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
</div>
</div>
</footer>
</div>
<aside class="toc-drawer no-toc">
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
</body>
</html>

294
docs/build/html/index.html vendored Normal file
View File

@ -0,0 +1,294 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Introduction" href="introduction.html" />
<!-- Generated with Sphinx 7.0.1 and Furo 2023.05.20 -->
<title>MusicMuster 2.11.2 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=e6660623a769aa55fea372102b9bf3151b292993" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
}
}
</style></head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-toc" viewBox="0 0 24 24">
<title>Contents</title>
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="#"><div class="brand">MusicMuster 2.11.2 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand" href="#">
<span class="sidebar-brand-text">MusicMuster 2.11.2 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
<input type="hidden" name="check_keywords" value="yes">
<input type="hidden" name="area" value="default">
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul>
<li class="toctree-l1"><a class="reference internal" href="introduction.html">Introduction</a></li>
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">Reference</a></li>
<li class="toctree-l1"><a class="reference internal" href="development.html">Development</a></li>
</ul>
</div>
</div>
</div>
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>Back to top</span>
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main">
<section id="welcome-to-musicmuster-s-documentation">
<h1>Welcome to MusicMusters documentation!<a class="headerlink" href="#welcome-to-musicmuster-s-documentation" title="Permalink to this heading">#</a></h1>
<p><strong>MusicMuster</strong> is a music player targeted at the production of live
internet radio shows.</p>
<section id="contents">
<h2>Contents<a class="headerlink" href="#contents" title="Permalink to this heading">#</a></h2>
<div class="toctree-wrapper compound">
<ul>
<li class="toctree-l1"><a class="reference internal" href="introduction.html">Introduction</a><ul>
<li class="toctree-l2"><a class="reference internal" href="introduction.html#why-musicmuster">Why MusicMuster?</a></li>
<li class="toctree-l2"><a class="reference internal" href="introduction.html#what-is-musicmuster">What is MusicMuster?</a></li>
<li class="toctree-l2"><a class="reference internal" href="introduction.html#features">Features</a></li>
<li class="toctree-l2"><a class="reference internal" href="introduction.html#requirements">Requirements</a></li>
<li class="toctree-l2"><a class="reference internal" href="introduction.html#feedback-bugs-etc">Feedback, bugs, etc</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">Reference</a></li>
<li class="toctree-l1"><a class="reference internal" href="development.html">Development</a></li>
</ul>
</div>
</section>
</section>
<section id="indices-and-tables">
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this heading">#</a></h1>
<ul class="simple">
<li><p><a class="reference internal" href="genindex.html"><span class="std std-ref">Index</span></a></p></li>
<li><p><a class="reference internal" href="py-modindex.html"><span class="std std-ref">Module Index</span></a></p></li>
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
</ul>
</section>
</article>
</div>
<footer>
<div class="related-pages">
<a class="next-page" href="introduction.html">
<div class="page-info">
<div class="context">
<span>Next</span>
</div>
<div class="title">Introduction</div>
</div>
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
</a>
</div>
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Keith Edmunds
</div>
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
</div>
</div>
</footer>
</div>
<aside class="toc-drawer">
<div class="toc-sticky toc-scroll">
<div class="toc-title-container">
<span class="toc-title">
On this page
</span>
</div>
<div class="toc-tree-container">
<div class="toc-tree">
<ul>
<li><a class="reference internal" href="#">Welcome to MusicMusters documentation!</a><ul>
<li><a class="reference internal" href="#contents">Contents</a></li>
</ul>
</li>
<li><a class="reference internal" href="#indices-and-tables">Indices and tables</a></li>
</ul>
</div>
</div>
</div>
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
</body>
</html>

255
docs/build/html/installation.html vendored Normal file
View File

@ -0,0 +1,255 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Tutorial" href="tutorial.html" /><link rel="prev" title="Introduction" href="introduction.html" />
<!-- Generated with Sphinx 7.0.1 and Furo 2023.05.20 -->
<title>Installation - MusicMuster 2.11.2 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=e6660623a769aa55fea372102b9bf3151b292993" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
}
}
</style></head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-toc" viewBox="0 0 24 24">
<title>Contents</title>
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">MusicMuster 2.11.2 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">MusicMuster 2.11.2 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
<input type="hidden" name="check_keywords" value="yes">
<input type="hidden" name="area" value="default">
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="introduction.html">Introduction</a></li>
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Installation</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">Reference</a></li>
<li class="toctree-l1"><a class="reference internal" href="development.html">Development</a></li>
</ul>
</div>
</div>
</div>
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>Back to top</span>
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main">
<section id="installation">
<h1>Installation<a class="headerlink" href="#installation" title="Permalink to this heading">#</a></h1>
</section>
</article>
</div>
<footer>
<div class="related-pages">
<a class="next-page" href="tutorial.html">
<div class="page-info">
<div class="context">
<span>Next</span>
</div>
<div class="title">Tutorial</div>
</div>
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
</a>
<a class="prev-page" href="introduction.html">
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
<div class="page-info">
<div class="context">
<span>Previous</span>
</div>
<div class="title">Introduction</div>
</div>
</a>
</div>
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Keith Edmunds
</div>
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
</div>
</div>
</footer>
</div>
<aside class="toc-drawer no-toc">
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
</body>
</html>

362
docs/build/html/introduction.html vendored Normal file
View File

@ -0,0 +1,362 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Installation" href="installation.html" /><link rel="prev" title="Welcome to MusicMusters documentation!" href="index.html" />
<!-- Generated with Sphinx 7.0.1 and Furo 2023.05.20 -->
<title>Introduction - MusicMuster 2.11.2 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=e6660623a769aa55fea372102b9bf3151b292993" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
}
}
</style></head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-toc" viewBox="0 0 24 24">
<title>Contents</title>
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">MusicMuster 2.11.2 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">MusicMuster 2.11.2 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
<input type="hidden" name="check_keywords" value="yes">
<input type="hidden" name="area" value="default">
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul class="current">
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Introduction</a></li>
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">Reference</a></li>
<li class="toctree-l1"><a class="reference internal" href="development.html">Development</a></li>
</ul>
</div>
</div>
</div>
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>Back to top</span>
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main">
<section id="introduction">
<h1>Introduction<a class="headerlink" href="#introduction" title="Permalink to this heading">#</a></h1>
<section id="why-musicmuster">
<h2>Why MusicMuster?<a class="headerlink" href="#why-musicmuster" title="Permalink to this heading">#</a></h2>
<p>In January 2022 I started my show on <cite>Mixcloud
&lt;https://www.mixcloud.com/KeithsMusicBox/&gt;</cite>. Until then, my show had
been on an internet radio station which required me to use a Windows
playout system. As I only use Linux, I had to set up a Windows PC
specifically for that purpose. The system I had to use had what I felt
were shortcomings in various areas.</p>
<p>Once I moved to Mixcloud I searched for a Linux equivalent that didnt
have the same shortcomings but was unable to find one that met my
criteria. I decided to see how practical it would be to write my own,
and MusicMuster was born.</p>
</section>
<section id="what-is-musicmuster">
<h2>What is MusicMuster?<a class="headerlink" href="#what-is-musicmuster" title="Permalink to this heading">#</a></h2>
<p>It is a Linux-based music player. Whilst it could be used as a general
home music player, there are much better applications for that role.
<strong>MusicMuster</strong> has been specifically designed to support the
production of live internet radio shows.</p>
</section>
<section id="features">
<h2>Features<a class="headerlink" href="#features" title="Permalink to this heading">#</a></h2>
<ul class="simple">
<li><p>Database backed</p></li>
<li><p>Can be almost entirely keyboard driven</p></li>
<li><p>Open multiple playlists in tabs</p></li>
<li><p>Play tracks from any playlist</p></li>
<li><p>Add notes/comments to tracks on playlist</p></li>
<li><p>Automatatic colour-coding of notes/comments according to content</p></li>
<li><p>Preview tracks before playing to audience</p></li>
<li><p>Time of day clock</p></li>
<li><p>Elapsed track time counter</p></li>
<li><p>Time to run until track starts to fade</p></li>
<li><p>Time to run until track is silent</p></li>
<li><p>Graphic of volume from 5 seconds (configurable) before fade until
track is silent</p></li>
<li><p>Optionally hide played tracks in playlist</p></li>
<li><p>Button to drop playout volume by 3dB for talkover</p></li>
<li><dl class="simple">
<dt>Playlist displays:</dt><dd><ul>
<li><p>Title</p></li>
<li><p>Artist</p></li>
<li><p>Length of track (mm:ss)</p></li>
<li><p>Estimated start time of track</p></li>
<li><p>Estimated end time of track</p></li>
<li><p>When track was last played</p></li>
<li><p>Bits per second (bitrate) of track</p></li>
<li><p>Length of leading silence in recording before track starts</p></li>
<li><p>Total track length of arbitrary sections of tracks</p></li>
<li><p>Commands that are sent to OBS Studio (eg, for automated scene
changes)</p></li>
</ul>
</dd>
</dl>
</li>
<li><p>Playlist templates</p></li>
<li><p>Move selected or unplayed tracks between playlists</p></li>
<li><p>Download CSV of tracks played between arbitrary dates/times</p></li>
<li><p>Search for tracks by title or artist</p></li>
<li><p>Automatic search of current and next track in Wikipedia</p></li>
<li><p>Optional search of any track in Wikipedia</p></li>
<li><p>Optional search of any track in Songfacts</p></li>
</ul>
</section>
<section id="requirements">
<h2>Requirements<a class="headerlink" href="#requirements" title="Permalink to this heading">#</a></h2>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>MusicMuster has only been tested on Debian 12, “Bookworm”;
however, it should run on most contemporary Linux systems.</p>
</div>
<p>The <a class="reference internal" href="installation.html"><span class="doc">Installation</span></a> page explains how to build MusicMuster in its
own environment which will automatcally install all requirements
except the database. The current version of MusicMuster uses MariaDB
version 10.11; however, any recent version of MariaDB should suffice.</p>
<p>MusicMuster is a Python 3 application and requires Python 3.8 or
later.</p>
</section>
<section id="feedback-bugs-etc">
<h2>Feedback, bugs, etc<a class="headerlink" href="#feedback-bugs-etc" title="Permalink to this heading">#</a></h2>
<p>Please send to <a class="reference external" href="mailto:keith&#37;&#52;&#48;midnighthax&#46;com">keith<span>&#64;</span>midnighthax<span>&#46;</span>com</a></p>
<p>Keith Edmunds,
July 2023</p>
</section>
</section>
</article>
</div>
<footer>
<div class="related-pages">
<a class="next-page" href="installation.html">
<div class="page-info">
<div class="context">
<span>Next</span>
</div>
<div class="title">Installation</div>
</div>
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
</a>
<a class="prev-page" href="index.html">
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
<div class="page-info">
<div class="context">
<span>Previous</span>
</div>
<div class="title">Home</div>
</div>
</a>
</div>
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Keith Edmunds
</div>
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
</div>
</div>
</footer>
</div>
<aside class="toc-drawer">
<div class="toc-sticky toc-scroll">
<div class="toc-title-container">
<span class="toc-title">
On this page
</span>
</div>
<div class="toc-tree-container">
<div class="toc-tree">
<ul>
<li><a class="reference internal" href="#">Introduction</a><ul>
<li><a class="reference internal" href="#why-musicmuster">Why MusicMuster?</a></li>
<li><a class="reference internal" href="#what-is-musicmuster">What is MusicMuster?</a></li>
<li><a class="reference internal" href="#features">Features</a></li>
<li><a class="reference internal" href="#requirements">Requirements</a></li>
<li><a class="reference internal" href="#feedback-bugs-etc">Feedback, bugs, etc</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
</body>
</html>

6
docs/build/html/objects.inv vendored Normal file
View File

@ -0,0 +1,6 @@
# Sphinx inventory version 2
# Project: MusicMuster
# Version:
# The remainder of this file is compressed using zlib.
xÚ…<EFBFBD>A E÷œP·®Ý¸hÒ¨‰k„±m2…¦Æî¼†×ó$ÒR¤<52>&nóæ¿Pp4mš¸#µUFòlÃUÂëŠä»X ºÖ
â8(‘†ü~¸²›æÎg@iàdxÞ¹ZúƒÀ¾OÇ}¸6 ª<>^ù)ŽâX-‡%·&â5²Fuò[K<j‰°Æ¨¿kû,6“{aúç¬ûG+zªüs¶…+XÐæ¯üÀà+«åšÀBòú…(<28>QGÆÖçƒ# éÓT±7¸€¾7

255
docs/build/html/reference.html vendored Normal file
View File

@ -0,0 +1,255 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Development" href="development.html" /><link rel="prev" title="Tutorial" href="tutorial.html" />
<!-- Generated with Sphinx 7.0.1 and Furo 2023.05.20 -->
<title>Reference - MusicMuster 2.11.2 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=e6660623a769aa55fea372102b9bf3151b292993" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
}
}
</style></head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-toc" viewBox="0 0 24 24">
<title>Contents</title>
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">MusicMuster 2.11.2 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">MusicMuster 2.11.2 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
<input type="hidden" name="check_keywords" value="yes">
<input type="hidden" name="area" value="default">
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="introduction.html">Introduction</a></li>
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Reference</a></li>
<li class="toctree-l1"><a class="reference internal" href="development.html">Development</a></li>
</ul>
</div>
</div>
</div>
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>Back to top</span>
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main">
<section id="reference">
<h1>Reference<a class="headerlink" href="#reference" title="Permalink to this heading">#</a></h1>
</section>
</article>
</div>
<footer>
<div class="related-pages">
<a class="next-page" href="development.html">
<div class="page-info">
<div class="context">
<span>Next</span>
</div>
<div class="title">Development</div>
</div>
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
</a>
<a class="prev-page" href="tutorial.html">
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
<div class="page-info">
<div class="context">
<span>Previous</span>
</div>
<div class="title">Tutorial</div>
</div>
</a>
</div>
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Keith Edmunds
</div>
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
</div>
</div>
</footer>
</div>
<aside class="toc-drawer no-toc">
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
</body>
</html>

244
docs/build/html/search.html vendored Normal file
View File

@ -0,0 +1,244 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="#" />
<!-- Generated with Sphinx 7.0.1 and Furo 2023.05.20 --><title>Search - MusicMuster 2.11.2 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=e6660623a769aa55fea372102b9bf3151b292993" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
}
}
</style></head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-toc" viewBox="0 0 24 24">
<title>Contents</title>
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">MusicMuster 2.11.2 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">MusicMuster 2.11.2 documentation</span>
</a><form class="sidebar-search-container" method="get" action="#" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
<input type="hidden" name="check_keywords" value="yes">
<input type="hidden" name="area" value="default">
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul>
<li class="toctree-l1"><a class="reference internal" href="introduction.html">Introduction</a></li>
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">Reference</a></li>
<li class="toctree-l1"><a class="reference internal" href="development.html">Development</a></li>
</ul>
</div>
</div>
</div>
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>Back to top</span>
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main">
<noscript>
<div class="admonition error">
<p class="admonition-title">Error</p>
<p>
Please activate JavaScript to enable the search functionality.
</p>
</div>
</noscript>
<div id="search-results"></div>
</article>
</div>
<footer>
<div class="related-pages">
</div>
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Keith Edmunds
</div>
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
</div>
</div>
</footer>
</div>
<aside class="toc-drawer no-toc">
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
<script src="_static/searchtools.js"></script>
<script src="_static/language_data.js"></script>
<script src="searchindex.js"></script></body>
</html>

1
docs/build/html/searchindex.js vendored Normal file
View File

@ -0,0 +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": [], "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": {}})

255
docs/build/html/tutorial.html vendored Normal file
View File

@ -0,0 +1,255 @@
<!doctype html>
<html class="no-js" lang="en">
<head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Reference" href="reference.html" /><link rel="prev" title="Installation" href="installation.html" />
<!-- Generated with Sphinx 7.0.1 and Furo 2023.05.20 -->
<title>Tutorial - MusicMuster 2.11.2 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=e6660623a769aa55fea372102b9bf3151b292993" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
<style>
body {
--color-code-background: #f8f8f8;
--color-code-foreground: black;
}
@media not print {
body[data-theme="dark"] {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
body:not([data-theme="light"]) {
--color-code-background: #202020;
--color-code-foreground: #d0d0d0;
}
}
}
</style></head>
<body>
<script>
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
</script>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="svg-toc" viewBox="0 0 24 24">
<title>Contents</title>
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
</svg>
</symbol>
<symbol id="svg-menu" viewBox="0 0 24 24">
<title>Menu</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</symbol>
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
<title>Expand</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</symbol>
<symbol id="svg-sun" viewBox="0 0 24 24">
<title>Light mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</symbol>
<symbol id="svg-moon" viewBox="0 0 24 24">
<title>Dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
</svg>
</symbol>
<symbol id="svg-sun-half" viewBox="0 0 24 24">
<title>Auto light/dark mode</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="12" r="9" />
<path d="M13 12h5" />
<path d="M13 15h4" />
<path d="M13 18h1" />
<path d="M13 9h4" />
<path d="M13 6h1" />
</svg>
</symbol>
</svg>
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
<label class="overlay sidebar-overlay" for="__navigation">
<div class="visually-hidden">Hide navigation sidebar</div>
</label>
<label class="overlay toc-overlay" for="__toc">
<div class="visually-hidden">Hide table of contents sidebar</div>
</label>
<div class="page">
<header class="mobile-header">
<div class="header-left">
<label class="nav-overlay-icon" for="__navigation">
<div class="visually-hidden">Toggle site navigation sidebar</div>
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">MusicMuster 2.11.2 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
</header>
<aside class="sidebar-drawer">
<div class="sidebar-container">
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
<span class="sidebar-brand-text">MusicMuster 2.11.2 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
<input type="hidden" name="check_keywords" value="yes">
<input type="hidden" name="area" value="default">
</form>
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
<ul class="current">
<li class="toctree-l1"><a class="reference internal" href="introduction.html">Introduction</a></li>
<li class="toctree-l1"><a class="reference internal" href="installation.html">Installation</a></li>
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">Reference</a></li>
<li class="toctree-l1"><a class="reference internal" href="development.html">Development</a></li>
</ul>
</div>
</div>
</div>
</div>
</aside>
<div class="main">
<div class="content">
<div class="article-container">
<a href="#" class="back-to-top muted-link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
</svg>
<span>Back to top</span>
</a>
<div class="content-icon-container">
<div class="theme-toggle-container theme-toggle-content">
<button class="theme-toggle">
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
</button>
</div>
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
<div class="visually-hidden">Toggle table of contents sidebar</div>
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
</label>
</div>
<article role="main">
<section id="tutorial">
<h1>Tutorial<a class="headerlink" href="#tutorial" title="Permalink to this heading">#</a></h1>
</section>
</article>
</div>
<footer>
<div class="related-pages">
<a class="next-page" href="reference.html">
<div class="page-info">
<div class="context">
<span>Next</span>
</div>
<div class="title">Reference</div>
</div>
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
</a>
<a class="prev-page" href="installation.html">
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
<div class="page-info">
<div class="context">
<span>Previous</span>
</div>
<div class="title">Installation</div>
</div>
</a>
</div>
<div class="bottom-of-page">
<div class="left-details">
<div class="copyright">
Copyright &#169; 2023, Keith Edmunds
</div>
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
<a href="https://github.com/pradyunsg/furo">Furo</a>
</div>
<div class="right-details">
</div>
</div>
</footer>
</div>
<aside class="toc-drawer no-toc">
</aside>
</div>
</div><script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/doctools.js"></script>
<script src="_static/sphinx_highlight.js"></script>
<script src="_static/scripts/furo.js"></script>
</body>
</html>

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

31
docs/source/conf.py Normal file
View File

@ -0,0 +1,31 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'MusicMuster'
copyright = '2023, Keith Edmunds'
author = 'Keith Edmunds'
release = '2.11.2'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.duration',
]
templates_path = ['_templates']
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
# html_theme = 'alabaster'
html_theme = 'furo'
html_static_path = ['_static']

View File

@ -0,0 +1,2 @@
Development
===========

25
docs/source/index.rst Normal file
View File

@ -0,0 +1,25 @@
.. MusicMuster documentation master file, created by
sphinx-quickstart on Sun Jul 2 17:58:44 2023.
Welcome to MusicMuster's documentation!
=======================================
**MusicMuster** is a music player targeted at the production of live
internet radio shows.
Contents
--------
.. toctree::
introduction
installation
tutorial
reference
development
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,2 @@
Installation
============

View File

@ -0,0 +1,87 @@
Introduction
============
Why MusicMuster?
----------------
In January 2022 I started my show on `Mixcloud
<https://www.mixcloud.com/KeithsMusicBox/>`. Until then, my show had
been on an internet radio station which required me to use a Windows
playout system. As I only use Linux, I had to set up a Windows PC
specifically for that purpose. The system I had to use had what I felt
were shortcomings in various areas.
Once I moved to Mixcloud I searched for a Linux equivalent that didn't
have the same shortcomings but was unable to find one that met my
criteria. I decided to see how practical it would be to write my own,
and MusicMuster was born.
What is MusicMuster?
--------------------
It is a Linux-based music player. Whilst it could be used as a general
home music player, there are much better applications for that role.
**MusicMuster** has been specifically designed to support the
production of live internet radio shows.
Features
--------
* Database backed
* Can be almost entirely keyboard driven
* Open multiple playlists in tabs
* Play tracks from any playlist
* Add notes/comments to tracks on playlist
* Automatatic colour-coding of notes/comments according to content
* Preview tracks before playing to audience
* Time of day clock
* Elapsed track time counter
* Time to run until track starts to fade
* Time to run until track is silent
* Graphic of volume from 5 seconds (configurable) before fade until
track is silent
* Optionally hide played tracks in playlist
* Button to drop playout volume by 3dB for talkover
* Playlist displays:
* Title
* Artist
* Length of track (mm:ss)
* Estimated start time of track
* Estimated end time of track
* When track was last played
* 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 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 and next track in Wikipedia
* Optional search of any track in Wikipedia
* Optional search of any track in Songfacts
Requirements
------------
.. note:: MusicMuster has only been tested on Debian 12, "Bookworm";
however, it should run on most contemporary Linux systems.
The :doc:`installation` page explains how to build MusicMuster in its
own environment which will automatcally install all requirements
except the database. The current version of MusicMuster uses MariaDB
version 10.11; however, any recent version of MariaDB should suffice.
MusicMuster is a Python 3 application and requires Python 3.8 or
later.
Feedback, bugs, etc
-------------------
Please send to keith@midnighthax.com
Keith Edmunds,
July 2023

View File

@ -0,0 +1,2 @@
Reference
=========

2
docs/source/tutorial.rst Normal file
View File

@ -0,0 +1,2 @@
Tutorial
========

1245
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,10 @@ mypy = "^0.991"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pudb = "^2022.1.3" pudb = "^2022.1.3"
sphinx = "^7.0.1"
furo = "^2023.5.20"
black = "^23.3.0"
flakehell = "^0.9.0"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View File

@ -2,6 +2,7 @@
from PyQt5 import QtGui, QtWidgets from PyQt5 import QtGui, QtWidgets
class TabBar(QtWidgets.QTabBar): class TabBar(QtWidgets.QTabBar):
def paintEvent(self, event): def paintEvent(self, event):
painter = QtWidgets.QStylePainter(self) painter = QtWidgets.QStylePainter(self)
@ -13,16 +14,18 @@ class TabBar(QtWidgets.QTabBar):
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option) painter.drawControl(QtWidgets.QStyle.CE_TabBarTabShape, option)
painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option) painter.drawControl(QtWidgets.QStyle.CE_TabBarTabLabel, option)
class Window(QtWidgets.QTabWidget): class Window(QtWidgets.QTabWidget):
def __init__(self): def __init__(self):
QtWidgets.QTabWidget.__init__(self) QtWidgets.QTabWidget.__init__(self)
self.setTabBar(TabBar(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) self.addTab(QtWidgets.QWidget(self), color)
if __name__ == '__main__':
if __name__ == "__main__":
import sys import sys
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
window = Window() window = Window()
window.resize(420, 200) window.resize(420, 200)

View File

@ -1,7 +1,12 @@
from config import Config
from datetime import datetime, timedelta from datetime import datetime, timedelta
from helpers import * from helpers import (
from models import Tracks fade_point,
get_audio_segment,
get_tags,
get_relative_date,
leading_silence,
ms_to_mmss,
)
def test_fade_point(): def test_fade_point():
@ -18,8 +23,8 @@ def test_fade_point():
testdata = eval(f.read()) testdata = eval(f.read())
# Volume detection can vary, so ± 1 second is OK # 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(): def test_get_tags():
@ -32,8 +37,8 @@ def test_get_tags():
with open(test_track_data) as f: with open(test_track_data) as f:
testdata = eval(f.read()) testdata = eval(f.read())
assert tags['artist'] == testdata['artist'] assert tags["artist"] == testdata["artist"]
assert tags['title'] == testdata['title'] assert tags["title"] == testdata["title"]
def test_get_relative_date(): def test_get_relative_date():
@ -44,8 +49,7 @@ def test_get_relative_date():
eight_days_ago = today_at_10 - timedelta(days=8) eight_days_ago = today_at_10 - timedelta(days=8)
assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago" assert get_relative_date(eight_days_ago, today_at_11) == "1 week, 1 day ago"
sixteen_days_ago = today_at_10 - timedelta(days=16) sixteen_days_ago = today_at_10 - timedelta(days=16)
assert get_relative_date( assert get_relative_date(sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
sixteen_days_ago, today_at_11) == "2 weeks, 2 days ago"
def test_leading_silence(): def test_leading_silence():
@ -62,8 +66,8 @@ def test_leading_silence():
testdata = eval(f.read()) testdata = eval(f.read())
# Volume detection can vary, so ± 1 second is OK # 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(): def test_ms_to_mmss():

View File

@ -135,7 +135,6 @@ def test_playdates_remove_track(session):
track_path = "/a/b/c" track_path = "/a/b/c"
track = Tracks(session, track_path) track = Tracks(session, track_path)
playdate = Playdates(session, track.id)
Playdates.remove_track(session, track.id) Playdates.remove_track(session, track.id)
last_played = Playdates.last_played(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): def test_playlist_add_note(session):
note_text = "my note" note_text = "my note"
note_row = 2
playlist = Playlists(session, "my playlist") playlist = Playlists(session, "my playlist")
note = playlist.add_note(session, note_row, note_text)
assert len(playlist.notes) == 1 assert len(playlist.notes) == 1
playlist_note = playlist.notes[0] playlist_note = playlist.notes[0]
@ -356,9 +353,7 @@ def test_tracks_get_all_paths(session):
def test_tracks_get_all_tracks(session): def test_tracks_get_all_tracks(session):
# Need two tracks # Need two tracks
track1_path = "/a/b/c" track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
track2_path = "/m/n/o" track2_path = "/m/n/o"
track2 = Tracks(session, track2_path)
result = Tracks.get_all_tracks(session) result = Tracks.get_all_tracks(session)
assert track1_path in [a.path for a in result] 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_path = "/a/b/c"
track1 = Tracks(session, track1_path) track1 = Tracks(session, track1_path)
assert Tracks.get_by_filename( assert Tracks.get_by_filename(session, os.path.basename(track1_path)) is track1
session, os.path.basename(track1_path)
) is track1
def test_tracks_by_path(session): def test_tracks_by_path(session):
@ -403,26 +396,24 @@ def test_tracks_rescan(session):
# Re-read the track # Re-read the track
track_read = Tracks.get_by_path(session, test_track_path) track_read = Tracks.get_by_path(session, test_track_path)
assert track_read.duration == testdata['duration'] assert track_read.duration == testdata["duration"]
assert track_read.start_gap == testdata['leading_silence'] assert track_read.start_gap == testdata["leading_silence"]
# Silence detection can vary, so ± 1 second is OK # 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.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.silence_at > testdata['trailing_silence'] - 1000 assert track_read.silence_at > testdata["trailing_silence"] - 1000
def test_tracks_remove_by_path(session): def test_tracks_remove_by_path(session):
track1_path = "/a/b/c" track1_path = "/a/b/c"
track1 = Tracks(session, track1_path)
assert len(Tracks.get_all_tracks(session)) == 1 assert len(Tracks.get_all_tracks(session)) == 1
Tracks.remove_by_path(session, track1_path) Tracks.remove_by_path(session, track1_path)
assert len(Tracks.get_all_tracks(session)) == 0 assert len(Tracks.get_all_tracks(session)) == 0
def test_tracks_search_artists(session): def test_tracks_search_artists(session):
track1_path = "/a/b/c" track1_path = "/a/b/c"
track1_artist = "Artist One" track1_artist = "Artist One"
track1 = Tracks(session, track1_path) track1 = Tracks(session, track1_path)
@ -435,7 +426,6 @@ def test_tracks_search_artists(session):
session.commit() session.commit()
x = Tracks.get_all_tracks(session)
artist_first_word = track1_artist.split()[0].lower() artist_first_word = track1_artist.split()[0].lower()
assert len(Tracks.search_artists(session, artist_first_word)) == 2 assert len(Tracks.search_artists(session, artist_first_word)) == 2
assert len(Tracks.search_artists(session, track1_artist)) == 1 assert len(Tracks.search_artists(session, track1_artist)) == 1
@ -454,7 +444,6 @@ def test_tracks_search_titles(session):
session.commit() session.commit()
x = Tracks.get_all_tracks(session)
title_first_word = track1_title.split()[0].lower() title_first_word = track1_title.split()[0].lower()
assert len(Tracks.search_titles(session, title_first_word)) == 2 assert len(Tracks.search_titles(session, title_first_word)) == 2
assert len(Tracks.search_titles(session, track1_title)) == 1 assert len(Tracks.search_titles(session, track1_title)) == 1

View File

@ -3,7 +3,6 @@ from PyQt5.QtCore import Qt
from app import playlists from app import playlists
from app import models from app import models
from app import musicmuster from app import musicmuster
from app import dbconfig
def seed2tracks(session): def seed2tracks(session):
@ -78,7 +77,6 @@ def test_save_and_restore(qtbot, session):
def test_meta_all_clear(qtbot, session): def test_meta_all_clear(qtbot, session):
# Create playlist # Create playlist
playlist = models.Playlists(session, "my playlist") playlist = models.Playlists(session, "my playlist")
playlist_tab = playlists.PlaylistTab(None, session, playlist.id) playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
@ -107,7 +105,6 @@ def test_meta_all_clear(qtbot, session):
def test_meta(qtbot, session): def test_meta(qtbot, session):
# Create playlist # Create playlist
playlist = playlists.Playlists(session, "my playlist") playlist = playlists.Playlists(session, "my playlist")
playlist_tab = playlists.PlaylistTab(None, session, playlist.id) playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
@ -211,7 +208,6 @@ def test_clear_next(qtbot, session):
def test_get_selected_row(qtbot, monkeypatch, session): def test_get_selected_row(qtbot, monkeypatch, session):
monkeypatch.setattr(musicmuster, "Session", session) monkeypatch.setattr(musicmuster, "Session", session)
monkeypatch.setattr(playlists, "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) row0_item0 = playlist_tab.item(0, 0)
assert row0_item0 is not None assert row0_item0 is not None
rect = playlist_tab.visualItemRect(row0_item0) rect = playlist_tab.visualItemRect(row0_item0)
qtbot.mouseClick( qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
)
row_number = playlist_tab.get_selected_row() row_number = playlist_tab.get_selected_row()
assert row_number == 0 assert row_number == 0
def test_set_next(qtbot, monkeypatch, session): def test_set_next(qtbot, monkeypatch, session):
monkeypatch.setattr(musicmuster, "Session", session) monkeypatch.setattr(musicmuster, "Session", session)
monkeypatch.setattr(playlists, "Session", session) monkeypatch.setattr(playlists, "Session", session)
seed2tracks(session) seed2tracks(session)
@ -274,14 +267,11 @@ def test_set_next(qtbot, monkeypatch, session):
row0_item2 = playlist_tab.item(0, 2) row0_item2 = playlist_tab.item(0, 2)
assert row0_item2 is not None assert row0_item2 is not None
rect = playlist_tab.visualItemRect(row0_item2) rect = playlist_tab.visualItemRect(row0_item2)
qtbot.mouseClick( qtbot.mouseClick(playlist_tab.viewport(), Qt.LeftButton, pos=rect.center())
playlist_tab.viewport(), Qt.LeftButton, pos=rect.center()
)
selected_title = playlist_tab.get_selected_title() selected_title = playlist_tab.get_selected_title()
assert selected_title == track1_title assert selected_title == track1_title
qtbot.keyPress(playlist_tab.viewport(), "N", qtbot.keyPress(playlist_tab.viewport(), "N", modifier=Qt.ControlModifier)
modifier=Qt.ControlModifier)
qtbot.wait(1000) qtbot.wait(1000)

24
tree.py
View File

@ -9,9 +9,10 @@ datas = {
], ],
"No Category": [ "No Category": [
("New Game", "Playnite", "", "", "Never", "Not Plated", ""), ("New Game", "Playnite", "", "", "Never", "Not Plated", ""),
] ],
} }
class GroupDelegate(QtWidgets.QStyledItemDelegate): class GroupDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None): def __init__(self, parent=None):
super(GroupDelegate, self).__init__(parent) super(GroupDelegate, self).__init__(parent)
@ -25,6 +26,7 @@ class GroupDelegate(QtWidgets.QStyledItemDelegate):
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
option.icon = self._minus_icon if is_open else self._plus_icon option.icon = self._minus_icon if is_open else self._plus_icon
class GroupView(QtWidgets.QTreeView): class GroupView(QtWidgets.QTreeView):
def __init__(self, model, parent=None): def __init__(self, model, parent=None):
super(GroupView, self).__init__(parent) super(GroupView, self).__init__(parent)
@ -48,7 +50,18 @@ class GroupModel(QtGui.QStandardItemModel):
def __init__(self, parent=None): def __init__(self, parent=None):
super(GroupModel, self).__init__(parent) super(GroupModel, self).__init__(parent)
self.setColumnCount(8) 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()): for i in range(self.columnCount()):
it = self.horizontalHeaderItem(i) it = self.horizontalHeaderItem(i)
it.setForeground(QtGui.QColor("#F2F2F2")) it.setForeground(QtGui.QColor("#F2F2F2"))
@ -84,7 +97,7 @@ class GroupModel(QtGui.QStandardItemModel):
item.setEditable(False) item.setEditable(False)
item.setBackground(QtGui.QColor("#0D1225")) item.setBackground(QtGui.QColor("#0D1225"))
item.setForeground(QtGui.QColor("#F2F2F2")) item.setForeground(QtGui.QColor("#F2F2F2"))
group_item.setChild(j, i+1, item) group_item.setChild(j, i + 1, item)
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
@ -100,11 +113,12 @@ class MainWindow(QtWidgets.QMainWindow):
for children in childrens: for children in childrens:
model.append_element_to_group(group_item, children) model.append_element_to_group(group_item, children)
if __name__ == '__main__':
if __name__ == "__main__":
import sys import sys
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
w = MainWindow() w = MainWindow()
w.resize(720, 240) w.resize(720, 240)
w.show() w.show()
sys.exit(app.exec_()) sys.exit(app.exec_())

1
web.py
View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
from pprint import pprint
from PyQt6.QtWidgets import QApplication, QLabel from PyQt6.QtWidgets import QApplication, QLabel
from PyQt6.QtGui import QColor, QPalette from PyQt6.QtGui import QColor, QPalette