Compare commits

...

10 Commits

Author SHA1 Message Date
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
20 changed files with 3691 additions and 281 deletions

View File

@ -1,18 +1,29 @@
# A generic, single database configuration. # a multi-database configuration.
[alembic] [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 # path to migration scripts
script_location = migrations script_location = migrations
# template used to generate migration files # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# file_template = %%(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. # sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. # defaults to the current working directory.
prepend_sys_path = . prepend_sys_path = app
# timezone to use when rendering the date # timezone to use when rendering the date within the migration file
# within the migration file as well as the filename. # 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() # string value is passed to dateutil.tz.gettz()
# leave blank for localtime # leave blank for localtime
# timezone = # timezone =
@ -30,30 +41,36 @@ prepend_sys_path = .
# versions/ directory # versions/ directory
# sourceless = false # sourceless = false
# version location specification; this defaults # version location specification; This defaults
# to migrations/versions. When using multiple version # to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path # directories, initial revisions must be specified with --version-path.
# version_locations = %(here)s/bar %(here)s/bat migrations/versions # 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 # the output encoding used when revision files
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # 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]
# post_write_hooks defines scripts or Python functions that are run # post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further # on newly generated revision scripts. See the documentation for further
# detail and examples # detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint # format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black # hooks = black
# black.type=console_scripts # black.type = console_scripts
# black.entrypoint=black # black.entrypoint = black
# black.options=-l 79 # black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration # Logging configuration
[loggers] [loggers]

View File

@ -1,5 +1,6 @@
# Standard library imports # Standard library imports
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import auto, Enum
from typing import Any, Optional from typing import Any, Optional
import datetime as dt import datetime as dt
@ -18,6 +19,19 @@ from models import PlaylistRows
import helpers import helpers
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()
class FadeCurve: class FadeCurve:
GraphWidget = None GraphWidget = None
@ -152,6 +166,7 @@ class PlaylistTrack:
self.duration = track.duration self.duration = track.duration
self.end_time = None self.end_time = None
self.fade_at = track.fade_at self.fade_at = track.fade_at
self.intro = track.intro
self.path = track.path self.path = track.path
self.playlist_id = plr.playlist_id self.playlist_id = plr.playlist_id
self.plr_id = plr.id self.plr_id = plr.id

View File

@ -52,6 +52,7 @@ class Config(object):
HEADER_BITRATE = "bps" HEADER_BITRATE = "bps"
HEADER_DURATION = "Length" HEADER_DURATION = "Length"
HEADER_END_TIME = "End" HEADER_END_TIME = "End"
HEADER_INTRO = "Intro"
HEADER_LAST_PLAYED = "Last played" HEADER_LAST_PLAYED = "Last played"
HEADER_NOTE = "Notes" HEADER_NOTE = "Notes"
HEADER_START_GAP = "Gap" HEADER_START_GAP = "Gap"
@ -59,6 +60,9 @@ class Config(object):
HEADER_TITLE = "Title" HEADER_TITLE = "Title"
HIDE_AFTER_PLAYING_OFFSET = 5000 HIDE_AFTER_PLAYING_OFFSET = 5000
INFO_TAB_TITLE_LENGTH = 15 INFO_TAB_TITLE_LENGTH = 15
INTRO_END_GAP_MS = 1000
INTRO_SECONDS_FORMAT = ".1f"
INTRO_SECONDS_WARNING_MS = 3000
LAST_PLAYED_TODAY_STRING = "Today" LAST_PLAYED_TODAY_STRING = "Today"
LAST_PLAYED_TOOLTIP_DATE_FORMAT = "%a, %d %b %Y" LAST_PLAYED_TOOLTIP_DATE_FORMAT = "%a, %d %b %Y"
LOG_LEVEL_STDERR = logging.INFO LOG_LEVEL_STDERR = logging.INFO
@ -80,6 +84,8 @@ class Config(object):
OBS_PORT = 4455 OBS_PORT = 4455
PLAY_NEXT_GUARD_MS = 10000 PLAY_NEXT_GUARD_MS = 10000
PLAY_SETTLE = 500000 PLAY_SETTLE = 500000
PREVIEW_ADVANCE_MS = 5000
PREVIEW_BACK_MS = 5000
REPLACE_FILES_DEFAULT_SOURCE = "/home/kae/music/Singles/tmp" REPLACE_FILES_DEFAULT_SOURCE = "/home/kae/music/Singles/tmp"
RETURN_KEY_DEBOUNCE_MS = 500 RETURN_KEY_DEBOUNCE_MS = 500
ROOT = os.environ.get("ROOT") or "/home/kae/music" ROOT = os.environ.get("ROOT") or "/home/kae/music"
@ -89,8 +95,10 @@ class Config(object):
TEXT_NO_TRACK_NO_NOTE = "[Section header]" TEXT_NO_TRACK_NO_NOTE = "[Section header]"
TOD_TIME_FORMAT = "%H:%M:%S" TOD_TIME_FORMAT = "%H:%M:%S"
TRACK_TIME_FORMAT = "%H:%M:%S" TRACK_TIME_FORMAT = "%H:%M:%S"
VOLUME_VLC_DEFAULT = 75 VLC_MAIN_PLAYER_NAME = "MusicMuster Main Player"
VOLUME_VLC_DROP3db = 65 VLC_PREVIEW_PLAYER_NAME = "MusicMuster Preview Player"
VLC_VOLUME_DEFAULT = 75
VLC_VOLUME_DROP3db = 65
WARNING_MS_BEFORE_FADE = 5500 WARNING_MS_BEFORE_FADE = 5500
WARNING_MS_BEFORE_SILENCE = 5500 WARNING_MS_BEFORE_SILENCE = 5500
WEB_ZOOM_FACTOR = 1.2 WEB_ZOOM_FACTOR = 1.2

View File

@ -157,6 +157,7 @@ class TracksTable(Model):
bitrate: Mapped[Optional[int]] = mapped_column(default=None) bitrate: Mapped[Optional[int]] = mapped_column(default=None)
duration: Mapped[int] = mapped_column(index=True) duration: Mapped[int] = mapped_column(index=True)
fade_at: Mapped[int] = mapped_column(index=False) fade_at: Mapped[int] = mapped_column(index=False)
intro: Mapped[Optional[int]] = mapped_column(default=None)
mtime: Mapped[float] = mapped_column(index=True) mtime: Mapped[float] = mapped_column(index=True)
path: Mapped[str] = mapped_column(String(2048), index=False, unique=True) path: Mapped[str] = mapped_column(String(2048), index=False, unique=True)
silence_at: Mapped[int] = mapped_column(index=False) silence_at: Mapped[int] = mapped_column(index=False)

View File

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

View File

@ -58,12 +58,32 @@ class Music:
Manage the playing of music tracks Manage the playing of music tracks
""" """
def __init__(self) -> None: def __init__(self, name) -> None:
self.VLC = vlc.Instance() self.VLC = vlc.Instance()
self.VLC.set_user_agent(name, name)
self.player = None self.player = None
self.max_volume = Config.VOLUME_VLC_DEFAULT self.name = name
self.max_volume = Config.VLC_VOLUME_DEFAULT
self.start_dt: Optional[dt.datetime] = None 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 fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None: def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
""" """
Fade the currently playing track. Fade the currently playing track.
@ -72,7 +92,7 @@ class Music:
to hold up the UI during the fade. to hold up the UI during the fade.
""" """
log.info("Music.stop()") log.info(f"Music[{self.name}].stop()")
if not self.player: if not self.player:
return return
@ -113,7 +133,12 @@ class Music:
return self.player.get_position() return self.player.get_position()
def is_playing(self) -> bool: def is_playing(self) -> bool:
"""Return True if playing""" """
Return True if we're playing
"""
if not self.player:
return False
# There is a discrete time between starting playing a track and # There is a discrete time between starting playing a track and
# player.is_playing() returning True, so assume playing if less # player.is_playing() returning True, so assume playing if less
@ -129,6 +154,20 @@ class Music:
) )
) )
def move_back(self, ms: int) -> None:
"""
Rewind player by ms milliseconds
"""
self._adjust_by_ms(ms * -1)
def move_forward(self, ms: int) -> None:
"""
Rewind player by ms milliseconds
"""
self._adjust_by_ms(ms)
def play(self, path: str, position: Optional[float] = None) -> None: def play(self, path: str, position: Optional[float] = None) -> None:
""" """
Start playing the track at path. Start playing the track at path.
@ -136,7 +175,7 @@ class Music:
Log and return if path not found. Log and return if path not found.
""" """
log.info(f"Music.play({path=}, {position=}") log.info(f"Music[{self.name}].play({path=}, {position=}")
if file_is_unreadable(path): if file_is_unreadable(path):
log.error(f"play({path}): path not readable") log.error(f"play({path}): path not readable")
@ -152,6 +191,28 @@ class Music:
self.player.set_position(position) self.player.set_position(position)
self.start_dt = dt.datetime.now() self.start_dt = dt.datetime.now()
# 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: def set_volume(self, volume=None, set_default=True) -> None:
"""Set maximum volume used for player""" """Set maximum volume used for player"""
@ -162,7 +223,7 @@ class Music:
self.max_volume = volume self.max_volume = volume
if volume is None: if volume is None:
volume = Config.VOLUME_VLC_DEFAULT volume = Config.VLC_VOLUME_DEFAULT
self.player.audio_set_volume(volume) self.player.audio_set_volume(volume)
# Ensure volume correct # Ensure volume correct
@ -180,7 +241,9 @@ class Music:
def stop(self) -> float: def stop(self) -> float:
"""Immediately stop playing""" """Immediately stop playing"""
log.info("Music.stop()") log.info(f"Music[{self.name}].stop()")
self.start_dt = None
if not self.player: if not self.player:
return 0.0 return 0.0

