Compare commits

..

22 Commits

Author SHA1 Message Date
Keith Edmunds
2a1d9e94bc All tests pass 2024-06-02 19:33:41 +01:00
Keith Edmunds
9f7af072dc Remove carts from tests 2024-06-02 19:28:26 +01:00
Keith Edmunds
648ef76234 Resume working 2024-06-02 19:19:35 +01:00
Keith Edmunds
909fb27bed All preview/intro management working 2024-06-02 17:58:20 +01:00
Keith Edmunds
09fdd7e4dc Display of countdown timer works 2024-06-02 16:50:49 +01:00
Keith Edmunds
983716e009 Row times updating working 2024-06-02 16:34:30 +01:00
Keith Edmunds
4ec1c0e09c Fade graph no longer lagging 2024-06-02 14:31:14 +01:00
Keith Edmunds
0361d25c7b WIP: fade graph working, slightly laggy 2024-06-02 13:33:57 +01:00
Keith Edmunds
c5ca1469dc Remove all carts code 2024-06-02 12:04:26 +01:00
Keith Edmunds
5278b124ca WIP: implemented trackmanager, tracks play, clocks work 2024-06-02 11:57:45 +01:00
Keith Edmunds
fbcedb6c3b Create trackmanager.py
music.py is fully absorbed into trackmanager.py and thus removed
Substantial parts of classes.py are absorbed into trackmanager.py
2024-06-02 10:00:31 +01:00
Keith Edmunds
8ea0a0dad5 WIP: moving player to PlaylistTrack. Player works. 2024-06-01 17:41:22 +01:00
Keith Edmunds
b1f682d2e6 Uncheck preview armed at end of preview 2024-05-25 09:36:19 +01:00
Keith Edmunds
3d3df85845 PoC: added intro time display and editing 2024-05-25 09:29:03 +01:00
Keith Edmunds
8ebaa2798f Set intro timer background colour 2024-05-24 16:48:48 +01:00
Keith Edmunds
afc3014b18 Much improved stderr reporting on exceptions 2024-05-24 15:04:07 +01:00
Keith Edmunds
45a22c47d0 Implement intro timing and countdown 2024-05-24 14:27:00 +01:00
Keith Edmunds
0c03db14d4 Migrate Alembic to Alchemical format 2024-05-24 14:25:54 +01:00
Keith Edmunds
fb5376cdf0 WIP time to vocals: record button icons 2024-05-24 14:25:54 +01:00
Keith Edmunds
01916c4adc WIP: time to vocals: preview +- working 2024-05-24 14:25:51 +01:00
Keith Edmunds
1d33622c13 WIP: time to vocals 2024-05-24 14:20:59 +01:00
Keith Edmunds
b86f0ac1b7 Unifty format of VLC config variables 2024-05-24 14:19:16 +01:00
24 changed files with 4785 additions and 1360 deletions

View File

@ -1,18 +1,29 @@
# A generic, single database configuration.
# a multi-database configuration.
[alembic]
# this must be configured to point to the Alchemical database instance
# there are two components separated by a colon:
# the left part is the import path to the module containing the database instance
# the right part is the name of the database instance, typically 'db'
alchemical_db = models:db
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
prepend_sys_path = app
# timezone to use when rendering the date
# within the migration file as well as the filename.
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
@ -30,20 +41,26 @@ prepend_sys_path = .
# versions/ directory
# sourceless = false
# version location specification; this defaults
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat migrations/versions
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = SET
# sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod
# sqlalchemy.url = mysql+mysqldb://dev_musicmuster:dev_musicmuster@localhost/dev_musicmuster
# sqlalchemy.url = mysql+mysqldb://dev_musicmuster:dev_musicmuster@localhost/dev_musicmuster_carts
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
@ -53,7 +70,7 @@ sqlalchemy.url = SET
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options=-l 79
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]

View File