View File

@ -47,7 +47,7 @@ from PyQt6.QtWidgets import (
) )
# Third party imports # Third party imports
from pygame import mixer # from pygame import mixer
import pipeclient import pipeclient
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -226,10 +226,14 @@ class Window(QMainWindow, Ui_MainWindow):
self.setupUi(self) self.setupUi(self)
self.timer10: QTimer = QTimer() self.timer10: QTimer = QTimer()
self.timer100: QTimer = QTimer()
self.timer500: QTimer = QTimer() self.timer500: QTimer = QTimer()
self.timer1000: QTimer = QTimer() self.timer1000: QTimer = QTimer()
self.music: music.Music = music.Music() self.music: music.Music = music.Music(name=Config.VLC_MAIN_PLAYER_NAME)
self.preview_player: music.Music = music.Music(
name=Config.VLC_PREVIEW_PLAYER_NAME
)
self.playing: bool = False self.playing: bool = False
self.set_main_window_size() self.set_main_window_size()
@ -239,11 +243,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.txtSearch.setHidden(True) self.txtSearch.setHidden(True)
self.statusbar.addWidget(self.txtSearch) self.statusbar.addWidget(self.txtSearch)
self.hide_played_tracks = False self.hide_played_tracks = False
try:
mixer.init()
except Exception:
helpers.show_warning(self, "Fatal error", "Cannot initialise sound device")
sys.exit(1)
self.widgetFadeVolume.hideAxis("bottom") self.widgetFadeVolume.hideAxis("bottom")
self.widgetFadeVolume.hideAxis("left") self.widgetFadeVolume.hideAxis("left")
@ -267,6 +266,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.disable_selection_timing = False self.disable_selection_timing = False
self.clock_counter = 0 self.clock_counter = 0
self.timer10.start(10) self.timer10.start(10)
self.timer100.start(100)
self.timer500.start(500) self.timer500.start(500)
self.timer1000.start(1000) self.timer1000.start(1000)
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
@ -306,7 +306,7 @@ class Window(QMainWindow, Ui_MainWindow):
btn.path = cart.path btn.path = cart.path
btn.player = self.music.VLC.media_player_new(cart.path) btn.player = self.music.VLC.media_player_new(cart.path)
if btn.player: if btn.player:
btn.player.audio_set_volume(Config.VOLUME_VLC_DEFAULT) btn.player.audio_set_volume(Config.VLC_VOLUME_DEFAULT)
if cart.enabled: if cart.enabled:
btn.setEnabled(True) btn.setEnabled(True)
btn.pgb.setVisible(True) btn.pgb.setVisible(True)
@ -420,7 +420,7 @@ class Window(QMainWindow, Ui_MainWindow):
btn.setEnabled(True) btn.setEnabled(True)
# Setting to position 0 doesn't seem to work # Setting to position 0 doesn't seem to work
btn.player = self.music.VLC.media_player_new(btn.path) btn.player = self.music.VLC.media_player_new(btn.path)
btn.player.audio_set_volume(Config.VOLUME_VLC_DEFAULT) btn.player.audio_set_volume(Config.VLC_VOLUME_DEFAULT)
colour = Config.COLOUR_CART_READY colour = Config.COLOUR_CART_READY
btn.setStyleSheet("background-color: " + colour + ";\n") btn.setStyleSheet("background-color: " + colour + ";\n")
btn.pgb.setValue(0) btn.pgb.setValue(0)
@ -582,7 +582,13 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnDrop3db.clicked.connect(self.drop3db) self.btnDrop3db.clicked.connect(self.drop3db)
self.btnFade.clicked.connect(self.fade) self.btnFade.clicked.connect(self.fade)
self.btnHidePlayed.clicked.connect(self.hide_played) self.btnHidePlayed.clicked.connect(self.hide_played)
self.btnPreviewArm.clicked.connect(self.preview_arm)
self.btnPreviewBack.clicked.connect(self.preview_back)
self.btnPreview.clicked.connect(self.preview) self.btnPreview.clicked.connect(self.preview)
self.btnPreviewEnd.clicked.connect(self.preview_end)
self.btnPreviewFwd.clicked.connect(self.preview_fwd)
self.btnPreviewMark.clicked.connect(self.preview_mark)
self.btnPreviewStart.clicked.connect(self.preview_start)
self.btnStop.clicked.connect(self.stop) self.btnStop.clicked.connect(self.stop)
self.hdrCurrentTrack.clicked.connect(self.show_current) self.hdrCurrentTrack.clicked.connect(self.show_current)
self.hdrNextTrack.clicked.connect(self.show_next) self.hdrNextTrack.clicked.connect(self.show_next)
@ -598,6 +604,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer10.timeout.connect(self.tick_10ms) self.timer10.timeout.connect(self.tick_10ms)
self.timer500.timeout.connect(self.tick_500ms) self.timer500.timeout.connect(self.tick_500ms)
self.timer100.timeout.connect(self.tick_100ms)
self.timer1000.timeout.connect(self.tick_1000ms) self.timer1000.timeout.connect(self.tick_1000ms)
def create_playlist( def create_playlist(
@ -715,9 +722,9 @@ class Window(QMainWindow, Ui_MainWindow):
"""Drop music level by 3db if button checked""" """Drop music level by 3db if button checked"""
if self.btnDrop3db.isChecked(): if self.btnDrop3db.isChecked():
self.music.set_volume(Config.VOLUME_VLC_DROP3db, set_default=False) self.music.set_volume(Config.VLC_VOLUME_DROP3db, set_default=False)
else: else:
self.music.set_volume(Config.VOLUME_VLC_DEFAULT, set_default=False) self.music.set_volume(Config.VLC_VOLUME_DEFAULT, set_default=False)
def enable_escape(self, enabled: bool) -> None: def enable_escape(self, enabled: bool) -> None:
""" """
@ -1237,9 +1244,7 @@ class Window(QMainWindow, Ui_MainWindow):
def preview(self) -> None: def preview(self) -> None:
""" """
Preview selected or next track. We use a different mechanism to Preview selected or next track.
normal track playing so that the user can route the output audio
differently (eg, to headphones).
""" """
if self.btnPreview.isChecked(): if self.btnPreview.isChecked():
@ -1251,11 +1256,67 @@ class Window(QMainWindow, Ui_MainWindow):
if not track_path: if not track_path:
self.btnPreview.setChecked(False) self.btnPreview.setChecked(False)
return return
mixer.music.load(track_path) self.preview_player.play(path=track_path)
mixer.music.play()
else: else:
mixer.music.stop() self.preview_player.stop()
self.label_intro_timer.setText("0.0")
self.btnPreviewMark.setEnabled(False)
self.btnPreviewArm.setChecked(False)
def preview_arm(self):
"""Manager arm button for setting intro length"""
self.btnPreviewMark.setEnabled(self.btnPreviewArm.isChecked())
def preview_back(self) -> None:
"""Wind back preview file"""
self.preview_player.move_back(Config.PREVIEW_BACK_MS)
def preview_end(self) -> None:
"""Advance preview file to just before end of intro"""
return
# preview_track_path = self.preview_player.path
# if not preview_track_path:
# return
# with Session() as session:
# preview_track = Tracks.get_by_path(session, preview_track_path)
# if not preview_track or not preview_track.intro:
# return
# new_position = max(0, preview_track.intro - Config.INTRO_END_GAP_MS)
# self.preview_player.set_position(new_position)
def preview_fwd(self) -> None:
"""Advance preview file"""
self.preview_player.move_forward(Config.PREVIEW_ADVANCE_MS)
def preview_mark(self) -> None:
"""Set intro time"""
track_id = self.active_tab().get_selected_row_track_id()
row_number = self.active_tab().get_selected_row()
if track_id:
with db.Session() as session:
track = session.get(Tracks, track_id)
if track:
# Save intro as millisends rounded to nearest 0.1
# second because editor spinbox only resolves to 0.1
# seconds
track.intro = round(self.preview_player.get_playtime() / 100) * 100
session.commit()
self.active_tab().source_model.refresh_row(session, row_number)
self.active_tab().source_model.invalidate_row(row_number)
def preview_start(self) -> None:
"""Advance preview file"""
self.preview_player.set_position(0)
def rename_playlist(self) -> None: def rename_playlist(self) -> None:
""" """
@ -1675,14 +1736,49 @@ class Window(QMainWindow, Ui_MainWindow):
# Update carts # Update carts
# self.cart_tick() # self.cart_tick()
def tick_100ms(self) -> None:
"""
Called every 100ms
"""
# Update intro counter if applicable and, if updated, return
# because playing an intro takes precedence over timing a
# preview.
if self.music.is_playing() and track_sequence.now.intro:
remaining_ms = track_sequence.now.intro - self.music.get_playtime()
if remaining_ms > 0:
self.label_intro_timer.setText(f"{remaining_ms / 1000:.1f}")
if remaining_ms <= Config.INTRO_SECONDS_WARNING_MS:
self.label_intro_timer.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
return
else:
self.label_intro_timer.setStyleSheet("")
# Ensure preview button is reset if preview finishes playing
self.btnPreview.setChecked(self.preview_player.is_playing())
# Update preview timer
if self.preview_player.is_playing():
playtime = self.preview_player.get_playtime()
self.label_intro_timer.setText(f"{playtime / 1000:.1f}")
if playtime <= 0:
self.label_intro_timer.setStyleSheet(
f"background: {Config.COLOUR_ENDING_TIMER}"
)
elif playtime <= Config.INTRO_SECONDS_WARNING_MS:
self.label_intro_timer.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
else:
self.label_intro_timer.setText("0.0")
def tick_1000ms(self) -> None: def tick_1000ms(self) -> None:
""" """
Called every 1000ms Called every 1000ms
""" """
# Ensure preview button is reset if preview finishes playing
self.btnPreview.setChecked(mixer.music.get_busy())
# Only update play clocks once a second so that their updates # Only update play clocks once a second so that their updates
# are synchronised (otherwise it looks odd) # are synchronised (otherwise it looks odd)

View File

@ -2,7 +2,6 @@
# Allow forward reference to PlaylistModel # Allow forward reference to PlaylistModel
from __future__ import annotations from __future__ import annotations
from enum import auto, Enum
from operator import attrgetter from operator import attrgetter
from random import shuffle from random import shuffle
from typing import List, Optional from typing import List, Optional
@ -31,7 +30,7 @@ import obswebsocket # type: ignore
# import snoop # type: ignore # import snoop # type: ignore
# App imports # App imports
from classes import track_sequence, MusicMusterSignals, PlaylistTrack from classes import Col, track_sequence, MusicMusterSignals, PlaylistTrack
from config import Config from config import Config
from helpers import ( from helpers import (
file_is_unreadable, file_is_unreadable,
@ -48,18 +47,6 @@ HEADER_NOTES_COLUMN = 1
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]") 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: def __init__(self, plr: PlaylistRows) -> None:
""" """
@ -69,6 +56,7 @@ class PlaylistRowData:
self.artist: str = "" self.artist: str = ""
self.bitrate = 0 self.bitrate = 0
self.duration: int = 0 self.duration: int = 0
self.intro: Optional[int] = None
self.lastplayed: dt.datetime = Config.EPOCH self.lastplayed: dt.datetime = Config.EPOCH
self.path = "" self.path = ""
self.played = False self.played = False
@ -86,6 +74,7 @@ class PlaylistRowData:
self.title = plr.track.title self.title = plr.track.title
self.artist = plr.track.artist self.artist = plr.track.artist
self.duration = plr.track.duration self.duration = plr.track.duration
self.intro = plr.track.intro
self.played = plr.played self.played = plr.played
if plr.track.playdates: if plr.track.playdates:
self.lastplayed = max([a.lastplayed for a in plr.track.playdates]) self.lastplayed = max([a.lastplayed for a in plr.track.playdates])
@ -244,7 +233,7 @@ class PlaylistModel(QAbstractTableModel):
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
"""Standard function for view""" """Standard function for view"""
return 9 return len(Col)
def current_track_started(self) -> None: def current_track_started(self) -> None:
""" """
@ -442,14 +431,20 @@ class PlaylistModel(QAbstractTableModel):
return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT)) return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
return QVariant() 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 = { dispatch_table = {
Col.START_GAP.value: QVariant(prd.start_gap),
Col.TITLE.value: QVariant(prd.title),
Col.ARTIST.value: QVariant(prd.artist), Col.ARTIST.value: QVariant(prd.artist),
Col.BITRATE.value: QVariant(prd.bitrate),
Col.DURATION.value: QVariant(ms_to_mmss(prd.duration)), Col.DURATION.value: QVariant(ms_to_mmss(prd.duration)),
Col.LAST_PLAYED.value: QVariant(get_relative_date(prd.lastplayed)), Col.LAST_PLAYED.value: QVariant(get_relative_date(prd.lastplayed)),
Col.BITRATE.value: QVariant(prd.bitrate),
Col.NOTE.value: QVariant(prd.note), 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: if column in dispatch_table:
return dispatch_table[column] return dispatch_table[column]
@ -481,6 +476,8 @@ class PlaylistModel(QAbstractTableModel):
if self.is_header_row(row) and column == HEADER_NOTES_COLUMN: if self.is_header_row(row) and column == HEADER_NOTES_COLUMN:
return QVariant(prd.note) return QVariant(prd.note)
if column == Col.INTRO.value:
return QVariant(prd.intro)
if column == Col.TITLE.value: if column == Col.TITLE.value:
return QVariant(prd.title) return QVariant(prd.title)
if column == Col.ARTIST.value: if column == Col.ARTIST.value:
@ -503,7 +500,12 @@ class PlaylistModel(QAbstractTableModel):
| Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsSelectable
| Qt.ItemFlag.ItemIsDragEnabled | 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 | Qt.ItemFlag.ItemIsEditable
return default return default
@ -574,6 +576,13 @@ class PlaylistModel(QAbstractTableModel):
return self.playlist_rows[row_number] 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: def get_row_track_path(self, row_number: int) -> str:
""" """
Return path of track associated with row or empty string if no track associated Return path of track associated with row or empty string if no track associated
@ -619,6 +628,8 @@ class PlaylistModel(QAbstractTableModel):
if orientation == Qt.Orientation.Horizontal: if orientation == Qt.Orientation.Horizontal:
if section == Col.START_GAP.value: if section == Col.START_GAP.value:
return QVariant(Config.HEADER_START_GAP) return QVariant(Config.HEADER_START_GAP)
if section == Col.INTRO.value:
return QVariant(Config.HEADER_INTRO)
elif section == Col.TITLE.value: elif section == Col.TITLE.value:
return QVariant(Config.HEADER_TITLE) return QVariant(Config.HEADER_TITLE)
elif section == Col.ARTIST.value: elif section == Col.ARTIST.value:
@ -1281,7 +1292,7 @@ class PlaylistModel(QAbstractTableModel):
self.update_track_times() self.update_track_times()
def setData( def setData(
self, index: QModelIndex, value: QVariant, role: int = Qt.ItemDataRole.EditRole self, index: QModelIndex, value: str | float, role: int = Qt.ItemDataRole.EditRole
) -> bool: ) -> bool:
""" """
Update model with edited data Update model with edited data
@ -1301,7 +1312,7 @@ class PlaylistModel(QAbstractTableModel):
return False return False
if plr.track_id: 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) track = session.get(Tracks, plr.track_id)
if not track: if not track:
print(f"Error retreiving track: {plr=}") print(f"Error retreiving track: {plr=}")
@ -1310,11 +1321,14 @@ class PlaylistModel(QAbstractTableModel):
track.title = str(value) track.title = str(value)
elif column == Col.ARTIST.value: elif column == Col.ARTIST.value:
track.artist = str(value) track.artist = str(value)
elif column == Col.INTRO.value:
track.intro = int(round(float(value), 1) * 1000)
else: else:
print(f"Error updating track: {column=}, {value=}") print(f"Error updating track: {column=}, {value=}")
return False return False
elif column == Col.NOTE.value: elif column == Col.NOTE.value:
plr.note = str(value) plr.note = str(value)
else: else:
# This is a header row # This is a header row
if column == HEADER_NOTES_COLUMN: if column == HEADER_NOTES_COLUMN:

View File

@ -17,24 +17,25 @@ from PyQt6.QtWidgets import (
QAbstractItemDelegate, QAbstractItemDelegate,
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
QDoubleSpinBox,
QHeaderView, QHeaderView,
QMenu, QMenu,
QMessageBox, QMessageBox,
QPlainTextEdit, QPlainTextEdit,
QProxyStyle,
QStyle,
QStyledItemDelegate, QStyledItemDelegate,
QStyleOption,
QStyleOptionViewItem, QStyleOptionViewItem,
QTableView, QTableView,
QTableWidgetItem, QTableWidgetItem,
QWidget, QWidget,
QProxyStyle,
QStyle,
QStyleOption,
) )
# Third party imports # Third party imports
# App imports # App imports
from classes import MusicMusterSignals, track_sequence from classes import Col, MusicMusterSignals, track_sequence
from config import Config from config import Config
from dialogs import TrackSelectDialog from dialogs import TrackSelectDialog
from helpers import ( from helpers import (
@ -73,15 +74,22 @@ class EscapeDelegate(QStyledItemDelegate):
Intercept createEditor call and make row just a little bit taller Intercept createEditor call and make row just a little bit taller
""" """
self.editor: QDoubleSpinBox | QPlainTextEdit
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
self.signals.enable_escape_signal.emit(False) self.signals.enable_escape_signal.emit(False)
if isinstance(self.parent(), PlaylistTab): if isinstance(self.parent(), PlaylistTab):
p = cast(PlaylistTab, self.parent()) 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 = index.row()
row_height = p.rowHeight(row) row_height = p.rowHeight(row)
p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT) p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT)
self.editor = QPlainTextEdit(parent)
return self.editor return self.editor
return super().createEditor(parent, option, index) return super().createEditor(parent, option, index)
@ -107,10 +115,16 @@ class EscapeDelegate(QStyledItemDelegate):
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
return True return True
elif key_event.key() == Qt.Key.Key_Escape: elif key_event.key() == Qt.Key.Key_Escape:
if self.original_text == self.editor.toPlainText(): # Close editor if no changes have been made
# No changes 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) self.closeEditor.emit(editor)
return True return True
discard_edits = QMessageBox.question( discard_edits = QMessageBox.question(
cast(QWidget, self.parent()), "Abandon edit", "Discard changes?" cast(QWidget, self.parent()), "Abandon edit", "Discard changes?"
) )
@ -123,16 +137,22 @@ class EscapeDelegate(QStyledItemDelegate):
proxy_model = index.model() proxy_model = index.model()
edit_index = proxy_model.mapToSource(index) 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 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): def setModelData(self, editor, model, index):
proxy_model = index.model() proxy_model = index.model()
edit_index = proxy_model.mapToSource(index) edit_index = proxy_model.mapToSource(index)
if isinstance(self.editor, QPlainTextEdit):
value = editor.toPlainText().strip() value = editor.toPlainText().strip()
elif isinstance(self.editor, QDoubleSpinBox):
value = editor.value()
self.source_model.setData(edit_index, value, Qt.ItemDataRole.EditRole) self.source_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
def updateEditorGeometry(self, editor, option, index): def updateEditorGeometry(self, editor, option, index):
@ -600,6 +620,24 @@ class PlaylistTab(QTableView):
self.source_model.delete_rows(self.selected_model_row_numbers()) self.source_model.delete_rows(self.selected_model_row_numbers())
self.clear_selection() self.clear_selection()
def get_selected_row_track_id(self) -> Optional[int]:
"""
Return the track_id of the selected row. If no row selected or selected
row does not have a track, return None.
"""
log.debug("get_selected_row_track_id() called")
model_row_number = self.source_model_selected_row_number()
if model_row_number is None:
result = None
else:
result = self.source_model.get_row_track_id(model_row_number)
log.debug(f"get_selected_row_track_id() returned: {result=}")
return result
def get_selected_row_track_path(self) -> str: def get_selected_row_track_path(self) -> str:
""" """
Return the path of the selected row. If no row selected or selected Return the path of the selected row. If no row selected or selected
@ -617,6 +655,17 @@ class PlaylistTab(QTableView):
log.debug(f"get_selected_row_track_path() returned: {result=}") log.debug(f"get_selected_row_track_path() returned: {result=}")
return 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]: def get_selected_rows(self) -> List[int]:
"""Return a list of model-selected row numbers sorted by row""" """Return a list of model-selected row numbers sorted by row"""

View File

@ -1,5 +1,9 @@
<RCC> <RCC>
<qresource prefix="icons"> <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="headphones">headphone-symbol.png</file>
<file alias="musicmuster">musicmuster.png</file> <file alias="musicmuster">musicmuster.png</file>
<file alias="stopsign">stopsign.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>
<item> <item>
<widget class="QFrame" name="frame_2"> <widget class="QFrame" name="frame_2">
<property name="minimumSize">
<size>
<width>0</width>
<height>131</height>
</size>
</property>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>230</width> <width>230</width>
<height>16777215</height> <height>131</height>
</size> </size>
</property> </property>
<property name="frameShape"> <property name="frameShape">
@ -241,13 +247,13 @@ padding-left: 8px;</string>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Raised</enum> <enum>QFrame::Raised</enum>
</property> </property>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_10">
<item row="0" column="0"> <item>
<widget class="QLabel" name="lblTOD"> <widget class="QLabel" name="lblTOD">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>208</width> <width>208</width>
<height>109</height> <height>0</height>
</size> </size>
</property> </property>
<property name="font"> <property name="font">
@ -263,6 +269,27 @@ padding-left: 8px;</string>
</property> </property>
</widget> </widget>
</item> </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> </layout>
</widget> </widget>
</item> </item>
@ -435,20 +462,234 @@ padding-left: 8px;</string>
</widget> </widget>
</item> </item>
<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"> <property name="font">
<font> <font>
<family>FreeSans</family> <family>FreeSans</family>
<pointsize>18</pointsize> <pointsize>40</pointsize>
<weight>50</weight> <weight>50</weight>
<bold>false</bold> <bold>false</bold>
</font> </font>
</property> </property>
<property name="styleSheet">
<string notr="true">color: black;</string>
</property>
<property name="text"> <property name="text">
<string>00:00 / 00:00</string> <string>0:0</string>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignCenter</set> <set>Qt::AlignCenter</set>
@ -592,15 +833,9 @@ padding-left: 8px;</string>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Raised</enum> <enum>QFrame::Raised</enum>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>45</width>
<height>24</height>
</rect>
</property>
<property name="text"> <property name="text">
<string>Silent</string> <string>Silent</string>
</property> </property>
@ -608,15 +843,9 @@ padding-left: 8px;</string>
<set>Qt::AlignCenter</set> <set>Qt::AlignCenter</set>
</property> </property>
</widget> </widget>
</item>
<item>
<widget class="QLabel" name="label_silent_timer"> <widget class="QLabel" name="label_silent_timer">
<property name="geometry">
<rect>
<x>10</x>
<y>48</y>
<width>132</width>
<height>54</height>
</rect>
</property>
<property name="font"> <property name="font">
<font> <font>
<family>FreeSans</family> <family>FreeSans</family>
@ -632,6 +861,8 @@ padding-left: 8px;</string>
<set>Qt::AlignCenter</set> <set>Qt::AlignCenter</set>
</property> </property>
</widget> </widget>
</item>
</layout>
</widget> </widget>
</item> </item>
<item> <item>
@ -661,7 +892,7 @@ padding-left: 8px;</string>
<property name="maximumSize"> <property name="maximumSize">
<size> <size>
<width>151</width> <width>151</width>
<height>16777215</height> <height>112</height>
</size> </size>
</property> </property>
<property name="frameShape"> <property name="frameShape">
@ -812,7 +1043,7 @@ padding-left: 8px;</string>
<action name="actionPlay_next"> <action name="actionPlay_next">
<property name="icon"> <property name="icon">
<iconset> <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>
<property name="text"> <property name="text">
<string>&amp;Play next</string> <string>&amp;Play next</string>
@ -836,7 +1067,7 @@ padding-left: 8px;</string>
<action name="actionInsertTrack"> <action name="actionInsertTrack">
<property name="icon"> <property name="icon">
<iconset> <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>
<property name="text"> <property name="text">
<string>Insert &amp;track...</string> <string>Insert &amp;track...</string>
@ -848,7 +1079,7 @@ padding-left: 8px;</string>
<action name="actionAdd_file"> <action name="actionAdd_file">
<property name="icon"> <property name="icon">
<iconset> <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>
<property name="text"> <property name="text">
<string>Add &amp;file</string> <string>Add &amp;file</string>
@ -860,7 +1091,7 @@ padding-left: 8px;</string>
<action name="actionFade"> <action name="actionFade">
<property name="icon"> <property name="icon">
<iconset> <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>
<property name="text"> <property name="text">
<string>F&amp;ade</string> <string>F&amp;ade</string>