@ -1,77 +1,28 @@
# Standard library imports
from dataclasses import dataclass, field
from enum import auto, Enum
from typing import Any, Optional
import datetime as dt
# PyQt imports
from PyQt6.QtCore import pyqtSignal, QObject, QThread
from PyQt6.QtCore import pyqtSignal, QObject
# Third party imports
import numpy as np
import pyqtgraph as pg # type: ignore
from sqlalchemy.orm import scoped_session
# App imports
from config import Config
from log import log
from models import PlaylistRows
import helpers
class FadeCurve:
GraphWidget = None
def __init__(
self, track_path: str, track_fade_at: int, track_silence_at: int
) -> None:
"""
Set up fade graph array
"""
audio = helpers.get_audio_segment(track_path)
if not audio:
log.error(f"FadeCurve: could not get audio for {track_path=}")
return None
# Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE
# milliseconds before fade starts to silence
self.start_ms = max(0, track_fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
self.end_ms = track_silence_at
self.audio_segment = audio[self.start_ms : self.end_ms]
self.graph_array = np.array(self.audio_segment.get_array_of_samples())
# Calculate the factor to map milliseconds of track to array
self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms)
self.region = None
def clear(self) -> None:
"""Clear the current graph"""
if self.GraphWidget:
self.GraphWidget.clear()
def plot(self):
self.curve = self.GraphWidget.plot(self.graph_array)
self.curve.setPen(Config.FADE_CURVE_FOREGROUND)
def tick(self, play_time) -> None:
"""Update volume fade curve"""
if not self.GraphWidget:
return
ms_of_graph = play_time - self.start_ms
if ms_of_graph < 0:
return
if self.region is None:
# Create the region now that we're into fade
self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)])
self.GraphWidget.addItem(self.region)
# Update region position
self.region.setRegion([0, ms_of_graph * self.ms_to_array_factor])
class Col(Enum):
START_GAP = 0
TITLE = auto()
ARTIST = auto()
INTRO = auto()
DURATION = auto()
START_TIME = auto()
END_TIME = auto()
LAST_PLAYED = auto()
BITRATE = auto()
NOTE = auto()
@helpers.singleton
@ -97,116 +48,12 @@ class MusicMusterSignals(QObject):
show_warning_signal = pyqtSignal(str, str)
span_cells_signal = pyqtSignal(int, int, int, int, int)
status_message_signal = pyqtSignal(str, int)
track_ended_signal = pyqtSignal()
def __post_init__(self):
super().__init__()
class PlaylistTrack:
"""
Used to provide a single reference point for specific playlist tracks,
typically the previous, current and next track.
"""
def __init__(self) -> None:
"""
Only initialises data structure. Call set_plr to populate.
"""
self.artist: Optional[str] = None
self.duration: Optional[int] = None
self.end_time: Optional[dt.datetime] = None
self.fade_at: Optional[int] = None
self.fade_graph: Optional[FadeCurve] = None
self.fade_graph_start_updates: Optional[dt.datetime] = None
self.fade_length: Optional[int] = None
self.path: Optional[str] = None
self.playlist_id: Optional[int] = None
self.plr_id: Optional[int] = None
self.plr_rownum: Optional[int] = None
self.resume_marker: Optional[float] = None
self.silence_at: Optional[int] = None
self.start_gap: Optional[int] = None
self.start_time: Optional[dt.datetime] = None
self.title: Optional[str] = None
self.track_id: Optional[int] = None
def __repr__(self) -> str:
return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
f"plr_rownum={self.plr_rownum}, playlist_id={self.playlist_id}>"
)
def set_plr(self, session: scoped_session, plr: PlaylistRows) -> None:
"""
Update with new plr information
"""
session.add(plr)
self.plr_rownum = plr.plr_rownum
if not plr.track:
return
track = plr.track
self.artist = track.artist
self.duration = track.duration
self.end_time = None
self.fade_at = track.fade_at
self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.start_time = None
self.title = track.title
self.track_id = track.id
if track.silence_at and track.fade_at:
self.fade_length = track.silence_at - track.fade_at
# Initialise and add FadeCurve in a thread as it's slow
self.fadecurve_thread = QThread()
self.worker = AddFadeCurve(
self,
track_path=track.path,
track_fade_at=track.fade_at,
track_silence_at=track.silence_at,
)
self.worker.moveToThread(self.fadecurve_thread)
self.fadecurve_thread.started.connect(self.worker.run)
self.worker.finished.connect(self.fadecurve_thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater)
self.fadecurve_thread.start()
def start(self) -> None:
"""
Called when track starts playing
"""
now = dt.datetime.now()
self.start_time = now
if self.duration:
self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration)
# Calculate time fade_graph should start updating
if self.fade_at:
update_graph_at_ms = max(
0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1
)
self.fade_graph_start_updates = now + dt.timedelta(
milliseconds=update_graph_at_ms
)
# Calculate time fade_graph should start updating
update_graph_at_ms = max(
0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1
)
self.fade_graph_start_updates = now + dt.timedelta(
milliseconds=update_graph_at_ms
)
@dataclass
class TrackFileData:
"""
@ -219,46 +66,3 @@ class TrackFileData:
obsolete_path: Optional[str] = None
tags: dict[str, Any] = field(default_factory=dict)
audio_metadata: dict[str, str | int | float] = field(default_factory=dict)
class AddFadeCurve(QObject):
"""
Initialising a fade curve introduces a noticeable delay so carry out in
a thread.
"""
finished = pyqtSignal()
def __init__(
self,
playlist_track: PlaylistTrack,
track_path: str,
track_fade_at: int,
track_silence_at: int,
):
super().__init__()
self.playlist_track = playlist_track
self.track_path = track_path
self.track_fade_at = track_fade_at
self.track_silence_at = track_silence_at
def run(self):
"""
Create fade curve and add to PlaylistTrack object
"""
fc = FadeCurve(self.track_path, self.track_fade_at, self.track_silence_at)
if not fc:
log.error(f"Failed to create FadeCurve for {self.track_path=}")
else:
self.playlist_track.fade_graph = fc
self.finished.emit()
class TrackSequence:
next = PlaylistTrack()
now = PlaylistTrack()
previous = PlaylistTrack()
track_sequence = TrackSequence()

View File

@ -9,17 +9,9 @@ class Config(object):
AUDIO_SEGMENT_CHUNK_SIZE = 10
BITRATE_LOW_THRESHOLD = 192
BITRATE_OK_THRESHOLD = 300
CART_DIRECTORY = "/home/kae/radio/CartTracks"
CARTS_COUNT = 10
CARTS_HIDE = True
COLOUR_BITRATE_LOW = "#ffcdd2"
COLOUR_BITRATE_MEDIUM = "#ffeb6f"
COLOUR_BITRATE_OK = "#dcedc8"
COLOUR_CART_ERROR = "#dc3545"
COLOUR_CART_PLAYING = "#248f24"
COLOUR_CART_PROGRESSBAR = "#000000"
COLOUR_CART_READY = "#ffc107"
COLOUR_CART_UNCONFIGURED = "#f2f2f2"
COLOUR_CURRENT_PLAYLIST = "#7eca8f"
COLOUR_CURRENT_TAB = "#248f24"
COLOUR_ENDING_TIMER = "#dc3545"
@ -52,6 +44,7 @@ class Config(object):
HEADER_BITRATE = "bps"
HEADER_DURATION = "Length"
HEADER_END_TIME = "End"
HEADER_INTRO = "Intro"
HEADER_LAST_PLAYED = "Last played"
HEADER_NOTE = "Notes"
HEADER_START_GAP = "Gap"
@ -59,6 +52,8 @@ class Config(object):
HEADER_TITLE = "Title"
HIDE_AFTER_PLAYING_OFFSET = 5000
INFO_TAB_TITLE_LENGTH = 15
INTRO_SECONDS_FORMAT = ".1f"
INTRO_SECONDS_WARNING_MS = 3000
LAST_PLAYED_TODAY_STRING = "Today"
LAST_PLAYED_TOOLTIP_DATE_FORMAT = "%a, %d %b %Y"
LOG_LEVEL_STDERR = logging.INFO
@ -80,6 +75,9 @@ class Config(object):
OBS_PORT = 4455
PLAY_NEXT_GUARD_MS = 10000
PLAY_SETTLE = 500000
PREVIEW_ADVANCE_MS = 5000
PREVIEW_BACK_MS = 5000
PREVIEW_END_BUFFER_MS = 1000
REPLACE_FILES_DEFAULT_SOURCE = "/home/kae/music/Singles/tmp"
RETURN_KEY_DEBOUNCE_MS = 500
ROOT = os.environ.get("ROOT") or "/home/kae/music"
@ -89,8 +87,10 @@ class Config(object):
TEXT_NO_TRACK_NO_NOTE = "[Section header]"
TOD_TIME_FORMAT = "%H:%M:%S"
TRACK_TIME_FORMAT = "%H:%M:%S"
VOLUME_VLC_DEFAULT = 75
VOLUME_VLC_DROP3db = 65
VLC_MAIN_PLAYER_NAME = "MusicMuster Main Player"
VLC_PREVIEW_PLAYER_NAME = "MusicMuster Preview Player"
VLC_VOLUME_DEFAULT = 75
VLC_VOLUME_DROP3db = 65
WARNING_MS_BEFORE_FADE = 5500
WARNING_MS_BEFORE_SILENCE = 5500
WEB_ZOOM_FACTOR = 1.2

View File

@ -23,23 +23,6 @@ from sqlalchemy.orm import (
# Database classes
class CartsTable(Model):
__tablename__ = "carts"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
cart_number: Mapped[int] = mapped_column(unique=True)
name: Mapped[str] = mapped_column(String(256), index=True)
duration: Mapped[Optional[int]] = mapped_column(index=True)
path: Mapped[Optional[str]] = mapped_column(String(2048), index=False)
enabled: Mapped[Optional[bool]] = mapped_column(default=False)
def __repr__(self) -> str:
return (
f"<Carts(id={self.id}, cart={self.cart_number}, "
f"name={self.name}, path={self.path}>"
)
class NoteColoursTable(Model):
__tablename__ = "notecolours"
@ -157,6 +140,7 @@ class TracksTable(Model):
bitrate: Mapped[Optional[int]] = mapped_column(default=None)
duration: Mapped[int] = mapped_column(index=True)
fade_at: Mapped[int] = mapped_column(index=False)
intro: Mapped[Optional[int]] = mapped_column(default=None)
mtime: Mapped[float] = mapped_column(index=True)
path: Mapped[str] = mapped_column(String(2048), index=False, unique=True)
silence_at: Mapped[int] = mapped_column(index=False)

View File

@ -6,7 +6,7 @@ import logging.handlers
import os
import stackprinter # type: ignore
import sys
import traceback
from traceback import print_exception
from config import Config
@ -48,19 +48,19 @@ syslog.setFormatter(syslog_fmt)
log.addHandler(syslog)
def log_uncaught_exceptions(_ex_cls, ex, tb):
def log_uncaught_exceptions(type_, value, traceback):
from helpers import send_mail
print("\033[1;31;47m")
logging.critical("".join(traceback.format_tb(tb)))
print_exception(type_, value, traceback)
print("\033[1;37;40m")
print(
stackprinter.format(
ex, suppressed_paths=["/pypoetry/virtualenvs/"], style="darkbg"
value, suppressed_paths=["/pypoetry/virtualenvs/"], style="darkbg"
)
)
if os.environ["MM_ENV"] == "PRODUCTION":
msg = stackprinter.format(ex)
msg = stackprinter.format(value)
send_mail(
Config.ERRORS_TO, Config.ERRORS_FROM, "Exception from musicmuster", msg
)

View File

@ -37,28 +37,6 @@ db = Alchemical(ALCHEMICAL_DATABASE_URI, engine_options=Config.ENGINE_OPTIONS)
# Database classes
class Carts(dbtables.CartsTable):
def __init__(
self,
session: Session,
cart_number: int,
name: str,
duration: Optional[int] = None,
path: Optional[str] = None,
enabled: bool = True,
) -> None:
"""Create new cart"""
self.cart_number = cart_number
self.name = name
self.duration = duration
self.path = path
self.enabled = enabled
session.add(self)
session.commit()
class NoteColours(dbtables.NoteColoursTable):
def __init__(
self,

View File

@ -1,197 +0,0 @@
# Standard library imports
import datetime as dt
import threading
from time import sleep
from typing import Optional
# Third party imports
import vlc # type: ignore
# PyQt imports
from PyQt6.QtCore import (
QRunnable,
QThreadPool,
)
# App imports
from config import Config
from helpers import file_is_unreadable
from log import log
lock = threading.Lock()
class FadeTrack(QRunnable):
def __init__(self, player: vlc.MediaPlayer, fade_seconds) -> None:
super().__init__()
self.player = player
self.fade_seconds = fade_seconds
def run(self) -> None:
"""
Implementation of fading the player
"""
if not self.player:
return
# Reduce volume logarithmically
total_steps = self.fade_seconds * Config.FADEOUT_STEPS_PER_SECOND
db_reduction_per_step = Config.FADEOUT_DB / total_steps
reduction_factor_per_step = pow(10, (db_reduction_per_step / 20))
volume = self.player.audio_get_volume()
for i in range(1, total_steps + 1):
self.player.audio_set_volume(
int(volume * pow(reduction_factor_per_step, i))
)
sleep(1 / Config.FADEOUT_STEPS_PER_SECOND)
self.player.stop()
log.debug(f"Releasing player {self.player=}")
self.player.release()
class Music:
"""
Manage the playing of music tracks
"""
def __init__(self) -> None:
self.VLC = vlc.Instance()
self.player = None
self.max_volume = Config.VOLUME_VLC_DEFAULT
self.start_dt: Optional[dt.datetime] = None
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
"""
Fade the currently playing track.
The actual management of fading runs in its own thread so as not
to hold up the UI during the fade.
"""
log.info("Music.stop()")
if not self.player:
return
if not self.player.get_position() > 0 and self.player.is_playing():
return
# Take a copy of current player to allow another track to be
# started without interfering here
with lock:
p = self.player
self.player = None
pool = QThreadPool.globalInstance()
fader = FadeTrack(p, fade_seconds=fade_seconds)
pool.start(fader)
self.start_dt = None
def get_playtime(self) -> int:
"""
Return number of milliseconds current track has been playing or
zero if not playing. The vlc function get_time() only updates 3-4
times a second; this function has much better resolution.
"""
if self.start_dt is None:
return 0
now = dt.datetime.now()
elapsed_seconds = (now - self.start_dt).total_seconds()
return int(elapsed_seconds * 1000)
def get_position(self) -> Optional[float]:
"""Return current position"""
if not self.player:
return None
return self.player.get_position()
def is_playing(self) -> bool:
"""Return True if playing"""
# There is a discrete time between starting playing a track and
# player.is_playing() returning True, so assume playing if less
# than Config.PLAY_SETTLE microseconds have passed since
# starting play.
return (
self.player is not None
and self.start_dt is not None
and (
self.player.is_playing()
or (dt.datetime.now() - self.start_dt)
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
)
)
def play(self, path: str, position: Optional[float] = None) -> None:
"""
Start playing the track at path.
Log and return if path not found.
"""
log.info(f"Music.play({path=}, {position=}")
if file_is_unreadable(path):
log.error(f"play({path}): path not readable")
return None
media = self.VLC.media_new_path(path)
self.player = media.player_new_from_media()
if self.player:
_ = self.player.play()
self.set_volume(self.max_volume)
if position:
self.player.set_position(position)
self.start_dt = dt.datetime.now()
def set_volume(self, volume=None, set_default=True) -> None:
"""Set maximum volume used for player"""
if not self.player:
return
if set_default:
self.max_volume = volume
if volume is None:
volume = Config.VOLUME_VLC_DEFAULT
self.player.audio_set_volume(volume)
# Ensure volume correct
# For as-yet unknown reasons. sometimes the volume gets
# reset to zero within 200mS or so of starting play. This
# only happened since moving to Debian 12, which uses
# Pipewire for sound (which may be irrelevant).
for _ in range(3):
current_volume = self.player.audio_get_volume()
if current_volume < volume:
self.player.audio_set_volume(volume)
log.debug(f"Reset from {volume=}")
sleep(0.1)
def stop(self) -> float:
"""Immediately stop playing"""
log.info("Music.stop()")
if not self.player:
return 0.0
p = self.player
self.player = None
self.start_dt = None
with lock:
position = p.get_position()
p.stop()
p.release()
p = None
return position

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,6 @@
# Allow forward reference to PlaylistModel
from __future__ import annotations
from enum import auto, Enum
from operator import attrgetter
from random import shuffle
from typing import List, Optional
@ -31,7 +30,7 @@ import obswebsocket # type: ignore
# import snoop # type: ignore
# App imports
from classes import track_sequence, MusicMusterSignals, PlaylistTrack
from classes import Col, MusicMusterSignals
from config import Config
from helpers import (
file_is_unreadable,
@ -42,25 +41,17 @@ from helpers import (
)
from log import log
from models import db, NoteColours, Playdates, PlaylistRows, Tracks
from trackmanager import (
MainTrackManager,
track_sequence,
)
HEADER_NOTES_COLUMN = 1
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
class Col(Enum):
START_GAP = 0
TITLE = auto()
ARTIST = auto()
DURATION = auto()
START_TIME = auto()
END_TIME = auto()
LAST_PLAYED = auto()
BITRATE = auto()
NOTE = auto()
class PlaylistRowData:
class _PlaylistRowData:
def __init__(self, plr: PlaylistRows) -> None:
"""
Populate PlaylistRowData from database PlaylistRows record
@ -69,6 +60,7 @@ class PlaylistRowData:
self.artist: str = ""
self.bitrate = 0
self.duration: int = 0
self.intro: Optional[int] = None
self.lastplayed: dt.datetime = Config.EPOCH
self.path = ""
self.played = False
@ -86,6 +78,7 @@ class PlaylistRowData:
self.title = plr.track.title
self.artist = plr.track.artist
self.duration = plr.track.duration
self.intro = plr.track.intro
self.played = plr.played
if plr.track.playdates:
self.lastplayed = max([a.lastplayed for a in plr.track.playdates])
@ -128,7 +121,7 @@ class PlaylistModel(QAbstractTableModel):
self.playlist_id = playlist_id
super().__init__(*args, **kwargs)
self.playlist_rows: dict[int, PlaylistRowData] = {}
self.playlist_rows: dict[int, _PlaylistRowData] = {}
self.signals = MusicMusterSignals()
self.played_tracks_hidden = False
@ -189,7 +182,7 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id)
def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush:
def background_role(self, row: int, column: int, prd: _PlaylistRowData) -> QBrush:
"""Return background setting"""
# Handle entire row colouring
@ -206,10 +199,10 @@ class PlaylistModel(QAbstractTableModel):
if file_is_unreadable(prd.path):
return QBrush(QColor(Config.COLOUR_UNREADABLE))
# Current track
if prd.plrid == track_sequence.now.plr_id:
if track_sequence.current and track_sequence.current.track_id == prd.track_id:
return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST))
# Next track
if prd.plrid == track_sequence.next.plr_id:
if track_sequence.next and track_sequence.next.track_id == prd.track_id:
return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST))
# Individual cell colouring
@ -244,7 +237,7 @@ class PlaylistModel(QAbstractTableModel):
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
"""Standard function for view"""
return 9
return len(Col)
def current_track_started(self) -> None:
"""
@ -261,24 +254,10 @@ class PlaylistModel(QAbstractTableModel):
- find next track
"""
row_number = track_sequence.now.plr_rownum
if row_number is not None:
prd = self.playlist_rows[row_number]
else:
prd = None
if not track_sequence.current:
return
# Sanity check
if not track_sequence.now.track_id:
log.error(
"playlistmodel:current_track_started called with no current track"
)
return
if row_number is None:
log.error(
"playlistmodel:current_track_started called with no row number "
f"({track_sequence.now=})"
)
return
row_number = track_sequence.current.row_number
# Check for OBS scene change
log.debug("Call OBS scene change")
@ -287,29 +266,23 @@ class PlaylistModel(QAbstractTableModel):
with db.Session() as session:
# Update Playdates in database
log.debug("update playdates")
Playdates(session, track_sequence.now.track_id)
Playdates(session, track_sequence.current.track_id)
# Mark track as played in playlist
log.debug("Mark track as played")
plr = session.get(PlaylistRows, track_sequence.now.plr_id)
plr = session.get(PlaylistRows, track_sequence.current.plr_id)
if plr:
plr.played = True
self.refresh_row(session, plr.plr_rownum)
else:
log.error(f"Can't retrieve plr, {track_sequence.now.plr_id=}")
# Update track times
log.debug("Update track times")
if prd:
prd.start_time = track_sequence.now.start_time
prd.end_time = track_sequence.now.end_time
log.error(f"Can't retrieve plr, {track_sequence.current.plr_id=}")
# Update colour and times for current row
self.invalidate_row(row_number)
# Update previous row in case we're hiding played rows
if track_sequence.previous.plr_rownum:
self.invalidate_row(track_sequence.previous.plr_rownum)
if track_sequence.previous and track_sequence.previous.row_number:
self.invalidate_row(track_sequence.previous.row_number)
# Update all other track times
self.update_track_times()
@ -404,7 +377,7 @@ class PlaylistModel(QAbstractTableModel):
self.reset_track_sequence_row_numbers()
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
def display_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant:
"""
Return text for display
"""
@ -442,14 +415,20 @@ class PlaylistModel(QAbstractTableModel):
return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
return QVariant()
if column == Col.INTRO.value:
if prd.intro:
return QVariant(f"{prd.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}")
else:
return QVariant()
dispatch_table = {
Col.START_GAP.value: QVariant(prd.start_gap),
Col.TITLE.value: QVariant(prd.title),
Col.ARTIST.value: QVariant(prd.artist),
Col.BITRATE.value: QVariant(prd.bitrate),
Col.DURATION.value: QVariant(ms_to_mmss(prd.duration)),
Col.LAST_PLAYED.value: QVariant(get_relative_date(prd.lastplayed)),
Col.BITRATE.value: QVariant(prd.bitrate),
Col.NOTE.value: QVariant(prd.note),
Col.START_GAP.value: QVariant(prd.start_gap),
Col.TITLE.value: QVariant(prd.title),
}
if column in dispatch_table:
return dispatch_table[column]
@ -471,7 +450,7 @@ class PlaylistModel(QAbstractTableModel):
super().endResetModel()
self.reset_track_sequence_row_numbers()
def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
def edit_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant:
"""
Return text for editing
"""
@ -481,6 +460,8 @@ class PlaylistModel(QAbstractTableModel):
if self.is_header_row(row) and column == HEADER_NOTES_COLUMN:
return QVariant(prd.note)
if column == Col.INTRO.value:
return QVariant(prd.intro)
if column == Col.TITLE.value:
return QVariant(prd.title)
if column == Col.ARTIST.value:
@ -503,12 +484,17 @@ class PlaylistModel(QAbstractTableModel):
| Qt.ItemFlag.ItemIsSelectable
| Qt.ItemFlag.ItemIsDragEnabled
)
if index.column() in [Col.TITLE.value, Col.ARTIST.value, Col.NOTE.value]:
if index.column() in [
Col.TITLE.value,
Col.ARTIST.value,
Col.NOTE.value,
Col.INTRO.value,
]:
return default | Qt.ItemFlag.ItemIsEditable
return default
def font_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
def font_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant:
"""
Return font
"""
@ -567,13 +553,20 @@ class PlaylistModel(QAbstractTableModel):
log.debug(f"get_new_row_number() return: {new_row_number=}")
return new_row_number
def get_row_info(self, row_number: int) -> PlaylistRowData:
def get_row_info(self, row_number: int) -> _PlaylistRowData:
"""
Return info about passed row
"""
return self.playlist_rows[row_number]
def get_row_track_id(self, row_number: int) -> Optional[int]:
"""
Return id of track associated with row or None if no track associated
"""
return self.playlist_rows[row_number].track_id
def get_row_track_path(self, row_number: int) -> str:
"""
Return path of track associated with row or empty string if no track associated
@ -619,6 +612,8 @@ class PlaylistModel(QAbstractTableModel):
if orientation == Qt.Orientation.Horizontal:
if section == Col.START_GAP.value:
return QVariant(Config.HEADER_START_GAP)
if section == Col.INTRO.value:
return QVariant(Config.HEADER_INTRO)
elif section == Col.TITLE.value:
return QVariant(Config.HEADER_TITLE)
elif section == Col.ARTIST.value:
@ -648,7 +643,7 @@ class PlaylistModel(QAbstractTableModel):
return QVariant()
def header_text(self, prd: PlaylistRowData) -> str:
def header_text(self, prd: _PlaylistRowData) -> str:
"""
Process possible section timing directives embeded in header
"""
@ -692,16 +687,16 @@ class PlaylistModel(QAbstractTableModel):
# calculate end time if all tracks are played.
end_time_str = ""
if (
track_sequence.now.plr_rownum
and track_sequence.now.end_time
track_sequence.current
and track_sequence.current.end_time
and (
row_number
< track_sequence.now.plr_rownum
< track_sequence.current.row_number
< prd.plr_rownum
)
):
section_end_time = (
track_sequence.now.end_time
track_sequence.current.end_time
+ dt.timedelta(milliseconds=duration)
)
end_time_str = (
@ -819,7 +814,7 @@ class PlaylistModel(QAbstractTableModel):
return self.playlist_rows[row_number].played
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
def is_track_in_playlist(self, track_id: int) -> Optional[_PlaylistRowData]:
"""
If this track_id is in the playlist, return the PlaylistRowData object
else return None
@ -895,14 +890,16 @@ class PlaylistModel(QAbstractTableModel):
row_map[old_row] = new_row
# Check to see whether any rows in track_sequence have moved
if track_sequence.previous.plr_rownum in row_map:
track_sequence.previous.plr_rownum = row_map[
track_sequence.previous.plr_rownum
if track_sequence.previous and track_sequence.previous.row_number in row_map:
track_sequence.previous.row_number = row_map[
track_sequence.previous.row_number
]
if track_sequence.now.plr_rownum in row_map:
track_sequence.now.plr_rownum = row_map[track_sequence.now.plr_rownum]
if track_sequence.next.plr_rownum in row_map:
track_sequence.next.plr_rownum = row_map[track_sequence.next.plr_rownum]
if track_sequence.current and track_sequence.current.row_number in row_map:
track_sequence.current.row_number = row_map[
track_sequence.current.row_number
]
if track_sequence.next and track_sequence.next.row_number in row_map:
track_sequence.next.row_number = row_map[track_sequence.next.row_number]
# For SQLAlchemy, build a list of dictionaries that map plrid to
# new row number:
@ -962,7 +959,10 @@ class PlaylistModel(QAbstractTableModel):
self.playlist_id,
[self.playlist_rows[a].plrid for a in row_group],
):
if plr.id == track_sequence.now.plr_id:
if (
track_sequence.current
and plr.id == track_sequence.current.plr_id
):
# Don't move current track
continue
plr.playlist_id = to_playlist_id
@ -983,7 +983,7 @@ class PlaylistModel(QAbstractTableModel):
self.update_track_times()
def move_track_add_note(
self, new_row_number: int, existing_prd: PlaylistRowData, note: str
self, new_row_number: int, existing_prd: _PlaylistRowData, note: str
) -> None:
"""
Move existing_prd track to new_row_number and append note to any existing note
@ -1007,7 +1007,10 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id)
def move_track_to_header(
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
self,
header_row_number: int,
existing_prd: _PlaylistRowData,
note: Optional[str],
) -> None:
"""
Add the existing_prd track details to the existing header at header_row_number
@ -1069,10 +1072,10 @@ class PlaylistModel(QAbstractTableModel):
log.info("previous_track_ended()")
# Sanity check
if not track_sequence.previous.track_id:
if not track_sequence.previous:
log.error("playlistmodel:previous_track_ended called with no current track")
return
if track_sequence.previous.plr_rownum is None:
if track_sequence.previous.row_number is None:
log.error(
"playlistmodel:previous_track_ended called with no row number "
f"({track_sequence.previous=})"
@ -1080,7 +1083,7 @@ class PlaylistModel(QAbstractTableModel):
return
# Update display
self.invalidate_row(track_sequence.previous.plr_rownum)
self.invalidate_row(track_sequence.previous.row_number)
def refresh_data(self, session: db.session):
"""Populate dicts for data calls"""
@ -1088,13 +1091,13 @@ class PlaylistModel(QAbstractTableModel):
# Populate self.playlist_rows with playlist data
self.playlist_rows.clear()
for p in PlaylistRows.deep_rows(session, self.playlist_id):
self.playlist_rows[p.plr_rownum] = PlaylistRowData(p)
self.playlist_rows[p.plr_rownum] = _PlaylistRowData(p)
def refresh_row(self, session, row_number):
"""Populate dict for one row from database"""
p = PlaylistRows.deep_row(session, self.playlist_id, row_number)
self.playlist_rows[row_number] = PlaylistRowData(p)
self.playlist_rows[row_number] = _PlaylistRowData(p)
def remove_track(self, row_number: int) -> None:
"""
@ -1134,21 +1137,23 @@ class PlaylistModel(QAbstractTableModel):
log.debug("reset_track_sequence_row_numbers()")
# Check the track_sequence next, now and previous plrs and
# Check the track_sequence next, current and previous plrs and
# update the row number
with db.Session() as session:
if track_sequence.next.plr_rownum:
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
if track_sequence.next and track_sequence.next.row_number:
next_plr = session.get(PlaylistRows, track_sequence.next.row_number)
if next_plr:
track_sequence.next.plr_rownum = next_plr.plr_rownum
if track_sequence.now.plr_rownum:
now_plr = session.get(PlaylistRows, track_sequence.now.plr_id)
track_sequence.next.row_number = next_plr.plr_rownum
if track_sequence.current and track_sequence.current.row_number:
now_plr = session.get(PlaylistRows, track_sequence.current.row_number)
if now_plr:
track_sequence.now.plr_rownum = now_plr.plr_rownum
if track_sequence.previous.plr_rownum:
previous_plr = session.get(PlaylistRows, track_sequence.previous.plr_id)
track_sequence.current.row_number = now_plr.plr_rownum
if track_sequence.previous and track_sequence.previous.row_number:
previous_plr = session.get(
PlaylistRows, track_sequence.previous.row_number
)
if previous_plr:
track_sequence.previous.plr_rownum = previous_plr.plr_rownum
track_sequence.previous.row_number = previous_plr.plr_rownum
self.update_track_times()
@ -1229,59 +1234,60 @@ class PlaylistModel(QAbstractTableModel):
Set row_number as next track. If row_number is None, clear next track.
"""
log.info(f"set_next_row({row_number=})")
next_row_was = track_sequence.next.plr_rownum
log.debug(f"set_next_row({row_number=})")
if row_number is None:
if next_row_was is None:
# Clear next track
if track_sequence.next:
track_sequence.next = None
else:
return
track_sequence.next = PlaylistTrack()
self.signals.next_track_changed_signal.emit()
return
# Update track_sequence
with db.Session() as session:
track_sequence.next = PlaylistTrack()
else:
# Get plrid of row
try:
plrid = self.playlist_rows[row_number].plrid
prd = self.playlist_rows[row_number]
except IndexError:
log.error(
f"playlistmodel.set_next_track({row_number=}, "
f"{self.playlist_id=}"
"IndexError"
)
return
plr = session.get(PlaylistRows, plrid)
if plr:
# Check this isn't a header row
if self.is_header_row(row_number):
if prd.track_id is None or prd.plr_rownum is None:
log.error(
"Tried to set next row on header row: "
f"playlistmodel.set_next_track({row_number=}, "
f"{self.playlist_id=}"
"No track / row number "
f"{self.playlist_id=}, {prd.track_id=}, {prd.plr_rownum=}"
)
return
# Check track is readable
if file_is_unreadable(plr.track.path):
log.error(
"Tried to set next row on unreadable row: "
f"playlistmodel.set_next_track({row_number=}, "
f"{self.playlist_id=}"
)
old_next_row: Optional[int] = None
if track_sequence.next:
old_next_row = track_sequence.next.row_number
with db.Session() as session:
try:
track_sequence.next = MainTrackManager(session, prd.plrid)
self.invalidate_row(row_number)
except ValueError as e:
log.error(f"Error creating PlaylistTrack({prd=}): ({str(e)})")
return
track_sequence.next.set_plr(session, plr)
self.signals.next_track_changed_signal.emit()
self.signals.search_wikipedia_signal.emit(
self.playlist_rows[row_number].title
)
if old_next_row:
self.invalidate_row(old_next_row)
self.invalidate_row(row_number)
if next_row_was is not None:
self.invalidate_row(next_row_was)
self.signals.next_track_changed_signal.emit()
self.update_track_times()
def setData(
self, index: QModelIndex, value: QVariant, role: int = Qt.ItemDataRole.EditRole
self,
index: QModelIndex,
value: str | float,
role: int = Qt.ItemDataRole.EditRole,
) -> bool:
"""
Update model with edited data
@ -1301,7 +1307,7 @@ class PlaylistModel(QAbstractTableModel):
return False
if plr.track_id:
if column == Col.TITLE.value or column == Col.ARTIST.value:
if column in [Col.TITLE.value, Col.ARTIST.value, Col.INTRO.value]:
track = session.get(Tracks, plr.track_id)
if not track:
print(f"Error retreiving track: {plr=}")
@ -1310,11 +1316,14 @@ class PlaylistModel(QAbstractTableModel):
track.title = str(value)
elif column == Col.ARTIST.value:
track.artist = str(value)
elif column == Col.INTRO.value:
track.intro = int(round(float(value), 1) * 1000)
else:
print(f"Error updating track: {column=}, {value=}")
return False
elif column == Col.NOTE.value:
plr.note = str(value)
else:
# This is a header row
if column == HEADER_NOTES_COLUMN:
@ -1382,7 +1391,7 @@ class PlaylistModel(QAbstractTableModel):
def supportedDropActions(self) -> Qt.DropAction:
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
def tooltip_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
def tooltip_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant:
"""
Return tooltip. Currently only used for last_played column.
"""
@ -1420,21 +1429,22 @@ class PlaylistModel(QAbstractTableModel):
prd = self.playlist_rows[row_number]
# Reset start_time if this is the current row
if row_number == track_sequence.now.plr_rownum:
prd.start_time = track_sequence.now.start_time
prd.end_time = track_sequence.now.end_time
if track_sequence.current:
if row_number == track_sequence.current.row_number:
prd.start_time = track_sequence.current.start_time
prd.end_time = track_sequence.current.end_time
update_rows.append(row_number)
if not next_start_time:
next_start_time = prd.end_time
continue
# Set start time for next row if we have a current track
if (
row_number == track_sequence.next.plr_rownum
and track_sequence.now.end_time
):
prd.start_time = track_sequence.now.end_time
prd.end_time = prd.start_time + dt.timedelta(milliseconds=prd.duration)
if track_sequence.next and track_sequence.current.end_time:
if row_number == track_sequence.next.row_number:
prd.start_time = track_sequence.current.end_time
prd.end_time = prd.start_time + dt.timedelta(
milliseconds=prd.duration
)
next_start_time = prd.end_time
update_rows.append(row_number)
continue
@ -1446,11 +1456,11 @@ class PlaylistModel(QAbstractTableModel):
# If we're between the current and next row, zero out
# times
if (
track_sequence.now.plr_rownum is not None
and track_sequence.next.plr_rownum is not None
and track_sequence.now.plr_rownum
track_sequence.current
and track_sequence.next
and track_sequence.current.row_number
< row_number
< track_sequence.next.plr_rownum
< track_sequence.next.row_number
):
prd.start_time = None
prd.end_time = None
@ -1525,7 +1535,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
if self.source_model.is_played_row(source_row):
# Don't hide current or next track
with db.Session() as session:
if track_sequence.next.plr_id:
if track_sequence.next:
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
if (
next_plr
@ -1533,8 +1543,10 @@ class PlaylistProxyModel(QSortFilterProxyModel):
and next_plr.playlist_id == self.source_model.playlist_id
):
return True
if track_sequence.now.plr_id:
now_plr = session.get(PlaylistRows, track_sequence.now.plr_id)
if track_sequence.current:
now_plr = session.get(
PlaylistRows, track_sequence.current.plr_id
)
if (
now_plr
and now_plr.plr_rownum == source_row
@ -1544,19 +1556,20 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# Don't hide previous track until
# HIDE_AFTER_PLAYING_OFFSET milliseconds after
# current track has started
if track_sequence.previous.plr_id:
if track_sequence.previous:
previous_plr = session.get(
PlaylistRows, track_sequence.previous.plr_id
)
if (
previous_plr
track_sequence.current
and previous_plr
and previous_plr.plr_rownum == source_row
and previous_plr.playlist_id
== self.source_model.playlist_id
):
if track_sequence.now.start_time:
if track_sequence.current.start_time:
if dt.datetime.now() > (
track_sequence.now.start_time
track_sequence.current.start_time
+ dt.timedelta(
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
)
@ -1609,7 +1622,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
def get_rows_duration(self, row_numbers: List[int]) -> int:
return self.source_model.get_rows_duration(row_numbers)
def get_row_info(self, row_number: int) -> PlaylistRowData:
def get_row_info(self, row_number: int) -> _PlaylistRowData:
return self.source_model.get_row_info(row_number)
def get_row_track_path(self, row_number: int) -> str:
@ -1635,7 +1648,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
def is_played_row(self, row_number: int) -> bool:
return self.source_model.is_played_row(row_number)
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
def is_track_in_playlist(self, track_id: int) -> Optional[_PlaylistRowData]:
return self.source_model.is_track_in_playlist(track_id)
def mark_unplayed(self, row_numbers: List[int]) -> None:
@ -1652,12 +1665,15 @@ class PlaylistProxyModel(QSortFilterProxyModel):
)
def move_track_add_note(
self, new_row_number: int, existing_prd: PlaylistRowData, note: str
self, new_row_number: int, existing_prd: _PlaylistRowData, note: str
) -> None:
return self.source_model.move_track_add_note(new_row_number, existing_prd, note)
def move_track_to_header(
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
self,
header_row_number: int,
existing_prd: _PlaylistRowData,
note: Optional[str],
) -> None:
return self.source_model.move_track_to_header(
header_row_number, existing_prd, note

View File

@ -17,24 +17,25 @@ from PyQt6.QtWidgets import (
QAbstractItemDelegate,
QAbstractItemView,
QApplication,
QDoubleSpinBox,
QHeaderView,
QMenu,
QMessageBox,
QPlainTextEdit,
QProxyStyle,
QStyle,
QStyledItemDelegate,
QStyleOption,
QStyleOptionViewItem,
QTableView,
QTableWidgetItem,
QWidget,
QProxyStyle,
QStyle,
QStyleOption,
)
# Third party imports
# App imports
from classes import MusicMusterSignals, track_sequence
from classes import Col, MusicMusterSignals
from config import Config
from dialogs import TrackSelectDialog
from helpers import (
@ -46,6 +47,7 @@ from helpers import (
from log import log
from models import db, Settings
from playlistmodel import PlaylistModel, PlaylistProxyModel
from trackmanager import track_sequence
if TYPE_CHECKING:
from musicmuster import Window
@ -73,15 +75,22 @@ class EscapeDelegate(QStyledItemDelegate):
Intercept createEditor call and make row just a little bit taller
"""
self.editor: QDoubleSpinBox | QPlainTextEdit
self.signals = MusicMusterSignals()
self.signals.enable_escape_signal.emit(False)
if isinstance(self.parent(), PlaylistTab):
p = cast(PlaylistTab, self.parent())
if isinstance(index.data(), str):
if index.column() == Col.INTRO.value:
self.editor = QDoubleSpinBox(parent)
self.editor.setDecimals(1)
self.editor.setSingleStep(0.1)
return self.editor
elif isinstance(index.data(), str):
self.editor = QPlainTextEdit(parent)
row = index.row()
row_height = p.rowHeight(row)
p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT)
self.editor = QPlainTextEdit(parent)
return self.editor
return super().createEditor(parent, option, index)
@ -107,10 +116,20 @@ class EscapeDelegate(QStyledItemDelegate):
self.closeEditor.emit(editor)
return True
elif key_event.key() == Qt.Key.Key_Escape:
if self.original_text == self.editor.toPlainText():
# No changes made
# Close editor if no changes have been made
data_modified = False
if isinstance(self.editor, QPlainTextEdit):
data_modified = (
self.original_model_data == self.editor.toPlainText()
)
elif isinstance(self.editor, QDoubleSpinBox):
data_modified = (
self.original_model_data == int(self.editor.value()) * 1000
)
if data_modified:
self.closeEditor.emit(editor)
return True
discard_edits = QMessageBox.question(
cast(QWidget, self.parent()), "Abandon edit", "Discard changes?"
)
@ -123,16 +142,22 @@ class EscapeDelegate(QStyledItemDelegate):
proxy_model = index.model()
edit_index = proxy_model.mapToSource(index)
self.original_text = self.source_model.data(
self.original_model_data = self.source_model.data(
edit_index, Qt.ItemDataRole.EditRole
)
editor.setPlainText(self.original_text.value())
if index.column() == Col.INTRO.value:
editor.setValue(self.original_model_data.value() / 1000)
else:
editor.setPlainText(self.original_model_data.value())
def setModelData(self, editor, model, index):
proxy_model = index.model()
edit_index = proxy_model.mapToSource(index)
if isinstance(self.editor, QPlainTextEdit):
value = editor.toPlainText().strip()
elif isinstance(self.editor, QDoubleSpinBox):
value = editor.value()
self.source_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
def updateEditorGeometry(self, editor, option, index):
@ -405,12 +430,18 @@ class PlaylistTab(QTableView):
header_row = proxy_model.is_header_row(model_row_number)
track_row = not header_row
current_row = model_row_number == track_sequence.now.plr_rownum
next_row = model_row_number == track_sequence.next.plr_rownum
if track_sequence.current:
this_is_current_row = model_row_number == track_sequence.current.row_number
else:
this_is_current_row = False
if track_sequence.next:
this_is_next_row = model_row_number == track_sequence.next.row_number
else:
this_is_next_row = False
track_path = self.source_model.get_row_info(model_row_number).path
# Open/import in/from Audacity
if track_row and not current_row:
if track_row and not this_is_current_row:
if track_path == self.musicmuster.audacity_file_path:
# This track was opened in Audacity
self._add_context_menu(
@ -427,7 +458,7 @@ class PlaylistTab(QTableView):
)
# Rescan
if track_row and not current_row:
if track_row and not this_is_current_row:
self._add_context_menu(
"Rescan track", lambda: self._rescan(model_row_number)
)
@ -436,11 +467,11 @@ class PlaylistTab(QTableView):
self.menu.addSeparator()
# Delete row
if not current_row and not next_row:
if not this_is_current_row and not this_is_next_row:
self._add_context_menu("Delete row", lambda: self._delete_rows())
# Remove track from row
if track_row and not current_row and not next_row:
if track_row and not this_is_current_row and not this_is_next_row:
self._add_context_menu(
"Remove track from row",
lambda: proxy_model.remove_track(model_row_number),
@ -461,7 +492,7 @@ class PlaylistTab(QTableView):
)
# Unmark as next
if next_row:
if this_is_next_row:
self._add_context_menu(
"Unmark as next track", lambda: self._unmark_as_next()
)
@ -600,6 +631,26 @@ class PlaylistTab(QTableView):
self.source_model.delete_rows(self.selected_model_row_numbers())
self.clear_selection()
def get_selected_row_and_track_id(self) -> Optional[tuple[int, int]]:
"""
Return the (row_number, track_id) of the selected row. If no row selected or selected
row does not have a track, return None.
"""
row_number = self.source_model_selected_row_number()
if row_number is None:
result = None
else:
track_id = self.source_model.get_row_track_id(row_number)
if not track_id:
result = None
else:
result = (row_number, track_id)
log.debug(f"get_selected_row_and_track_id() returned: {result=}")
return result
def get_selected_row_track_path(self) -> str:
"""
Return the path of the selected row. If no row selected or selected
@ -617,6 +668,17 @@ class PlaylistTab(QTableView):
log.debug(f"get_selected_row_track_path() returned: {result=}")
return result
def get_selected_row(self) -> Optional[int]:
"""
Return selected row number. If no rows or multiple rows selected, return None
"""
selected = self.get_selected_rows()
if len(selected) == 1:
return selected[0]
else:
return None
def get_selected_rows(self) -> List[int]:
"""Return a list of model-selected row numbers sorted by row"""

645
app/trackmanager.py Normal file
View File

@ -0,0 +1,645 @@
# Standard library imports
from __future__ import annotations
import datetime as dt
import threading
from time import sleep
from typing import Optional
# Third party imports
import numpy as np
import pyqtgraph as pg # type: ignore
import vlc # type: ignore
# PyQt imports
from PyQt6.QtCore import (
pyqtSignal,
QObject,
QRunnable,
QThread,
QThreadPool,
)
# App imports
from classes import MusicMusterSignals
from config import Config
from log import log
from models import db, PlaylistRows, Tracks
from helpers import (
file_is_unreadable,
get_audio_segment,
)
lock = threading.Lock()
class _AddFadeCurve(QObject):
"""
Initialising a fade curve introduces a noticeable delay so carry out in
a thread.
"""
finished = pyqtSignal()
def __init__(
self,
track_manager: _TrackManager,
track_path: str,
track_fade_at: int,
track_silence_at: int,
):
super().__init__()
self.track_manager = track_manager
self.track_path = track_path
self.track_fade_at = track_fade_at
self.track_silence_at = track_silence_at
def run(self):
"""
Create fade curve and add to PlaylistTrack object
"""
fc = _FadeCurve(self.track_path, self.track_fade_at, self.track_silence_at)
if not fc:
log.error(f"Failed to create FadeCurve for {self.track_path=}")
else:
self.track_manager.fade_graph = fc
self.finished.emit()
class _FadeCurve:
GraphWidget = None
def __init__(
self, track_path: str, track_fade_at: int, track_silence_at: int
) -> None:
"""
Set up fade graph array
"""
audio = get_audio_segment(track_path)
if not audio:
log.error(f"FadeCurve: could not get audio for {track_path=}")
return None
# Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE
# milliseconds before fade starts to silence
self.start_ms: int = max(
0, track_fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1
)
self.end_ms: int = track_silence_at
self.audio_segment = audio[self.start_ms : self.end_ms]
self.graph_array = np.array(self.audio_segment.get_array_of_samples())
# Calculate the factor to map milliseconds of track to array
self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms)
self.region = None
def clear(self) -> None:
"""Clear the current graph"""
if self.GraphWidget:
self.GraphWidget.clear()
def plot(self):
self.curve = self.GraphWidget.plot(self.graph_array)
self.curve.setPen(Config.FADE_CURVE_FOREGROUND)
def tick(self, play_time) -> None:
"""Update volume fade curve"""
if not self.GraphWidget:
return
ms_of_graph = play_time - self.start_ms
if ms_of_graph < 0:
return
if self.region is None:
# Create the region now that we're into fade
self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)])
self.GraphWidget.addItem(self.region)
# Update region position
self.region.setRegion([0, ms_of_graph * self.ms_to_array_factor])
class _FadeTrack(QRunnable):
def __init__(self, player: vlc.MediaPlayer, fade_seconds) -> None:
super().__init__()
self.player = player
self.fade_seconds = fade_seconds
def run(self) -> None:
"""
Implementation of fading the player
"""
if not self.player:
return
# Reduce volume logarithmically
total_steps = self.fade_seconds * Config.FADEOUT_STEPS_PER_SECOND
db_reduction_per_step = Config.FADEOUT_DB / total_steps
reduction_factor_per_step = pow(10, (db_reduction_per_step / 20))
volume = self.player.audio_get_volume()
for i in range(1, total_steps + 1):
self.player.audio_set_volume(
int(volume * pow(reduction_factor_per_step, i))
)
sleep(1 / Config.FADEOUT_STEPS_PER_SECOND)
self.player.stop()
log.debug(f"Releasing player {self.player=}")
self.player.release()
class _Music:
"""
Manage the playing of music tracks
"""
def __init__(self, name) -> None:
self.VLC = vlc.Instance()
self.VLC.set_user_agent(name, name)
self.player = None
self.name = name
self.max_volume = Config.VLC_VOLUME_DEFAULT
self.start_dt: Optional[dt.datetime] = None
def adjust_by_ms(self, ms: int) -> None:
"""Move player position by ms milliseconds"""
if not self.player:
return
elapsed_ms = self.get_playtime()
position = self.get_position()
if not position:
position = 0
new_position = max(0, position + ((position * ms) / elapsed_ms))
self.set_position(new_position)
# Adjus start time so elapsed time calculations are correct
if new_position == 0:
self.start_dt = dt.datetime.now()
else:
self.start_dt -= dt.timedelta(milliseconds=ms)
def stop(self) -> None:
"""Immediately stop playing"""
log.debug(f"Music[{self.name}].stop()")
self.start_dt = None
if not self.player:
return
p = self.player
self.player = None
self.start_dt = None
with lock:
p.stop()
p.release()
p = None
def fade(self, fade_seconds: int) -> None:
"""
Fade the currently playing track.
The actual management of fading runs in its own thread so as not
to hold up the UI during the fade.
"""
if not self.player:
return
if not self.player.get_position() > 0 and self.player.is_playing():
return
if fade_seconds <= 0:
self.stop()
return
# Take a copy of current player to allow another track to be
# started without interfering here
with lock:
p = self.player
self.player = None
pool = QThreadPool.globalInstance()
fader = _FadeTrack(p, fade_seconds=fade_seconds)
pool.start(fader)
self.start_dt = None
def get_playtime(self) -> int:
"""
Return number of milliseconds current track has been playing or
zero if not playing. The vlc function get_time() only updates 3-4
times a second; this function has much better resolution.
"""
if self.start_dt is None:
return 0
now = dt.datetime.now()
elapsed_seconds = (now - self.start_dt).total_seconds()
return int(elapsed_seconds * 1000)
def get_position(self) -> Optional[float]:
"""Return current position"""
if not self.player:
return None
return self.player.get_position()
def is_playing(self) -> bool:
"""
Return True if we're playing
"""
if not self.player:
return False
# There is a discrete time between starting playing a track and
# player.is_playing() returning True, so assume playing if less
# than Config.PLAY_SETTLE microseconds have passed since
# starting play.
return self.start_dt is not None and (
self.player.is_playing()
or (dt.datetime.now() - self.start_dt)
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
)
def play(
self,
path: str,
start_time: dt.datetime,
position: Optional[float] = None,
) -> None:
"""
Start playing the track at path.
Log and return if path not found.
start_time ensures our version and our caller's version of
the start time is the same
"""
log.debug(f"Music[{self.name}].play({path=}, {position=}")
if file_is_unreadable(path):
log.error(f"play({path}): path not readable")
return None
media = self.VLC.media_new_path(path)
self.player = media.player_new_from_media()
if self.player:
_ = self.player.play()
self.set_volume(self.max_volume)
if position:
self.player.set_position(position)
self.start_dt = start_time
# For as-yet unknown reasons. sometimes the volume gets
# reset to zero within 200mS or so of starting play. This
# only happened since moving to Debian 12, which uses
# Pipewire for sound (which may be irrelevant).
# It has been known for the volume to need correcting more
# than once in the first 200mS.
for _ in range(3):
if self.player:
volume = self.player.audio_get_volume()
if volume < Config.VLC_VOLUME_DEFAULT:
self.set_volume(Config.VLC_VOLUME_DEFAULT)
log.error(f"Reset from {volume=}")
sleep(0.1)
def set_position(self, position: int) -> None:
"""
Set player position
"""
if self.player:
self.player.set_position(position)
def set_volume(self, volume=None, set_default=True) -> None:
"""Set maximum volume used for player"""
if not self.player:
return
if set_default:
self.max_volume = volume
if volume is None:
volume = Config.VLC_VOLUME_DEFAULT
self.player.audio_set_volume(volume)
# Ensure volume correct
# For as-yet unknown reasons. sometimes the volume gets
# reset to zero within 200mS or so of starting play. This
# only happened since moving to Debian 12, which uses
# Pipewire for sound (which may be irrelevant).
for _ in range(3):
current_volume = self.player.audio_get_volume()
if current_volume < volume:
self.player.audio_set_volume(volume)
log.debug(f"Reset from {volume=}")
sleep(0.1)
class _TrackManager:
"""
Object to manage active playlist tracks,
typically the previous, current and next track.
"""
def __init__(
self, session: db.Session, player_name: str, track_id: int, row_number: int
) -> None:
"""
Initialises data structure.
Define a player.
Raise ValueError if no track in passed plr.
"""
track = session.get(Tracks, track_id)
if not track:
raise ValueError(f"_TrackPlayer: unable to retreived {track_id=}")
self.player_name = player_name
self.row_number = row_number
self.artist = track.artist
self.bitrate = track.bitrate
self.duration = track.duration
self.fade_at = track.fade_at
self.intro = track.intro
self.path = track.path
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.title = track.title
self.track_id = track.id
self.end_time: Optional[dt.datetime] = None
self.fade_graph: Optional[_FadeCurve] = None
self.fade_graph_start_updates: Optional[dt.datetime] = None
self.resume_marker: Optional[float]
self.start_time: Optional[dt.datetime] = None
self.end_of_track_signalled: bool = False
self.signals = MusicMusterSignals()
# Initialise player
self.player = _Music(name=player_name)
# Initialise and add FadeCurve in a thread as it's slow
self.fadecurve_thread = QThread()
self.worker = _AddFadeCurve(
self,
track_path=track.path,
track_fade_at=track.fade_at,
track_silence_at=track.silence_at,
)
self.worker.moveToThread(self.fadecurve_thread)
self.fadecurve_thread.started.connect(self.worker.run)
self.worker.finished.connect(self.fadecurve_thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater)
self.fadecurve_thread.start()
def check_for_end_of_track(self) -> None:
"""
Check whether track has ended. If so, emit track_ended_signal
"""
if self.start_time is None:
return
if self.end_of_track_signalled:
return
if not self.player.is_playing():
self.start_time = None
if self.fade_graph:
self.fade_graph.clear()
self.signals.track_ended_signal.emit()
self.end_of_track_signalled = True
def drop3db(self, enable: bool) -> None:
"""
If enable is true, drop output by 3db else restore to full volume
"""
if enable:
self.player.set_volume(volume=Config.VLC_VOLUME_DROP3db, set_default=False)
else:
self.player.set_volume(volume=Config.VLC_VOLUME_DEFAULT, set_default=False)
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
"""Fade music"""
self.resume_marker = self.player.get_position()
self.player.fade(fade_seconds)
self.signals.track_ended_signal.emit()
def is_playing(self) -> bool:
"""
Return True if we're currently playing else False
"""
if self.start_time is None:
return False
return self.player.is_playing()
def move_back(self, ms: int = Config.PREVIEW_BACK_MS) -> None:
"""
Rewind player by ms milliseconds
"""
self.player.adjust_by_ms(ms * -1)
def move_forward(self, ms: int = Config.PREVIEW_ADVANCE_MS) -> None:
"""
Rewind player by ms milliseconds
"""
self.player.adjust_by_ms(ms)
def move_to_intro_end(self, buffer: int = Config.PREVIEW_END_BUFFER_MS) -> None:
"""
Move play position to 'buffer' milliseconds before end of intro.
If no intro defined, do nothing.
"""
if self.intro is None:
return
new_position = max(0, self.intro - Config.PREVIEW_END_BUFFER_MS)
self.player.adjust_by_ms(new_position - self.time_playing())
def play(self, position: Optional[float] = None) -> None:
"""Play track"""
now = dt.datetime.now()
self.start_time = now
self.player.play(self.path, start_time=now, position=position)
self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration)
# Calculate time fade_graph should start updating
if self.fade_at:
update_graph_at_ms = max(
0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1
)
self.fade_graph_start_updates = now + dt.timedelta(
milliseconds=update_graph_at_ms
)
def restart(self) -> None:
"""
Restart player
"""
self.player.adjust_by_ms(self.time_playing() * -1)
def stop(self, fade_seconds: int = 0) -> None:
"""
Stop this track playing
"""
self.resume_marker = self.player.get_position()
self.fade(fade_seconds)
# Reset fade graph
if self.fade_graph:
self.fade_graph.clear()
def time_playing(self) -> int:
"""
Return time track has been playing in milliseconds, zero if not playing
"""
if self.start_time is None:
return 0
return self.player.get_playtime()
def time_remaining_intro(self) -> int:
"""
Return milliseconds of intro remaining. Return 0 if no intro time in track
record or if intro has finished.
"""
if not self.intro:
return 0
return max(0, self.intro - self.time_playing())
def time_to_fade(self) -> int:
"""
Return milliseconds until fade time. Return zero if we're not playing.
"""
if self.start_time is None:
return 0
return self.fade_at - self.time_playing()
def time_to_silence(self) -> int:
"""
Return milliseconds until silent. Return zero if we're not playing.
"""
if self.start_time is None:
return 0
return self.silence_at - self.time_playing()
def update_fade_graph(self) -> None:
"""
Update fade graph
"""
if (
not self.is_playing()
or not self.fade_graph_start_updates
or not self.fade_graph
):
return
now = dt.datetime.now()
if self.fade_graph_start_updates > now:
return
self.fade_graph.tick(self.time_playing())
class MainTrackManager(_TrackManager):
"""
Manage playing tracks from the playlist with associated data
"""
def __init__(self, session: db.Session, plr_id: int) -> None:
"""
Set up manager for playlist tracks
"""
# Ensure we have a track
plr = session.get(PlaylistRows, plr_id)
if not plr:
raise ValueError(f"PlaylistTrack: unable to retreive plr {plr_id=}")
self.track_id: int = plr.track_id
super().__init__(
session=session,
player_name=Config.VLC_MAIN_PLAYER_NAME,
track_id=self.track_id,
row_number=plr.plr_rownum,
)
# Save non-track plr info
self.plr_id: int = plr.id
self.playlist_id: int = plr.playlist_id
def __repr__(self) -> str:
return (
f"<MainTrackManager(plr_id={self.plr_id}, playlist_id={self.playlist_id}, "
f"row_number={self.row_number}>"
)
class PreviewTrackManager(_TrackManager):
"""
Manage previewing tracks
"""
def __init__(self, session: db.Session, track_id: int, row_number: int) -> None:
super().__init__(
session=session,
player_name=Config.VLC_PREVIEW_PLAYER_NAME,
track_id=track_id,
row_number=row_number,
)
def __repr__(self) -> str:
return f"<PreviewTrackManager(track_id={self.track_id}>"
class TrackSequence:
next: Optional[MainTrackManager] = None
current: Optional[MainTrackManager] = None
previous: Optional[MainTrackManager] = None
track_sequence = TrackSequence()

View File

@ -1,5 +1,9 @@
<RCC>
<qresource prefix="icons">
<file>star.png</file>
<file>star_empty.png</file>
<file>record-red-button.png</file>
<file>record-button.png</file>
<file alias="headphones">headphone-symbol.png</file>
<file alias="musicmuster">musicmuster.png</file>
<file alias="stopsign">stopsign.png</file>

File diff suppressed because it is too large Load Diff

View File

@ -229,10 +229,16 @@ padding-left: 8px;</string>
</item>
<item>
<widget class="QFrame" name="frame_2">
<property name="minimumSize">
<size>
<width>0</width>
<height>131</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>230</width>
<height>16777215</height>
<height>131</height>
</size>
</property>
<property name="frameShape">
@ -241,13 +247,13 @@ padding-left: 8px;</string>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QLabel" name="lblTOD">
<property name="minimumSize">
<size>
<width>208</width>
<height>109</height>
<height>0</height>
</size>
</property>
<property name="font">
@ -263,6 +269,27 @@ padding-left: 8px;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_elapsed_timer">
<property name="font">
<font>
<family>FreeSans</family>
<pointsize>18</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">color: black;</string>
</property>
<property name="text">
<string>00:00 / 00:00</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -435,20 +462,234 @@ padding-left: 8px;</string>
</widget>
</item>
<item>
<widget class="QLabel" name="label_elapsed_timer">
<widget class="QGroupBox" name="groupBoxIntroControls">
<property name="minimumSize">
<size>
<width>132</width>
<height>46</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>132</width>
<height>46</height>
</size>
</property>
<property name="title">
<string/>
</property>
<widget class="QPushButton" name="btnPreviewStart">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string>&lt;&lt;</string>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewArm">
<property name="geometry">
<rect>
<x>44</x>
<y>0</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/record-button.png</normaloff>
<normalon>:/icons/record-red-button.png</normalon>:/icons/record-button.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewEnd">
<property name="geometry">
<rect>
<x>88</x>
<y>0</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string>&gt;&gt;</string>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewBack">
<property name="geometry">
<rect>
<x>0</x>
<y>23</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string>&lt;</string>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewMark">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>44</x>
<y>23</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/star.png</normalon>
<disabledoff>:/icons/star_empty.png</disabledoff>
</iconset>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewFwd">
<property name="geometry">
<rect>
<x>88</x>
<y>23</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string>&gt;</string>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_intro">
<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_9">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Intro</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_intro_timer">
<property name="font">
<font>
<family>FreeSans</family>
<pointsize>18</pointsize>
<pointsize>40</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">color: black;</string>
</property>
<property name="text">
<string>00:00 / 00:00</string>
<string>0:0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
@ -592,15 +833,9 @@ padding-left: 8px;</string>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<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">
<string>Silent</string>
</property>
@ -608,15 +843,9 @@ padding-left: 8px;</string>
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<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">
<font>
<family>FreeSans</family>
@ -632,6 +861,8 @@ padding-left: 8px;</string>
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
@ -661,7 +892,7 @@ padding-left: 8px;</string>
<property name="maximumSize">
<size>
<width>151</width>
<height>16777215</height>
<height>112</height>
</size>
</property>
<property name="frameShape">
@ -812,7 +1043,7 @@ padding-left: 8px;</string>
<action name="actionPlay_next">
<property name="icon">
<iconset>
<normaloff>../../../../.designer/backup/icon-play.png</normaloff>../../../../.designer/backup/icon-play.png</iconset>
<normaloff>../../../../../../.designer/backup/icon-play.png</normaloff>../../../../../../.designer/backup/icon-play.png</iconset>
</property>
<property name="text">
<string>&amp;Play next</string>
@ -836,7 +1067,7 @@ padding-left: 8px;</string>
<action name="actionInsertTrack">
<property name="icon">
<iconset>
<normaloff>../../../../.designer/backup/icon_search_database.png</normaloff>../../../../.designer/backup/icon_search_database.png</iconset>
<normaloff>../../../../../../.designer/backup/icon_search_database.png</normaloff>../../../../../../.designer/backup/icon_search_database.png</iconset>
</property>
<property name="text">
<string>Insert &amp;track...</string>
@ -848,7 +1079,7 @@ padding-left: 8px;</string>
<action name="actionAdd_file">
<property name="icon">
<iconset>
<normaloff>../../../../.designer/backup/icon_open_file.png</normaloff>../../../../.designer/backup/icon_open_file.png</iconset>
<normaloff>../../../../../../.designer/backup/icon_open_file.png</normaloff>../../../../../../.designer/backup/icon_open_file.png</iconset>
</property>
<property name="text">
<string>Add &amp;file</string>
@ -860,7 +1091,7 @@ padding-left: 8px;</string>
<action name="actionFade">
<property name="icon">
<iconset>
<normaloff>../../../../.designer/backup/icon-fade.png</normaloff>../../../../.designer/backup/icon-fade.png</iconset>
<normaloff>../../../../../../.designer/backup/icon-fade.png</normaloff>../../../../../../.designer/backup/icon-fade.png</iconset>
</property>
<property name="text">
<string>F&amp;ade</string>

View File

@ -15,7 +15,11 @@ class Ui_MainWindow(object):
MainWindow.resize(1280, 857)
MainWindow.setMinimumSize(QtCore.QSize(1280, 0))
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/icons/musicmuster"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon.addPixmap(
QtGui.QPixmap(":/icons/musicmuster"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
MainWindow.setWindowIcon(icon)
MainWindow.setStyleSheet("")
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
@ -27,39 +31,62 @@ class Ui_MainWindow(object):
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.previous_track_2.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.previous_track_2.sizePolicy().hasHeightForWidth()
)
self.previous_track_2.setSizePolicy(sizePolicy)
self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.previous_track_2.setFont(font)
self.previous_track_2.setStyleSheet("background-color: #f8d7da;\n"
"border: 1px solid rgb(85, 87, 83);")
self.previous_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.previous_track_2.setStyleSheet(
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.previous_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.previous_track_2.setObjectName("previous_track_2")
self.verticalLayout_3.addWidget(self.previous_track_2)
self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.current_track_2.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.current_track_2.sizePolicy().hasHeightForWidth()
)
self.current_track_2.setSizePolicy(sizePolicy)
self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.current_track_2.setFont(font)
self.current_track_2.setStyleSheet("background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);")
self.current_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.current_track_2.setStyleSheet(
"background-color: #d4edda;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.current_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.current_track_2.setObjectName("current_track_2")
self.verticalLayout_3.addWidget(self.current_track_2)
self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth())
@ -69,19 +96,29 @@ class Ui_MainWindow(object):
font.setFamily("Sans")
font.setPointSize(20)
self.next_track_2.setFont(font)
self.next_track_2.setStyleSheet("background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);")
self.next_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.next_track_2.setStyleSheet(
"background-color: #fff3cd;\n" "border: 1px solid rgb(85, 87, 83);"
)
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.verticalLayout_3.addWidget(self.next_track_2)
self.horizontalLayout_3.addLayout(self.verticalLayout_3)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrPreviousTrack.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.hdrPreviousTrack.sizePolicy().hasHeightForWidth()
)
self.hdrPreviousTrack.setSizePolicy(sizePolicy)
self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0))
self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215))
@ -89,32 +126,43 @@ class Ui_MainWindow(object):
font.setFamily("Sans")
font.setPointSize(20)
self.hdrPreviousTrack.setFont(font)
self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
"border: 1px solid rgb(85, 87, 83);")
self.hdrPreviousTrack.setStyleSheet(
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.hdrPreviousTrack.setText("")
self.hdrPreviousTrack.setWordWrap(False)
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
self.verticalLayout.addWidget(self.hdrPreviousTrack)
self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrCurrentTrack.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.hdrCurrentTrack.sizePolicy().hasHeightForWidth()
)
self.hdrCurrentTrack.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(20)
self.hdrCurrentTrack.setFont(font)
self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
self.hdrCurrentTrack.setStyleSheet(
"background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;\n"
"")
""
)
self.hdrCurrentTrack.setText("")
self.hdrCurrentTrack.setFlat(True)
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
self.verticalLayout.addWidget(self.hdrCurrentTrack)
self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth())
@ -122,30 +170,44 @@ class Ui_MainWindow(object):
font = QtGui.QFont()
font.setPointSize(20)
self.hdrNextTrack.setFont(font)
self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
self.hdrNextTrack.setStyleSheet(
"background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;")
"padding-left: 8px;"
)
self.hdrNextTrack.setText("")
self.hdrNextTrack.setFlat(True)
self.hdrNextTrack.setObjectName("hdrNextTrack")
self.verticalLayout.addWidget(self.hdrNextTrack)
self.horizontalLayout_3.addLayout(self.verticalLayout)
self.frame_2 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_2.setMaximumSize(QtCore.QSize(230, 16777215))
self.frame_2.setMinimumSize(QtCore.QSize(0, 131))
self.frame_2.setMaximumSize(QtCore.QSize(230, 131))
self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_2.setObjectName("frame_2")
self.gridLayout_2 = QtWidgets.QGridLayout(self.frame_2)
self.gridLayout_2.setObjectName("gridLayout_2")
self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.frame_2)
self.verticalLayout_10.setObjectName("verticalLayout_10")
self.lblTOD = QtWidgets.QLabel(parent=self.frame_2)
self.lblTOD.setMinimumSize(QtCore.QSize(208, 109))
self.lblTOD.setMinimumSize(QtCore.QSize(208, 0))
font = QtGui.QFont()
font.setPointSize(35)
self.lblTOD.setFont(font)
self.lblTOD.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.lblTOD.setObjectName("lblTOD")
self.gridLayout_2.addWidget(self.lblTOD, 0, 0, 1, 1)
self.verticalLayout_10.addWidget(self.lblTOD)
self.label_elapsed_timer = QtWidgets.QLabel(parent=self.frame_2)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(18)
font.setBold(False)
font.setWeight(50)
self.label_elapsed_timer.setFont(font)
self.label_elapsed_timer.setStyleSheet("color: black;")
self.label_elapsed_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_elapsed_timer.setObjectName("label_elapsed_timer")
self.verticalLayout_10.addWidget(self.label_elapsed_timer)
self.horizontalLayout_3.addWidget(self.frame_2)
self.gridLayout_4.addLayout(self.horizontalLayout_3, 0, 0, 1, 1)
self.frame_4 = QtWidgets.QFrame(parent=self.centralwidget)
@ -160,7 +222,12 @@ class Ui_MainWindow(object):
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)
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)
@ -205,24 +272,104 @@ class Ui_MainWindow(object):
self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
self.btnPreview.setMinimumSize(QtCore.QSize(132, 41))
icon1 = QtGui.QIcon()
icon1.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(icon1)
self.btnPreview.setIconSize(QtCore.QSize(30, 30))
self.btnPreview.setCheckable(True)
self.btnPreview.setObjectName("btnPreview")
self.verticalLayout_4.addWidget(self.btnPreview)
self.label_elapsed_timer = QtWidgets.QLabel(parent=self.FadeStopInfoFrame)
self.groupBoxIntroControls = QtWidgets.QGroupBox(parent=self.FadeStopInfoFrame)
self.groupBoxIntroControls.setMinimumSize(QtCore.QSize(132, 46))
self.groupBoxIntroControls.setMaximumSize(QtCore.QSize(132, 46))
self.groupBoxIntroControls.setTitle("")
self.groupBoxIntroControls.setObjectName("groupBoxIntroControls")
self.btnPreviewStart = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewStart.setGeometry(QtCore.QRect(0, 0, 44, 23))
self.btnPreviewStart.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewStart.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewStart.setObjectName("btnPreviewStart")
self.btnPreviewArm = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewArm.setGeometry(QtCore.QRect(44, 0, 44, 23))
self.btnPreviewArm.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setText("")
icon2 = QtGui.QIcon()
icon2.addPixmap(
QtGui.QPixmap(":/icons/record-button.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon2.addPixmap(
QtGui.QPixmap(":/icons/record-red-button.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.On,
)
self.btnPreviewArm.setIcon(icon2)
self.btnPreviewArm.setCheckable(True)
self.btnPreviewArm.setObjectName("btnPreviewArm")
self.btnPreviewEnd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewEnd.setGeometry(QtCore.QRect(88, 0, 44, 23))
self.btnPreviewEnd.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewEnd.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewEnd.setObjectName("btnPreviewEnd")
self.btnPreviewBack = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewBack.setGeometry(QtCore.QRect(0, 23, 44, 23))
self.btnPreviewBack.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewBack.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewBack.setObjectName("btnPreviewBack")
self.btnPreviewMark = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewMark.setEnabled(False)
self.btnPreviewMark.setGeometry(QtCore.QRect(44, 23, 44, 23))
self.btnPreviewMark.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setText("")
icon3 = QtGui.QIcon()
icon3.addPixmap(
QtGui.QPixmap(":/icons/star.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.On,
)
icon3.addPixmap(
QtGui.QPixmap(":/icons/star_empty.png"),
QtGui.QIcon.Mode.Disabled,
QtGui.QIcon.State.Off,
)
self.btnPreviewMark.setIcon(icon3)
self.btnPreviewMark.setObjectName("btnPreviewMark")
self.btnPreviewFwd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewFwd.setGeometry(QtCore.QRect(88, 23, 44, 23))
self.btnPreviewFwd.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewFwd.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewFwd.setObjectName("btnPreviewFwd")
self.verticalLayout_4.addWidget(self.groupBoxIntroControls)
self.horizontalLayout.addWidget(self.FadeStopInfoFrame)
self.frame_intro = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_intro.setMinimumSize(QtCore.QSize(152, 112))
self.frame_intro.setStyleSheet("")
self.frame_intro.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_intro.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_intro.setObjectName("frame_intro")
self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.frame_intro)
self.verticalLayout_9.setObjectName("verticalLayout_9")
self.label_7 = QtWidgets.QLabel(parent=self.frame_intro)
self.label_7.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_7.setObjectName("label_7")
self.verticalLayout_9.addWidget(self.label_7)
self.label_intro_timer = QtWidgets.QLabel(parent=self.frame_intro)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(18)
font.setPointSize(40)
font.setBold(False)
font.setWeight(50)
self.label_elapsed_timer.setFont(font)
self.label_elapsed_timer.setStyleSheet("color: black;")
self.label_elapsed_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_elapsed_timer.setObjectName("label_elapsed_timer")
self.verticalLayout_4.addWidget(self.label_elapsed_timer)
self.horizontalLayout.addWidget(self.FadeStopInfoFrame)
self.label_intro_timer.setFont(font)
self.label_intro_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_intro_timer.setObjectName("label_intro_timer")
self.verticalLayout_9.addWidget(self.label_intro_timer)
self.horizontalLayout.addWidget(self.frame_intro)
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))
@ -273,12 +420,13 @@ class Ui_MainWindow(object):
self.frame_silent.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_silent.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_silent.setObjectName("frame_silent")
self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.frame_silent)
self.verticalLayout_7.setObjectName("verticalLayout_7")
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.verticalLayout_7.addWidget(self.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)
@ -287,19 +435,25 @@ class Ui_MainWindow(object):
self.label_silent_timer.setFont(font)
self.label_silent_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_silent_timer.setObjectName("label_silent_timer")
self.verticalLayout_7.addWidget(self.label_silent_timer)
self.horizontalLayout.addWidget(self.frame_silent)
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.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.widgetFadeVolume.sizePolicy().hasHeightForWidth()
)
self.widgetFadeVolume.setSizePolicy(sizePolicy)
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
self.widgetFadeVolume.setObjectName("widgetFadeVolume")
self.horizontalLayout.addWidget(self.widgetFadeVolume)
self.frame = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame.setMinimumSize(QtCore.QSize(151, 0))
self.frame.setMaximumSize(QtCore.QSize(151, 16777215))
self.frame.setMaximumSize(QtCore.QSize(151, 112))
self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame.setObjectName("frame")
@ -308,17 +462,25 @@ class Ui_MainWindow(object):
self.btnFade = QtWidgets.QPushButton(parent=self.frame)
self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
icon2 = QtGui.QIcon()
icon2.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnFade.setIcon(icon2)
icon4 = QtGui.QIcon()
icon4.addPixmap(
QtGui.QPixmap(":/icons/fade"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnFade.setIcon(icon4)
self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade")
self.verticalLayout_5.addWidget(self.btnFade)
self.btnStop = QtWidgets.QPushButton(parent=self.frame)
self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
icon3 = QtGui.QIcon()
icon3.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnStop.setIcon(icon3)
icon5 = QtGui.QIcon()
icon5.addPixmap(
QtGui.QPixmap(":/icons/stopsign"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnStop.setIcon(icon5)
self.btnStop.setObjectName("btnStop")
self.verticalLayout_5.addWidget(self.btnStop)
self.horizontalLayout.addWidget(self.frame)
@ -342,41 +504,73 @@ class Ui_MainWindow(object):
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.actionPlay_next = QtGui.QAction(parent=MainWindow)
icon4 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionPlay_next.setIcon(icon4)
icon6 = QtGui.QIcon()
icon6.addPixmap(
QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionPlay_next.setIcon(icon6)
self.actionPlay_next.setObjectName("actionPlay_next")
self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
icon5 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionSkipToNext.setIcon(icon5)
icon7 = QtGui.QIcon()
icon7.addPixmap(
QtGui.QPixmap(":/icons/next"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionSkipToNext.setIcon(icon7)
self.actionSkipToNext.setObjectName("actionSkipToNext")
self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionInsertTrack.setIcon(icon6)
icon8 = QtGui.QIcon()
icon8.addPixmap(
QtGui.QPixmap(
"app/ui/../../../../../../.designer/backup/icon_search_database.png"
),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionInsertTrack.setIcon(icon8)
self.actionInsertTrack.setObjectName("actionInsertTrack")
self.actionAdd_file = QtGui.QAction(parent=MainWindow)
icon7 = QtGui.QIcon()
icon7.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionAdd_file.setIcon(icon7)
icon9 = QtGui.QIcon()
icon9.addPixmap(
QtGui.QPixmap(
"app/ui/../../../../../../.designer/backup/icon_open_file.png"
),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionAdd_file.setIcon(icon9)
self.actionAdd_file.setObjectName("actionAdd_file")
self.actionFade = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon()
icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionFade.setIcon(icon8)
icon10 = QtGui.QIcon()
icon10.addPixmap(
QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionFade.setIcon(icon10)
self.actionFade.setObjectName("actionFade")
self.actionStop = QtGui.QAction(parent=MainWindow)
icon9 = QtGui.QIcon()
icon9.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionStop.setIcon(icon9)
icon11 = QtGui.QIcon()
icon11.addPixmap(
QtGui.QPixmap(":/icons/stop"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionStop.setIcon(icon11)
self.actionStop.setObjectName("actionStop")
self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
self.action_Clear_selection.setObjectName("action_Clear_selection")
self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
icon10 = QtGui.QIcon()
icon10.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.action_Resume_previous.setIcon(icon10)
icon12 = QtGui.QIcon()
icon12.addPixmap(
QtGui.QPixmap(":/icons/previous"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.action_Resume_previous.setIcon(icon12)
self.action_Resume_previous.setObjectName("action_Resume_previous")
self.actionE_xit = QtGui.QAction(parent=MainWindow)
self.actionE_xit.setObjectName("actionE_xit")
@ -422,7 +616,9 @@ class Ui_MainWindow(object):
self.actionImport = QtGui.QAction(parent=MainWindow)
self.actionImport.setObjectName("actionImport")
self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow)
self.actionDownload_CSV_of_played_tracks.setObjectName("actionDownload_CSV_of_played_tracks")
self.actionDownload_CSV_of_played_tracks.setObjectName(
"actionDownload_CSV_of_played_tracks"
)
self.actionSearch = QtGui.QAction(parent=MainWindow)
self.actionSearch.setObjectName("actionSearch")
self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow)
@ -450,9 +646,13 @@ class Ui_MainWindow(object):
self.actionResume = QtGui.QAction(parent=MainWindow)
self.actionResume.setObjectName("actionResume")
self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Wikipedia.setObjectName("actionSearch_title_in_Wikipedia")
self.actionSearch_title_in_Wikipedia.setObjectName(
"actionSearch_title_in_Wikipedia"
)
self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Songfacts.setObjectName("actionSearch_title_in_Songfacts")
self.actionSearch_title_in_Songfacts.setObjectName(
"actionSearch_title_in_Songfacts"
)
self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow)
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
self.actionReplace_files = QtGui.QAction(parent=MainWindow)
@ -517,8 +717,14 @@ class Ui_MainWindow(object):
self.current_track_2.setText(_translate("MainWindow", "Current track:"))
self.next_track_2.setText(_translate("MainWindow", "Next track:"))
self.lblTOD.setText(_translate("MainWindow", "00:00:00"))
self.btnPreview.setText(_translate("MainWindow", " Preview"))
self.label_elapsed_timer.setText(_translate("MainWindow", "00:00 / 00:00"))
self.btnPreview.setText(_translate("MainWindow", " Preview"))
self.btnPreviewStart.setText(_translate("MainWindow", "<<"))
self.btnPreviewEnd.setText(_translate("MainWindow", ">>"))
self.btnPreviewBack.setText(_translate("MainWindow", "<"))
self.btnPreviewFwd.setText(_translate("MainWindow", ">"))
self.label_7.setText(_translate("MainWindow", "Intro"))
self.label_intro_timer.setText(_translate("MainWindow", "0:0"))
self.btnDrop3db.setText(_translate("MainWindow", "-3dB to talk"))
self.btnHidePlayed.setText(_translate("MainWindow", "Hide played"))
self.label_4.setText(_translate("MainWindow", "Fade"))
@ -543,38 +749,58 @@ class Ui_MainWindow(object):
self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z"))
self.actionStop.setText(_translate("MainWindow", "S&top"))
self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S"))
self.action_Clear_selection.setText(_translate("MainWindow", "Clear &selection"))
self.action_Clear_selection.setText(
_translate("MainWindow", "Clear &selection")
)
self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc"))
self.action_Resume_previous.setText(_translate("MainWindow", "&Resume previous"))
self.action_Resume_previous.setText(
_translate("MainWindow", "&Resume previous")
)
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
self.actionTest.setText(_translate("MainWindow", "&Test"))
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
self.actionSkipToFade.setText(_translate("MainWindow", "&Skip to start of fade"))
self.actionSkipToFade.setText(
_translate("MainWindow", "&Skip to start of fade")
)
self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track"))
self.actionClosePlaylist.setText(_translate("MainWindow", "&Close"))
self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename..."))
self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te..."))
self.actionMoveSelected.setText(_translate("MainWindow", "Mo&ve selected tracks to..."))
self.actionMoveSelected.setText(
_translate("MainWindow", "Mo&ve selected tracks to...")
)
self.actionExport_playlist.setText(_translate("MainWindow", "E&xport..."))
self.actionSetNext.setText(_translate("MainWindow", "Set &next"))
self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N"))
self.actionSelect_next_track.setText(_translate("MainWindow", "Select next track"))
self.actionSelect_next_track.setText(
_translate("MainWindow", "Select next track")
)
self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J"))
self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track"))
self.actionSelect_previous_track.setText(
_translate("MainWindow", "Select previous track")
)
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to..."))
self.actionSelect_played_tracks.setText(
_translate("MainWindow", "Select played tracks")
)
self.actionMoveUnplayed.setText(
_translate("MainWindow", "Move &unplayed tracks to...")
)
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
self.actionImport.setText(_translate("MainWindow", "Import track..."))
self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I"))
self.actionDownload_CSV_of_played_tracks.setText(_translate("MainWindow", "Download CSV of played tracks..."))
self.actionDownload_CSV_of_played_tracks.setText(
_translate("MainWindow", "Download CSV of played tracks...")
)
self.actionSearch.setText(_translate("MainWindow", "Search..."))
self.actionSearch.setShortcut(_translate("MainWindow", "/"))
self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert &section header..."))
self.actionInsertSectionHeader.setText(
_translate("MainWindow", "Insert &section header...")
)
self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H"))
self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
self.actionFind_next.setText(_translate("MainWindow", "Find next"))
@ -582,8 +808,12 @@ class Ui_MainWindow(object):
self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
self.action_About.setText(_translate("MainWindow", "&About"))
self.actionSave_as_template.setText(_translate("MainWindow", "Save as template..."))
self.actionNew_from_template.setText(_translate("MainWindow", "New from template..."))
self.actionSave_as_template.setText(
_translate("MainWindow", "Save as template...")
)
self.actionNew_from_template.setText(
_translate("MainWindow", "New from template...")
)
self.actionDebug.setText(_translate("MainWindow", "Debug"))
self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1..."))
self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving"))
@ -592,11 +822,23 @@ class Ui_MainWindow(object):
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
self.actionResume.setText(_translate("MainWindow", "Resume"))
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
self.actionSearch_title_in_Wikipedia.setText(_translate("MainWindow", "Search title in Wikipedia"))
self.actionSearch_title_in_Wikipedia.setShortcut(_translate("MainWindow", "Ctrl+W"))
self.actionSearch_title_in_Songfacts.setText(_translate("MainWindow", "Search title in Songfacts"))
self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S"))
self.actionSelect_duplicate_rows.setText(_translate("MainWindow", "Select duplicate rows..."))
self.actionSearch_title_in_Wikipedia.setText(
_translate("MainWindow", "Search title in Wikipedia")
)
self.actionSearch_title_in_Wikipedia.setShortcut(
_translate("MainWindow", "Ctrl+W")
)
self.actionSearch_title_in_Songfacts.setText(
_translate("MainWindow", "Search title in Songfacts")
)
self.actionSearch_title_in_Songfacts.setShortcut(
_translate("MainWindow", "Ctrl+S")
)
self.actionSelect_duplicate_rows.setText(
_translate("MainWindow", "Select duplicate rows...")
)
self.actionReplace_files.setText(_translate("MainWindow", "Replace files..."))
from infotabs import InfoTabs
from pyqtgraph import PlotWidget

BIN
app/ui/record-button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
app/ui/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
app/ui/star_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1 +1 @@
Generic single-database configuration.
Alembic configuration for Alchemical

View File

@ -1,84 +1,27 @@
import sys
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from importlib import import_module
from alembic import context
from alchemical.alembic.env import run_migrations
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# https://stackoverflow.com/questions/32032940/how-to-import-the-own-model-into-myproject-alembic-env-py
path = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, path)
sys.path.insert(0, os.path.join(path, "app"))
from app.models import Base
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
# import the application's Alchemical instance
try:
import_mod, db_name = config.get_main_option('alchemical_db', '').split(
':')
db = getattr(import_module(import_mod), db_name)
except (ModuleNotFoundError, AttributeError):
raise ValueError(
'Could not import the Alchemical database instance. '
'Ensure that the alchemical_db setting in alembic.ini is correct.'
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
# run the migration engine
# The dictionary provided as second argument includes options to pass to the
# Alembic context. For details on what other options are available, see
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html
run_migrations(db, {
'render_as_batch': True,
'compare_type': True,
})

View File

@ -0,0 +1,110 @@
"""add Tracks.intro column
Revision ID: 2caa3d37f211
Revises: 5bb2c572e1e5
Create Date: 2024-05-07 20:06:00.845979
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '2caa3d37f211'
down_revision = '5bb2c572e1e5'
branch_labels = None
depends_on = None
def upgrade(engine_name: str) -> None:
globals()["upgrade_%s" % engine_name]()
def downgrade(engine_name: str) -> None:
globals()["downgrade_%s" % engine_name]()
def upgrade_() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('carts', schema=None) as batch_op:
batch_op.alter_column('name',
existing_type=mysql.VARCHAR(length=256),
nullable=False)
with op.batch_alter_table('notecolours', schema=None) as batch_op:
batch_op.alter_column('substring',
existing_type=mysql.VARCHAR(length=256),
nullable=False)
batch_op.alter_column('colour',
existing_type=mysql.VARCHAR(length=21),
nullable=False)
batch_op.alter_column('enabled',
existing_type=mysql.TINYINT(display_width=1),
nullable=False)
batch_op.alter_column('is_regex',
existing_type=mysql.TINYINT(display_width=1),
nullable=False)
batch_op.alter_column('is_casesensitive',
existing_type=mysql.TINYINT(display_width=1),
nullable=False)
with op.batch_alter_table('playdates', schema=None) as batch_op:
batch_op.alter_column('lastplayed',
existing_type=mysql.DATETIME(),
nullable=False)
batch_op.alter_column('track_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
with op.batch_alter_table('playlists', schema=None) as batch_op:
batch_op.drop_index('tab')
with op.batch_alter_table('tracks', schema=None) as batch_op:
batch_op.add_column(sa.Column('intro', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade_() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tracks', schema=None) as batch_op:
batch_op.drop_column('intro')
with op.batch_alter_table('playlists', schema=None) as batch_op:
batch_op.create_index('tab', ['tab'], unique=True)
with op.batch_alter_table('playdates', schema=None) as batch_op:
batch_op.alter_column('track_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
batch_op.alter_column('lastplayed',
existing_type=mysql.DATETIME(),
nullable=True)
with op.batch_alter_table('notecolours', schema=None) as batch_op:
batch_op.alter_column('is_casesensitive',
existing_type=mysql.TINYINT(display_width=1),
nullable=True)
batch_op.alter_column('is_regex',
existing_type=mysql.TINYINT(display_width=1),
nullable=True)
batch_op.alter_column('enabled',
existing_type=mysql.TINYINT(display_width=1),
nullable=True)
batch_op.alter_column('colour',
existing_type=mysql.VARCHAR(length=21),
nullable=True)
batch_op.alter_column('substring',
existing_type=mysql.VARCHAR(length=256),
nullable=True)
with op.batch_alter_table('carts', schema=None) as batch_op:
batch_op.alter_column('name',
existing_type=mysql.VARCHAR(length=256),
nullable=True)
# ### end Alembic commands ###

View File

@ -18,7 +18,6 @@ if os.path.exists(DB_FILE):
os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
from app.models import ( # noqa: E402
db,
Carts,
NoteColours,
Playdates,
Playlists,
@ -153,7 +152,7 @@ class TestMMModels(unittest.TestCase):
assert len(Playlists.get_open(session)) == 1
assert len(Playlists.get_closed(session)) == 0
playlist.close()
playlist.close(session)
assert len(Playlists.get_open(session)) == 0
assert len(Playlists.get_closed(session)) == 1
@ -256,12 +255,6 @@ class TestMMModels(unittest.TestCase):
colour = nc3.get_colour(session, BAD_STRING)
assert colour is None
def test_create_cart(self):
with db.Session() as session:
cart = Carts(session, 1, "name")
assert cart
_ = str(cart)
def test_name_available(self):
PLAYLIST_NAME = "a name"
RENAME = "new name"

View File

@ -19,14 +19,9 @@ os.environ["ALCHEMICAL_DATABASE_URI"] = "sqlite:///" + DB_FILE
from app import playlistmodel, utilities
from app.models import ( # noqa: E402
db,
Carts,
NoteColours,
Playdates,
Playlists,
PlaylistRows,
Tracks,
)
from app import playlists
from app import musicmuster