View File

@ -132,20 +132,32 @@ class Ui_MainWindow(object):
self.verticalLayout.addWidget(self.hdrNextTrack) self.verticalLayout.addWidget(self.hdrNextTrack)
self.horizontalLayout_3.addLayout(self.verticalLayout) self.horizontalLayout_3.addLayout(self.verticalLayout)
self.frame_2 = QtWidgets.QFrame(parent=self.centralwidget) self.frame_2 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_2.setMaximumSize(QtCore.QSize(230, 16777215)) self.frame_2.setMinimumSize(QtCore.QSize(0, 131))
self.frame_2.setMaximumSize(QtCore.QSize(230, 131))
self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_2.setObjectName("frame_2") self.frame_2.setObjectName("frame_2")
self.gridLayout_2 = QtWidgets.QGridLayout(self.frame_2) self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.frame_2)
self.gridLayout_2.setObjectName("gridLayout_2") self.verticalLayout_10.setObjectName("verticalLayout_10")
self.lblTOD = QtWidgets.QLabel(parent=self.frame_2) 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 = QtGui.QFont()
font.setPointSize(35) font.setPointSize(35)
self.lblTOD.setFont(font) self.lblTOD.setFont(font)
self.lblTOD.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.lblTOD.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.lblTOD.setObjectName("lblTOD") self.lblTOD.setObjectName("lblTOD")
self.gridLayout_2.addWidget(self.lblTOD, 0, 0, 1, 1) self.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.horizontalLayout_3.addWidget(self.frame_2)
self.gridLayout_4.addLayout(self.horizontalLayout_3, 0, 0, 1, 1) self.gridLayout_4.addLayout(self.horizontalLayout_3, 0, 0, 1, 1)
self.frame_4 = QtWidgets.QFrame(parent=self.centralwidget) self.frame_4 = QtWidgets.QFrame(parent=self.centralwidget)
@ -211,18 +223,78 @@ class Ui_MainWindow(object):
self.btnPreview.setCheckable(True) self.btnPreview.setCheckable(True)
self.btnPreview.setObjectName("btnPreview") self.btnPreview.setObjectName("btnPreview")
self.verticalLayout_4.addWidget(self.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 = QtGui.QFont()
font.setFamily("FreeSans") font.setFamily("FreeSans")
font.setPointSize(18) font.setPointSize(40)
font.setBold(False) font.setBold(False)
font.setWeight(50) font.setWeight(50)
self.label_elapsed_timer.setFont(font) self.label_intro_timer.setFont(font)
self.label_elapsed_timer.setStyleSheet("color: black;") self.label_intro_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_elapsed_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.label_intro_timer.setObjectName("label_intro_timer")
self.label_elapsed_timer.setObjectName("label_elapsed_timer") self.verticalLayout_9.addWidget(self.label_intro_timer)
self.verticalLayout_4.addWidget(self.label_elapsed_timer) self.horizontalLayout.addWidget(self.frame_intro)
self.horizontalLayout.addWidget(self.FadeStopInfoFrame)
self.frame_toggleplayed_3db = QtWidgets.QFrame(parent=self.InfoFooterFrame) self.frame_toggleplayed_3db = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_toggleplayed_3db.setMinimumSize(QtCore.QSize(152, 112)) self.frame_toggleplayed_3db.setMinimumSize(QtCore.QSize(152, 112))
self.frame_toggleplayed_3db.setMaximumSize(QtCore.QSize(184, 16777215)) self.frame_toggleplayed_3db.setMaximumSize(QtCore.QSize(184, 16777215))
@ -273,12 +345,13 @@ class Ui_MainWindow(object):
self.frame_silent.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.frame_silent.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_silent.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) self.frame_silent.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_silent.setObjectName("frame_silent") 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 = 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.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_5.setObjectName("label_5") 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 = QtWidgets.QLabel(parent=self.frame_silent)
self.label_silent_timer.setGeometry(QtCore.QRect(10, 48, 132, 54))
font = QtGui.QFont() font = QtGui.QFont()
font.setFamily("FreeSans") font.setFamily("FreeSans")
font.setPointSize(40) font.setPointSize(40)
@ -287,6 +360,7 @@ class Ui_MainWindow(object):
self.label_silent_timer.setFont(font) self.label_silent_timer.setFont(font)
self.label_silent_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.label_silent_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_silent_timer.setObjectName("label_silent_timer") self.label_silent_timer.setObjectName("label_silent_timer")
self.verticalLayout_7.addWidget(self.label_silent_timer)
self.horizontalLayout.addWidget(self.frame_silent) self.horizontalLayout.addWidget(self.frame_silent)
self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame) self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
@ -299,7 +373,7 @@ class Ui_MainWindow(object):
self.horizontalLayout.addWidget(self.widgetFadeVolume) self.horizontalLayout.addWidget(self.widgetFadeVolume)
self.frame = QtWidgets.QFrame(parent=self.InfoFooterFrame) self.frame = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame.setMinimumSize(QtCore.QSize(151, 0)) 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.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame.setObjectName("frame") self.frame.setObjectName("frame")
@ -308,17 +382,17 @@ class Ui_MainWindow(object):
self.btnFade = QtWidgets.QPushButton(parent=self.frame) self.btnFade = QtWidgets.QPushButton(parent=self.frame)
self.btnFade.setMinimumSize(QtCore.QSize(132, 32)) self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215)) self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
icon2 = QtGui.QIcon() icon4 = QtGui.QIcon()
icon2.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon4.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnFade.setIcon(icon2) self.btnFade.setIcon(icon4)
self.btnFade.setIconSize(QtCore.QSize(30, 30)) self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade") self.btnFade.setObjectName("btnFade")
self.verticalLayout_5.addWidget(self.btnFade) self.verticalLayout_5.addWidget(self.btnFade)
self.btnStop = QtWidgets.QPushButton(parent=self.frame) self.btnStop = QtWidgets.QPushButton(parent=self.frame)
self.btnStop.setMinimumSize(QtCore.QSize(0, 36)) self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
icon3 = QtGui.QIcon() icon5 = QtGui.QIcon()
icon3.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon5.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnStop.setIcon(icon3) self.btnStop.setIcon(icon5)
self.btnStop.setObjectName("btnStop") self.btnStop.setObjectName("btnStop")
self.verticalLayout_5.addWidget(self.btnStop) self.verticalLayout_5.addWidget(self.btnStop)
self.horizontalLayout.addWidget(self.frame) self.horizontalLayout.addWidget(self.frame)
@ -342,41 +416,41 @@ class Ui_MainWindow(object):
self.statusbar.setObjectName("statusbar") self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar) MainWindow.setStatusBar(self.statusbar)
self.actionPlay_next = QtGui.QAction(parent=MainWindow) self.actionPlay_next = QtGui.QAction(parent=MainWindow)
icon4 = QtGui.QIcon() icon6 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionPlay_next.setIcon(icon4) self.actionPlay_next.setIcon(icon6)
self.actionPlay_next.setObjectName("actionPlay_next") self.actionPlay_next.setObjectName("actionPlay_next")
self.actionSkipToNext = QtGui.QAction(parent=MainWindow) self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
icon5 = QtGui.QIcon() icon7 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon7.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionSkipToNext.setIcon(icon5) self.actionSkipToNext.setIcon(icon7)
self.actionSkipToNext.setObjectName("actionSkipToNext") self.actionSkipToNext.setObjectName("actionSkipToNext")
self.actionInsertTrack = QtGui.QAction(parent=MainWindow) self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon() icon8 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionInsertTrack.setIcon(icon6) self.actionInsertTrack.setIcon(icon8)
self.actionInsertTrack.setObjectName("actionInsertTrack") self.actionInsertTrack.setObjectName("actionInsertTrack")
self.actionAdd_file = QtGui.QAction(parent=MainWindow) self.actionAdd_file = QtGui.QAction(parent=MainWindow)
icon7 = QtGui.QIcon() icon9 = QtGui.QIcon()
icon7.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon9.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionAdd_file.setIcon(icon7) self.actionAdd_file.setIcon(icon9)
self.actionAdd_file.setObjectName("actionAdd_file") self.actionAdd_file.setObjectName("actionAdd_file")
self.actionFade = QtGui.QAction(parent=MainWindow) self.actionFade = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon() icon10 = QtGui.QIcon()
icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon10.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionFade.setIcon(icon8) self.actionFade.setIcon(icon10)
self.actionFade.setObjectName("actionFade") self.actionFade.setObjectName("actionFade")
self.actionStop = QtGui.QAction(parent=MainWindow) self.actionStop = QtGui.QAction(parent=MainWindow)
icon9 = QtGui.QIcon() icon11 = QtGui.QIcon()
icon9.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon11.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionStop.setIcon(icon9) self.actionStop.setIcon(icon11)
self.actionStop.setObjectName("actionStop") self.actionStop.setObjectName("actionStop")
self.action_Clear_selection = QtGui.QAction(parent=MainWindow) self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
self.action_Clear_selection.setObjectName("action_Clear_selection") self.action_Clear_selection.setObjectName("action_Clear_selection")
self.action_Resume_previous = QtGui.QAction(parent=MainWindow) self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
icon10 = QtGui.QIcon() icon12 = QtGui.QIcon()
icon10.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) icon12.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.action_Resume_previous.setIcon(icon10) self.action_Resume_previous.setIcon(icon12)
self.action_Resume_previous.setObjectName("action_Resume_previous") self.action_Resume_previous.setObjectName("action_Resume_previous")
self.actionE_xit = QtGui.QAction(parent=MainWindow) self.actionE_xit = QtGui.QAction(parent=MainWindow)
self.actionE_xit.setObjectName("actionE_xit") self.actionE_xit.setObjectName("actionE_xit")
@ -517,8 +591,14 @@ class Ui_MainWindow(object):
self.current_track_2.setText(_translate("MainWindow", "Current track:")) self.current_track_2.setText(_translate("MainWindow", "Current track:"))
self.next_track_2.setText(_translate("MainWindow", "Next track:")) self.next_track_2.setText(_translate("MainWindow", "Next track:"))
self.lblTOD.setText(_translate("MainWindow", "00:00:00")) self.lblTOD.setText(_translate("MainWindow", "00:00:00"))
self.btnPreview.setText(_translate("MainWindow", " Preview"))
self.label_elapsed_timer.setText(_translate("MainWindow", "00:00 / 00:00")) 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.btnDrop3db.setText(_translate("MainWindow", "-3dB to talk"))
self.btnHidePlayed.setText(_translate("MainWindow", "Hide played")) self.btnHidePlayed.setText(_translate("MainWindow", "Hide played"))
self.label_4.setText(_translate("MainWindow", "Fade")) self.label_4.setText(_translate("MainWindow", "Fade"))

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 from importlib import import_module
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context from alembic import context
from alchemical.alembic.env import run_migrations
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
config = context.config config = context.config
# Interpret the config file for Python logging. # import the application's Alchemical instance
# This line sets up loggers basically. try:
fileConfig(config.config_file_name) import_mod, db_name = config.get_main_option('alchemical_db', '').split(
':')
# add your model's MetaData object here db = getattr(import_module(import_mod), db_name)
# for 'autogenerate' support except (ModuleNotFoundError, AttributeError):
# from myapp import mymodel raise ValueError(
# target_metadata = mymodel.Base.metadata 'Could not import the Alchemical database instance. '
# https://stackoverflow.com/questions/32032940/how-to-import-the-own-model-into-myproject-alembic-env-py 'Ensure that the alchemical_db setting in alembic.ini is correct.'
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"},
) )
with context.begin_transaction(): # run the migration engine
context.run_migrations() # 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
def run_migrations_online(): run_migrations(db, {
"""Run migrations in 'online' mode. 'render_as_batch': True,
'compare_type': True,
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()

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