Compare commits

..

No commits in common. "a46b9a3d6f27ec87d6ca87b5a71f386bb5115498" and "b1f682d2e6326448cd5983e69e41614e57c7bfa1" have entirely different histories.

12 changed files with 1249 additions and 1294 deletions

View File

@ -2,13 +2,20 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import auto, Enum from enum import auto, Enum
from typing import Any, Optional from typing import Any, Optional
import datetime as dt
# PyQt imports # PyQt imports
from PyQt6.QtCore import pyqtSignal, QObject from PyQt6.QtCore import pyqtSignal, QObject, QThread
# Third party imports # Third party imports
import numpy as np
import pyqtgraph as pg # type: ignore
from sqlalchemy.orm import scoped_session
# App imports # App imports
from config import Config
from log import log
from models import PlaylistRows
import helpers import helpers
@ -25,6 +32,62 @@ class Col(Enum):
NOTE = auto() NOTE = auto()
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])
@helpers.singleton @helpers.singleton
@dataclass @dataclass
class MusicMusterSignals(QObject): class MusicMusterSignals(QObject):
@ -48,12 +111,117 @@ class MusicMusterSignals(QObject):
show_warning_signal = pyqtSignal(str, str) show_warning_signal = pyqtSignal(str, str)
span_cells_signal = pyqtSignal(int, int, int, int, int) span_cells_signal = pyqtSignal(int, int, int, int, int)
status_message_signal = pyqtSignal(str, int) status_message_signal = pyqtSignal(str, int)
track_ended_signal = pyqtSignal()
def __post_init__(self): def __post_init__(self):
super().__init__() 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.intro = track.intro
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 @dataclass
class TrackFileData: class TrackFileData:
""" """
@ -66,3 +234,46 @@ class TrackFileData:
obsolete_path: Optional[str] = None obsolete_path: Optional[str] = None
tags: dict[str, Any] = field(default_factory=dict) tags: dict[str, Any] = field(default_factory=dict)
audio_metadata: dict[str, str | int | float] = 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,9 +9,17 @@ class Config(object):
AUDIO_SEGMENT_CHUNK_SIZE = 10 AUDIO_SEGMENT_CHUNK_SIZE = 10
BITRATE_LOW_THRESHOLD = 192 BITRATE_LOW_THRESHOLD = 192
BITRATE_OK_THRESHOLD = 300 BITRATE_OK_THRESHOLD = 300
CART_DIRECTORY = "/home/kae/radio/CartTracks"
CARTS_COUNT = 10
CARTS_HIDE = True
COLOUR_BITRATE_LOW = "#ffcdd2" COLOUR_BITRATE_LOW = "#ffcdd2"
COLOUR_BITRATE_MEDIUM = "#ffeb6f" COLOUR_BITRATE_MEDIUM = "#ffeb6f"
COLOUR_BITRATE_OK = "#dcedc8" 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_PLAYLIST = "#7eca8f"
COLOUR_CURRENT_TAB = "#248f24" COLOUR_CURRENT_TAB = "#248f24"
COLOUR_ENDING_TIMER = "#dc3545" COLOUR_ENDING_TIMER = "#dc3545"
@ -52,6 +60,7 @@ 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_FORMAT = ".1f"
INTRO_SECONDS_WARNING_MS = 3000 INTRO_SECONDS_WARNING_MS = 3000
LAST_PLAYED_TODAY_STRING = "Today" LAST_PLAYED_TODAY_STRING = "Today"
@ -77,7 +86,6 @@ class Config(object):
PLAY_SETTLE = 500000 PLAY_SETTLE = 500000
PREVIEW_ADVANCE_MS = 5000 PREVIEW_ADVANCE_MS = 5000
PREVIEW_BACK_MS = 5000 PREVIEW_BACK_MS = 5000
PREVIEW_END_BUFFER_MS = 1000
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"

View File

@ -23,6 +23,23 @@ from sqlalchemy.orm import (
# Database classes # 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): class NoteColoursTable(Model):
__tablename__ = "notecolours" __tablename__ = "notecolours"

View File

@ -37,6 +37,28 @@ db = Alchemical(ALCHEMICAL_DATABASE_URI, engine_options=Config.ENGINE_OPTIONS)
# Database classes # 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): class NoteColours(dbtables.NoteColoursTable):
def __init__( def __init__(
self, self,

260
app/music.py Normal file
View File

@ -0,0 +1,260 @@
# 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, 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 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(f"Music[{self.name}].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 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.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 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:
"""
Start playing the track at path.
Log and return if path not found.
"""
log.info(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 = 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:
"""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)
def stop(self) -> float:
"""Immediately stop playing"""
log.info(f"Music[{self.name}].stop()")
self.start_dt = None
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

@ -30,7 +30,7 @@ import obswebsocket # type: ignore
# import snoop # type: ignore # import snoop # type: ignore
# App imports # App imports
from classes import Col, MusicMusterSignals 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,
@ -41,17 +41,13 @@ from helpers import (
) )
from log import log from log import log
from models import db, NoteColours, Playdates, PlaylistRows, Tracks from models import db, NoteColours, Playdates, PlaylistRows, Tracks
from trackmanager import (
MainTrackManager,
track_sequence,
)
HEADER_NOTES_COLUMN = 1 HEADER_NOTES_COLUMN = 1
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]") scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
class _PlaylistRowData: class PlaylistRowData:
def __init__(self, plr: PlaylistRows) -> None: def __init__(self, plr: PlaylistRows) -> None:
""" """
Populate PlaylistRowData from database PlaylistRows record Populate PlaylistRowData from database PlaylistRows record
@ -121,7 +117,7 @@ class PlaylistModel(QAbstractTableModel):
self.playlist_id = playlist_id self.playlist_id = playlist_id
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.playlist_rows: dict[int, _PlaylistRowData] = {} self.playlist_rows: dict[int, PlaylistRowData] = {}
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
self.played_tracks_hidden = False self.played_tracks_hidden = False
@ -182,7 +178,7 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id) 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""" """Return background setting"""
# Handle entire row colouring # Handle entire row colouring
@ -199,10 +195,10 @@ class PlaylistModel(QAbstractTableModel):
if file_is_unreadable(prd.path): if file_is_unreadable(prd.path):
return QBrush(QColor(Config.COLOUR_UNREADABLE)) return QBrush(QColor(Config.COLOUR_UNREADABLE))
# Current track # Current track
if track_sequence.current and track_sequence.current.track_id == prd.track_id: if prd.plrid == track_sequence.now.plr_id:
return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST)) return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST))
# Next track # Next track
if track_sequence.next and track_sequence.next.track_id == prd.track_id: if prd.plrid == track_sequence.next.plr_id:
return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST)) return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST))
# Individual cell colouring # Individual cell colouring
@ -254,10 +250,24 @@ class PlaylistModel(QAbstractTableModel):
- find next track - find next track
""" """
if not track_sequence.current: row_number = track_sequence.now.plr_rownum
return if row_number is not None:
prd = self.playlist_rows[row_number]
else:
prd = None
row_number = track_sequence.current.row_number # 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
# Check for OBS scene change # Check for OBS scene change
log.debug("Call OBS scene change") log.debug("Call OBS scene change")
@ -266,23 +276,29 @@ class PlaylistModel(QAbstractTableModel):
with db.Session() as session: with db.Session() as session:
# Update Playdates in database # Update Playdates in database
log.debug("update playdates") log.debug("update playdates")
Playdates(session, track_sequence.current.track_id) Playdates(session, track_sequence.now.track_id)
# Mark track as played in playlist # Mark track as played in playlist
log.debug("Mark track as played") log.debug("Mark track as played")
plr = session.get(PlaylistRows, track_sequence.current.plr_id) plr = session.get(PlaylistRows, track_sequence.now.plr_id)
if plr: if plr:
plr.played = True plr.played = True
self.refresh_row(session, plr.plr_rownum) self.refresh_row(session, plr.plr_rownum)
else: else:
log.error(f"Can't retrieve plr, {track_sequence.current.plr_id=}") 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
# Update colour and times for current row # Update colour and times for current row
self.invalidate_row(row_number) self.invalidate_row(row_number)
# Update previous row in case we're hiding played rows # Update previous row in case we're hiding played rows
if track_sequence.previous and track_sequence.previous.row_number: if track_sequence.previous.plr_rownum:
self.invalidate_row(track_sequence.previous.row_number) self.invalidate_row(track_sequence.previous.plr_rownum)
# Update all other track times # Update all other track times
self.update_track_times() self.update_track_times()
@ -377,11 +393,13 @@ class PlaylistModel(QAbstractTableModel):
self.reset_track_sequence_row_numbers() 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 Return text for display
""" """
log.debug(f"display_role({row=}, {column=}")
# Set / reset column span # Set / reset column span
if column == HEADER_NOTES_COLUMN: if column == HEADER_NOTES_COLUMN:
column_span = 1 column_span = 1
@ -448,7 +466,7 @@ class PlaylistModel(QAbstractTableModel):
super().endResetModel() super().endResetModel()
self.reset_track_sequence_row_numbers() 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 Return text for editing
""" """
@ -492,7 +510,7 @@ class PlaylistModel(QAbstractTableModel):
return default 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 Return font
""" """
@ -551,7 +569,7 @@ class PlaylistModel(QAbstractTableModel):
log.debug(f"get_new_row_number() return: {new_row_number=}") log.debug(f"get_new_row_number() return: {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 info about passed row
""" """
@ -641,7 +659,7 @@ class PlaylistModel(QAbstractTableModel):
return QVariant() return QVariant()
def header_text(self, prd: _PlaylistRowData) -> str: def header_text(self, prd: PlaylistRowData) -> str:
""" """
Process possible section timing directives embeded in header Process possible section timing directives embeded in header
""" """
@ -685,16 +703,16 @@ class PlaylistModel(QAbstractTableModel):
# calculate end time if all tracks are played. # calculate end time if all tracks are played.
end_time_str = "" end_time_str = ""
if ( if (
track_sequence.current track_sequence.now.plr_rownum
and track_sequence.current.end_time and track_sequence.now.end_time
and ( and (
row_number row_number
< track_sequence.current.row_number < track_sequence.now.plr_rownum
< prd.plr_rownum < prd.plr_rownum
) )
): ):
section_end_time = ( section_end_time = (
track_sequence.current.end_time track_sequence.now.end_time
+ dt.timedelta(milliseconds=duration) + dt.timedelta(milliseconds=duration)
) )
end_time_str = ( end_time_str = (
@ -812,7 +830,7 @@ class PlaylistModel(QAbstractTableModel):
return self.playlist_rows[row_number].played 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 If this track_id is in the playlist, return the PlaylistRowData object
else return None else return None
@ -888,16 +906,14 @@ class PlaylistModel(QAbstractTableModel):
row_map[old_row] = new_row row_map[old_row] = new_row
# Check to see whether any rows in track_sequence have moved # Check to see whether any rows in track_sequence have moved
if track_sequence.previous and track_sequence.previous.row_number in row_map: if track_sequence.previous.plr_rownum in row_map:
track_sequence.previous.row_number = row_map[ track_sequence.previous.plr_rownum = row_map[
track_sequence.previous.row_number track_sequence.previous.plr_rownum
] ]
if track_sequence.current and track_sequence.current.row_number in row_map: if track_sequence.now.plr_rownum in row_map:
track_sequence.current.row_number = row_map[ track_sequence.now.plr_rownum = row_map[track_sequence.now.plr_rownum]
track_sequence.current.row_number if track_sequence.next.plr_rownum in row_map:
] track_sequence.next.plr_rownum = row_map[track_sequence.next.plr_rownum]
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 # For SQLAlchemy, build a list of dictionaries that map plrid to
# new row number: # new row number:
@ -957,10 +973,7 @@ class PlaylistModel(QAbstractTableModel):
self.playlist_id, self.playlist_id,
[self.playlist_rows[a].plrid for a in row_group], [self.playlist_rows[a].plrid for a in row_group],
): ):
if ( if plr.id == track_sequence.now.plr_id:
track_sequence.current
and plr.id == track_sequence.current.plr_id
):
# Don't move current track # Don't move current track
continue continue
plr.playlist_id = to_playlist_id plr.playlist_id = to_playlist_id
@ -981,7 +994,7 @@ class PlaylistModel(QAbstractTableModel):
self.update_track_times() self.update_track_times()
def move_track_add_note( 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: ) -> None:
""" """
Move existing_prd track to new_row_number and append note to any existing note Move existing_prd track to new_row_number and append note to any existing note
@ -1005,10 +1018,7 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id) self.signals.resize_rows_signal.emit(self.playlist_id)
def move_track_to_header( def move_track_to_header(
self, self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
header_row_number: int,
existing_prd: _PlaylistRowData,
note: Optional[str],
) -> None: ) -> None:
""" """
Add the existing_prd track details to the existing header at header_row_number Add the existing_prd track details to the existing header at header_row_number
@ -1070,10 +1080,10 @@ class PlaylistModel(QAbstractTableModel):
log.info("previous_track_ended()") log.info("previous_track_ended()")
# Sanity check # Sanity check
if not track_sequence.previous: if not track_sequence.previous.track_id:
log.error("playlistmodel:previous_track_ended called with no current track") log.error("playlistmodel:previous_track_ended called with no current track")
return return
if track_sequence.previous.row_number is None: if track_sequence.previous.plr_rownum is None:
log.error( log.error(
"playlistmodel:previous_track_ended called with no row number " "playlistmodel:previous_track_ended called with no row number "
f"({track_sequence.previous=})" f"({track_sequence.previous=})"
@ -1081,7 +1091,7 @@ class PlaylistModel(QAbstractTableModel):
return return
# Update display # Update display
self.invalidate_row(track_sequence.previous.row_number) self.invalidate_row(track_sequence.previous.plr_rownum)
def refresh_data(self, session: db.session): def refresh_data(self, session: db.session):
"""Populate dicts for data calls""" """Populate dicts for data calls"""
@ -1089,13 +1099,13 @@ class PlaylistModel(QAbstractTableModel):
# Populate self.playlist_rows with playlist data # Populate self.playlist_rows with playlist data
self.playlist_rows.clear() self.playlist_rows.clear()
for p in PlaylistRows.deep_rows(session, self.playlist_id): 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): def refresh_row(self, session, row_number):
"""Populate dict for one row from database""" """Populate dict for one row from database"""
p = PlaylistRows.deep_row(session, self.playlist_id, row_number) 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: def remove_track(self, row_number: int) -> None:
""" """
@ -1135,23 +1145,21 @@ class PlaylistModel(QAbstractTableModel):
log.debug("reset_track_sequence_row_numbers()") log.debug("reset_track_sequence_row_numbers()")
# Check the track_sequence next, current and previous plrs and # Check the track_sequence next, now and previous plrs and
# update the row number # update the row number
with db.Session() as session: with db.Session() as session:
if track_sequence.next and track_sequence.next.row_number: if track_sequence.next.plr_rownum:
next_plr = session.get(PlaylistRows, track_sequence.next.row_number) next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
if next_plr: if next_plr:
track_sequence.next.row_number = next_plr.plr_rownum track_sequence.next.plr_rownum = next_plr.plr_rownum
if track_sequence.current and track_sequence.current.row_number: if track_sequence.now.plr_rownum:
now_plr = session.get(PlaylistRows, track_sequence.current.row_number) now_plr = session.get(PlaylistRows, track_sequence.now.plr_id)
if now_plr: if now_plr:
track_sequence.current.row_number = now_plr.plr_rownum track_sequence.now.plr_rownum = now_plr.plr_rownum
if track_sequence.previous and track_sequence.previous.row_number: if track_sequence.previous.plr_rownum:
previous_plr = session.get( previous_plr = session.get(PlaylistRows, track_sequence.previous.plr_id)
PlaylistRows, track_sequence.previous.row_number
)
if previous_plr: if previous_plr:
track_sequence.previous.row_number = previous_plr.plr_rownum track_sequence.previous.plr_rownum = previous_plr.plr_rownum
self.update_track_times() self.update_track_times()
@ -1227,69 +1235,64 @@ class PlaylistModel(QAbstractTableModel):
return True return True
def set_next_row(self, row_number: Optional[int]) -> bool: def set_next_row(self, row_number: Optional[int]) -> None:
""" """
Set row_number as next track. If row_number is None, clear next track. Set row_number as next track. If row_number is None, clear next track.
Return True if successful else False.
""" """
log.debug(f"set_next_row({row_number=})") log.info(f"set_next_row({row_number=})")
next_row_was = track_sequence.next.plr_rownum
if row_number is None: if row_number is None:
# Clear next track if next_row_was is None:
if track_sequence.next: return
track_sequence.next = None track_sequence.next = PlaylistTrack()
else: self.signals.next_track_changed_signal.emit()
return True return
else:
# Get plrid of row # Update track_sequence
with db.Session() as session:
track_sequence.next = PlaylistTrack()
try: try:
prd = self.playlist_rows[row_number] plrid = self.playlist_rows[row_number].plrid
except IndexError: except IndexError:
log.error( log.error(
f"playlistmodel.set_next_track({row_number=}, " f"playlistmodel.set_next_track({row_number=}, "
f"{self.playlist_id=}" f"{self.playlist_id=}"
"IndexError"
) )
return False return
if prd.track_id is None or prd.plr_rownum is None: plr = session.get(PlaylistRows, plrid)
log.error( if plr:
f"playlistmodel.set_next_track({row_number=}, " # Check this isn't a header row
"No track / row number " if self.is_header_row(row_number):
f"{self.playlist_id=}, {prd.track_id=}, {prd.plr_rownum=}" log.error(
"Tried to set next row on header row: "
f"playlistmodel.set_next_track({row_number=}, "
f"{self.playlist_id=}"
)
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=}"
)
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
) )
return False self.invalidate_row(row_number)
old_next_row: Optional[int] = None if next_row_was is not None:
if track_sequence.next: self.invalidate_row(next_row_was)
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 MainTrackManager({prd=}): ({str(e)})")
return False
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)
self.signals.next_track_changed_signal.emit()
self.update_track_times() self.update_track_times()
return True
def setData( def setData(
self, self, index: QModelIndex, value: str | float, role: int = Qt.ItemDataRole.EditRole
index: QModelIndex,
value: str | float,
role: int = Qt.ItemDataRole.EditRole,
) -> bool: ) -> bool:
""" """
Update model with edited data Update model with edited data
@ -1393,7 +1396,7 @@ class PlaylistModel(QAbstractTableModel):
def supportedDropActions(self) -> Qt.DropAction: def supportedDropActions(self) -> Qt.DropAction:
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction 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. Return tooltip. Currently only used for last_played column.
""" """
@ -1409,7 +1412,7 @@ class PlaylistModel(QAbstractTableModel):
"<br>".join( "<br>".join(
[ [
a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT) a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
for a in reversed(playdates) for a in playdates
] ]
) )
) )
@ -1431,25 +1434,24 @@ class PlaylistModel(QAbstractTableModel):
prd = self.playlist_rows[row_number] prd = self.playlist_rows[row_number]
# Reset start_time if this is the current row # Reset start_time if this is the current row
if track_sequence.current: if row_number == track_sequence.now.plr_rownum:
if row_number == track_sequence.current.row_number: prd.start_time = track_sequence.now.start_time
prd.start_time = track_sequence.current.start_time prd.end_time = track_sequence.now.end_time
prd.end_time = track_sequence.current.end_time update_rows.append(row_number)
update_rows.append(row_number) if not next_start_time:
if not next_start_time: next_start_time = prd.end_time
next_start_time = prd.end_time continue
continue
# Set start time for next row if we have a current track # Set start time for next row if we have a current track
if track_sequence.next and track_sequence.current.end_time: if (
if row_number == track_sequence.next.row_number: row_number == track_sequence.next.plr_rownum
prd.start_time = track_sequence.current.end_time and track_sequence.now.end_time
prd.end_time = prd.start_time + dt.timedelta( ):
milliseconds=prd.duration prd.start_time = track_sequence.now.end_time
) prd.end_time = prd.start_time + dt.timedelta(milliseconds=prd.duration)
next_start_time = prd.end_time next_start_time = prd.end_time
update_rows.append(row_number) update_rows.append(row_number)
continue continue
# Don't update times for tracks that have been played # Don't update times for tracks that have been played
if prd.played: if prd.played:
@ -1458,11 +1460,11 @@ class PlaylistModel(QAbstractTableModel):
# If we're between the current and next row, zero out # If we're between the current and next row, zero out
# times # times
if ( if (
track_sequence.current track_sequence.now.plr_rownum is not None
and track_sequence.next and track_sequence.next.plr_rownum is not None
and track_sequence.current.row_number and track_sequence.now.plr_rownum
< row_number < row_number
< track_sequence.next.row_number < track_sequence.next.plr_rownum
): ):
prd.start_time = None prd.start_time = None
prd.end_time = None prd.end_time = None
@ -1537,7 +1539,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
if self.source_model.is_played_row(source_row): if self.source_model.is_played_row(source_row):
# Don't hide current or next track # Don't hide current or next track
with db.Session() as session: with db.Session() as session:
if track_sequence.next: if track_sequence.next.plr_id:
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
if ( if (
next_plr next_plr
@ -1545,10 +1547,8 @@ class PlaylistProxyModel(QSortFilterProxyModel):
and next_plr.playlist_id == self.source_model.playlist_id and next_plr.playlist_id == self.source_model.playlist_id
): ):
return True return True
if track_sequence.current: if track_sequence.now.plr_id:
now_plr = session.get( now_plr = session.get(PlaylistRows, track_sequence.now.plr_id)
PlaylistRows, track_sequence.current.plr_id
)
if ( if (
now_plr now_plr
and now_plr.plr_rownum == source_row and now_plr.plr_rownum == source_row
@ -1558,20 +1558,19 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# Don't hide previous track until # Don't hide previous track until
# HIDE_AFTER_PLAYING_OFFSET milliseconds after # HIDE_AFTER_PLAYING_OFFSET milliseconds after
# current track has started # current track has started
if track_sequence.previous: if track_sequence.previous.plr_id:
previous_plr = session.get( previous_plr = session.get(
PlaylistRows, track_sequence.previous.plr_id PlaylistRows, track_sequence.previous.plr_id
) )
if ( if (
track_sequence.current previous_plr
and previous_plr
and previous_plr.plr_rownum == source_row and previous_plr.plr_rownum == source_row
and previous_plr.playlist_id and previous_plr.playlist_id
== self.source_model.playlist_id == self.source_model.playlist_id
): ):
if track_sequence.current.start_time: if track_sequence.now.start_time:
if dt.datetime.now() > ( if dt.datetime.now() > (
track_sequence.current.start_time track_sequence.now.start_time
+ dt.timedelta( + dt.timedelta(
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
) )
@ -1624,7 +1623,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
def get_rows_duration(self, row_numbers: List[int]) -> int: def get_rows_duration(self, row_numbers: List[int]) -> int:
return self.source_model.get_rows_duration(row_numbers) 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) return self.source_model.get_row_info(row_number)
def get_row_track_path(self, row_number: int) -> str: def get_row_track_path(self, row_number: int) -> str:
@ -1650,7 +1649,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
def is_played_row(self, row_number: int) -> bool: def is_played_row(self, row_number: int) -> bool:
return self.source_model.is_played_row(row_number) 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) return self.source_model.is_track_in_playlist(track_id)
def mark_unplayed(self, row_numbers: List[int]) -> None: def mark_unplayed(self, row_numbers: List[int]) -> None:
@ -1667,15 +1666,12 @@ class PlaylistProxyModel(QSortFilterProxyModel):
) )
def move_track_add_note( 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: ) -> None:
return self.source_model.move_track_add_note(new_row_number, existing_prd, note) return self.source_model.move_track_add_note(new_row_number, existing_prd, note)
def move_track_to_header( def move_track_to_header(
self, self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
header_row_number: int,
existing_prd: _PlaylistRowData,
note: Optional[str],
) -> None: ) -> None:
return self.source_model.move_track_to_header( return self.source_model.move_track_to_header(
header_row_number, existing_prd, note header_row_number, existing_prd, note
@ -1690,7 +1686,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
def rescan_track(self, row_number: int) -> None: def rescan_track(self, row_number: int) -> None:
return self.source_model.rescan_track(row_number) return self.source_model.rescan_track(row_number)
def set_next_row(self, row_number: Optional[int]) -> bool: def set_next_row(self, row_number: Optional[int]) -> None:
return self.source_model.set_next_row(row_number) return self.source_model.set_next_row(row_number)
def sort_by_artist(self, row_numbers: List[int]) -> None: def sort_by_artist(self, row_numbers: List[int]) -> None:

View File

@ -35,7 +35,7 @@ from PyQt6.QtWidgets import (
# Third party imports # Third party imports
# App imports # App imports
from classes import Col, MusicMusterSignals 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 (
@ -47,7 +47,6 @@ from helpers import (
from log import log from log import log
from models import db, Settings from models import db, Settings
from playlistmodel import PlaylistModel, PlaylistProxyModel from playlistmodel import PlaylistModel, PlaylistProxyModel
from trackmanager import track_sequence
if TYPE_CHECKING: if TYPE_CHECKING:
from musicmuster import Window from musicmuster import Window
@ -119,13 +118,9 @@ class EscapeDelegate(QStyledItemDelegate):
# Close editor if no changes have been made # Close editor if no changes have been made
data_modified = False data_modified = False
if isinstance(self.editor, QPlainTextEdit): if isinstance(self.editor, QPlainTextEdit):
data_modified = ( data_modified = self.original_model_data == self.editor.toPlainText()
self.original_model_data == self.editor.toPlainText()
)
elif isinstance(self.editor, QDoubleSpinBox): elif isinstance(self.editor, QDoubleSpinBox):
data_modified = ( data_modified = self.original_model_data == int(self.editor.value()) * 1000
self.original_model_data == int(self.editor.value()) * 1000
)
if data_modified: if data_modified:
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
return True return True
@ -430,18 +425,12 @@ class PlaylistTab(QTableView):
header_row = proxy_model.is_header_row(model_row_number) header_row = proxy_model.is_header_row(model_row_number)
track_row = not header_row track_row = not header_row
if track_sequence.current: current_row = model_row_number == track_sequence.now.plr_rownum
this_is_current_row = model_row_number == track_sequence.current.row_number next_row = model_row_number == track_sequence.next.plr_rownum
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 track_path = self.source_model.get_row_info(model_row_number).path
# Open/import in/from Audacity # Open/import in/from Audacity
if track_row and not this_is_current_row: if track_row and not current_row:
if track_path == self.musicmuster.audacity_file_path: if track_path == self.musicmuster.audacity_file_path:
# This track was opened in Audacity # This track was opened in Audacity
self._add_context_menu( self._add_context_menu(
@ -458,7 +447,7 @@ class PlaylistTab(QTableView):
) )
# Rescan # Rescan
if track_row and not this_is_current_row: if track_row and not current_row:
self._add_context_menu( self._add_context_menu(
"Rescan track", lambda: self._rescan(model_row_number) "Rescan track", lambda: self._rescan(model_row_number)
) )
@ -467,11 +456,11 @@ class PlaylistTab(QTableView):
self.menu.addSeparator() self.menu.addSeparator()
# Delete row # Delete row
if not this_is_current_row and not this_is_next_row: if not current_row and not next_row:
self._add_context_menu("Delete row", lambda: self._delete_rows()) self._add_context_menu("Delete row", lambda: self._delete_rows())
# Remove track from row # Remove track from row
if track_row and not this_is_current_row and not this_is_next_row: if track_row and not current_row and not next_row:
self._add_context_menu( self._add_context_menu(
"Remove track from row", "Remove track from row",
lambda: proxy_model.remove_track(model_row_number), lambda: proxy_model.remove_track(model_row_number),
@ -492,7 +481,7 @@ class PlaylistTab(QTableView):
) )
# Unmark as next # Unmark as next
if this_is_next_row: if next_row:
self._add_context_menu( self._add_context_menu(
"Unmark as next track", lambda: self._unmark_as_next() "Unmark as next track", lambda: self._unmark_as_next()
) )
@ -631,23 +620,21 @@ 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_and_track_id(self) -> Optional[tuple[int, int]]: def get_selected_row_track_id(self) -> Optional[int]:
""" """
Return the (row_number, track_id) of the selected row. If no row selected or selected Return the track_id of the selected row. If no row selected or selected
row does not have a track, return None. row does not have a track, return None.
""" """
row_number = self.source_model_selected_row_number() log.debug("get_selected_row_track_id() called")
if row_number is None:
model_row_number = self.source_model_selected_row_number()
if model_row_number is None:
result = None result = None
else: else:
track_id = self.source_model.get_row_track_id(row_number) result = self.source_model.get_row_track_id(model_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=}") log.debug(f"get_selected_row_track_id() returned: {result=}")
return result return result

View File

@ -1,668 +0,0 @@
# 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,
preview_player: bool = False,
) -> 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.preview_player = preview_player
# Check file readable
if file_is_unreadable(track.path):
raise ValueError(f"_TrackManager.__init__: {track.path=} unreadable")
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
if not self.preview_player:
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.preview_player:
return
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.signal_end_of_track()
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.signal_end_of_track()
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 signal_end_of_track(self) -> None:
"""
Send end of track signal unless we are a preview player
"""
if not self.preview_player:
self.signals.track_ended_signal.emit()
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,
preview_player=True,
)
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

@ -15,11 +15,7 @@ class Ui_MainWindow(object):
MainWindow.resize(1280, 857) MainWindow.resize(1280, 857)
MainWindow.setMinimumSize(QtCore.QSize(1280, 0)) MainWindow.setMinimumSize(QtCore.QSize(1280, 0))
icon = QtGui.QIcon() icon = QtGui.QIcon()
icon.addPixmap( icon.addPixmap(QtGui.QPixmap(":/icons/musicmuster"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap(":/icons/musicmuster"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
MainWindow.setWindowIcon(icon) MainWindow.setWindowIcon(icon)
MainWindow.setStyleSheet("") MainWindow.setStyleSheet("")
self.centralwidget = QtWidgets.QWidget(parent=MainWindow) self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
@ -31,62 +27,39 @@ class Ui_MainWindow(object):
self.verticalLayout_3 = QtWidgets.QVBoxLayout() self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3") self.verticalLayout_3.setObjectName("verticalLayout_3")
self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget) self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy( sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth( sizePolicy.setHeightForWidth(self.previous_track_2.sizePolicy().hasHeightForWidth())
self.previous_track_2.sizePolicy().hasHeightForWidth()
)
self.previous_track_2.setSizePolicy(sizePolicy) self.previous_track_2.setSizePolicy(sizePolicy)
self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215)) self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont() font = QtGui.QFont()
font.setFamily("Sans") font.setFamily("Sans")
font.setPointSize(20) font.setPointSize(20)
self.previous_track_2.setFont(font) self.previous_track_2.setFont(font)
self.previous_track_2.setStyleSheet( self.previous_track_2.setStyleSheet("background-color: #f8d7da;\n"
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);" "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.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.previous_track_2.setObjectName("previous_track_2") self.previous_track_2.setObjectName("previous_track_2")
self.verticalLayout_3.addWidget(self.previous_track_2) self.verticalLayout_3.addWidget(self.previous_track_2)
self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget) self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy( sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth( sizePolicy.setHeightForWidth(self.current_track_2.sizePolicy().hasHeightForWidth())
self.current_track_2.sizePolicy().hasHeightForWidth()
)
self.current_track_2.setSizePolicy(sizePolicy) self.current_track_2.setSizePolicy(sizePolicy)
self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215)) self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont() font = QtGui.QFont()
font.setFamily("Sans") font.setFamily("Sans")
font.setPointSize(20) font.setPointSize(20)
self.current_track_2.setFont(font) self.current_track_2.setFont(font)
self.current_track_2.setStyleSheet( self.current_track_2.setStyleSheet("background-color: #d4edda;\n"
"background-color: #d4edda;\n" "border: 1px solid rgb(85, 87, 83);" "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.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.current_track_2.setObjectName("current_track_2") self.current_track_2.setObjectName("current_track_2")
self.verticalLayout_3.addWidget(self.current_track_2) self.verticalLayout_3.addWidget(self.current_track_2)
self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget) self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy( sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth())
@ -96,29 +69,19 @@ class Ui_MainWindow(object):
font.setFamily("Sans") font.setFamily("Sans")
font.setPointSize(20) font.setPointSize(20)
self.next_track_2.setFont(font) self.next_track_2.setFont(font)
self.next_track_2.setStyleSheet( self.next_track_2.setStyleSheet("background-color: #fff3cd;\n"
"background-color: #fff3cd;\n" "border: 1px solid rgb(85, 87, 83);" "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.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.next_track_2.setObjectName("next_track_2") self.next_track_2.setObjectName("next_track_2")
self.verticalLayout_3.addWidget(self.next_track_2) self.verticalLayout_3.addWidget(self.next_track_2)
self.horizontalLayout_3.addLayout(self.verticalLayout_3) self.horizontalLayout_3.addLayout(self.verticalLayout_3)
self.verticalLayout = QtWidgets.QVBoxLayout() self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setObjectName("verticalLayout")
self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget) self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy( sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth( sizePolicy.setHeightForWidth(self.hdrPreviousTrack.sizePolicy().hasHeightForWidth())
self.hdrPreviousTrack.sizePolicy().hasHeightForWidth()
)
self.hdrPreviousTrack.setSizePolicy(sizePolicy) self.hdrPreviousTrack.setSizePolicy(sizePolicy)
self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0)) self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0))
self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215)) self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215))
@ -126,43 +89,32 @@ class Ui_MainWindow(object):
font.setFamily("Sans") font.setFamily("Sans")
font.setPointSize(20) font.setPointSize(20)
self.hdrPreviousTrack.setFont(font) self.hdrPreviousTrack.setFont(font)
self.hdrPreviousTrack.setStyleSheet( self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);" "border: 1px solid rgb(85, 87, 83);")
)
self.hdrPreviousTrack.setText("") self.hdrPreviousTrack.setText("")
self.hdrPreviousTrack.setWordWrap(False) self.hdrPreviousTrack.setWordWrap(False)
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack") self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
self.verticalLayout.addWidget(self.hdrPreviousTrack) self.verticalLayout.addWidget(self.hdrPreviousTrack)
self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget) self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy( sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth( sizePolicy.setHeightForWidth(self.hdrCurrentTrack.sizePolicy().hasHeightForWidth())
self.hdrCurrentTrack.sizePolicy().hasHeightForWidth()
)
self.hdrCurrentTrack.setSizePolicy(sizePolicy) self.hdrCurrentTrack.setSizePolicy(sizePolicy)
font = QtGui.QFont() font = QtGui.QFont()
font.setPointSize(20) font.setPointSize(20)
self.hdrCurrentTrack.setFont(font) self.hdrCurrentTrack.setFont(font)
self.hdrCurrentTrack.setStyleSheet( self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
"background-color: #d4edda;\n" "border: 1px solid rgb(85, 87, 83);\n"
"border: 1px solid rgb(85, 87, 83);\n" "text-align: left;\n"
"text-align: left;\n" "padding-left: 8px;\n"
"padding-left: 8px;\n" "")
""
)
self.hdrCurrentTrack.setText("") self.hdrCurrentTrack.setText("")
self.hdrCurrentTrack.setFlat(True) self.hdrCurrentTrack.setFlat(True)
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack") self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
self.verticalLayout.addWidget(self.hdrCurrentTrack) self.verticalLayout.addWidget(self.hdrCurrentTrack)
self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget) self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy( sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth())
@ -170,12 +122,10 @@ class Ui_MainWindow(object):
font = QtGui.QFont() font = QtGui.QFont()
font.setPointSize(20) font.setPointSize(20)
self.hdrNextTrack.setFont(font) self.hdrNextTrack.setFont(font)
self.hdrNextTrack.setStyleSheet( self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
"background-color: #fff3cd;\n" "border: 1px solid rgb(85, 87, 83);\n"
"border: 1px solid rgb(85, 87, 83);\n" "text-align: left;\n"
"text-align: left;\n" "padding-left: 8px;")
"padding-left: 8px;"
)
self.hdrNextTrack.setText("") self.hdrNextTrack.setText("")
self.hdrNextTrack.setFlat(True) self.hdrNextTrack.setFlat(True)
self.hdrNextTrack.setObjectName("hdrNextTrack") self.hdrNextTrack.setObjectName("hdrNextTrack")
@ -222,12 +172,7 @@ class Ui_MainWindow(object):
self.cartsWidget.setObjectName("cartsWidget") self.cartsWidget.setObjectName("cartsWidget")
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget) self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts") self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts")
spacerItem = QtWidgets.QSpacerItem( spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
40,
20,
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Minimum,
)
self.horizontalLayout_Carts.addItem(spacerItem) self.horizontalLayout_Carts.addItem(spacerItem)
self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1) self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget) self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget)
@ -272,11 +217,7 @@ class Ui_MainWindow(object):
self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame) self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
self.btnPreview.setMinimumSize(QtCore.QSize(132, 41)) self.btnPreview.setMinimumSize(QtCore.QSize(132, 41))
icon1 = QtGui.QIcon() icon1 = QtGui.QIcon()
icon1.addPixmap( icon1.addPixmap(QtGui.QPixmap(":/icons/headphones"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap(":/icons/headphones"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnPreview.setIcon(icon1) self.btnPreview.setIcon(icon1)
self.btnPreview.setIconSize(QtCore.QSize(30, 30)) self.btnPreview.setIconSize(QtCore.QSize(30, 30))
self.btnPreview.setCheckable(True) self.btnPreview.setCheckable(True)
@ -298,16 +239,8 @@ class Ui_MainWindow(object):
self.btnPreviewArm.setMaximumSize(QtCore.QSize(44, 23)) self.btnPreviewArm.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setText("") self.btnPreviewArm.setText("")
icon2 = QtGui.QIcon() icon2 = QtGui.QIcon()
icon2.addPixmap( icon2.addPixmap(QtGui.QPixmap(":/icons/record-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap(":/icons/record-button.png"), icon2.addPixmap(QtGui.QPixmap(":/icons/record-red-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
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.setIcon(icon2)
self.btnPreviewArm.setCheckable(True) self.btnPreviewArm.setCheckable(True)
self.btnPreviewArm.setObjectName("btnPreviewArm") self.btnPreviewArm.setObjectName("btnPreviewArm")
@ -328,16 +261,8 @@ class Ui_MainWindow(object):
self.btnPreviewMark.setMaximumSize(QtCore.QSize(44, 23)) self.btnPreviewMark.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setText("") self.btnPreviewMark.setText("")
icon3 = QtGui.QIcon() icon3 = QtGui.QIcon()
icon3.addPixmap( icon3.addPixmap(QtGui.QPixmap(":/icons/star.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
QtGui.QPixmap(":/icons/star.png"), icon3.addPixmap(QtGui.QPixmap(":/icons/star_empty.png"), QtGui.QIcon.Mode.Disabled, QtGui.QIcon.State.Off)
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.setIcon(icon3)
self.btnPreviewMark.setObjectName("btnPreviewMark") self.btnPreviewMark.setObjectName("btnPreviewMark")
self.btnPreviewFwd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls) self.btnPreviewFwd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
@ -438,15 +363,10 @@ class Ui_MainWindow(object):
self.verticalLayout_7.addWidget(self.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( sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(1) sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth( sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth())
self.widgetFadeVolume.sizePolicy().hasHeightForWidth()
)
self.widgetFadeVolume.setSizePolicy(sizePolicy) self.widgetFadeVolume.setSizePolicy(sizePolicy)
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0)) self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
self.widgetFadeVolume.setObjectName("widgetFadeVolume") self.widgetFadeVolume.setObjectName("widgetFadeVolume")
@ -463,11 +383,7 @@ class Ui_MainWindow(object):
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))
icon4 = QtGui.QIcon() icon4 = QtGui.QIcon()
icon4.addPixmap( icon4.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap(":/icons/fade"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnFade.setIcon(icon4) 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")
@ -475,11 +391,7 @@ class Ui_MainWindow(object):
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))
icon5 = QtGui.QIcon() icon5 = QtGui.QIcon()
icon5.addPixmap( icon5.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap(":/icons/stopsign"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnStop.setIcon(icon5) self.btnStop.setIcon(icon5)
self.btnStop.setObjectName("btnStop") self.btnStop.setObjectName("btnStop")
self.verticalLayout_5.addWidget(self.btnStop) self.verticalLayout_5.addWidget(self.btnStop)
@ -505,71 +417,39 @@ class Ui_MainWindow(object):
MainWindow.setStatusBar(self.statusbar) MainWindow.setStatusBar(self.statusbar)
self.actionPlay_next = QtGui.QAction(parent=MainWindow) self.actionPlay_next = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon() icon6 = QtGui.QIcon()
icon6.addPixmap( icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
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.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)
icon7 = QtGui.QIcon() icon7 = QtGui.QIcon()
icon7.addPixmap( icon7.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap(":/icons/next"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionSkipToNext.setIcon(icon7) self.actionSkipToNext.setIcon(icon7)
self.actionSkipToNext.setObjectName("actionSkipToNext") self.actionSkipToNext.setObjectName("actionSkipToNext")
self.actionInsertTrack = QtGui.QAction(parent=MainWindow) self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon() icon8 = QtGui.QIcon()
icon8.addPixmap( icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap(
"app/ui/../../../../../../.designer/backup/icon_search_database.png"
),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionInsertTrack.setIcon(icon8) 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)
icon9 = QtGui.QIcon() icon9 = QtGui.QIcon()
icon9.addPixmap( icon9.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
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.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)
icon10 = QtGui.QIcon() icon10 = QtGui.QIcon()
icon10.addPixmap( icon10.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionFade.setIcon(icon10) self.actionFade.setIcon(icon10)
self.actionFade.setObjectName("actionFade") self.actionFade.setObjectName("actionFade")
self.actionStop = QtGui.QAction(parent=MainWindow) self.actionStop = QtGui.QAction(parent=MainWindow)
icon11 = QtGui.QIcon() icon11 = QtGui.QIcon()
icon11.addPixmap( icon11.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap(":/icons/stop"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionStop.setIcon(icon11) 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)
icon12 = QtGui.QIcon() icon12 = QtGui.QIcon()
icon12.addPixmap( icon12.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
QtGui.QPixmap(":/icons/previous"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.action_Resume_previous.setIcon(icon12) 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)
@ -616,9 +496,7 @@ class Ui_MainWindow(object):
self.actionImport = QtGui.QAction(parent=MainWindow) self.actionImport = QtGui.QAction(parent=MainWindow)
self.actionImport.setObjectName("actionImport") self.actionImport.setObjectName("actionImport")
self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow) self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow)
self.actionDownload_CSV_of_played_tracks.setObjectName( self.actionDownload_CSV_of_played_tracks.setObjectName("actionDownload_CSV_of_played_tracks")
"actionDownload_CSV_of_played_tracks"
)
self.actionSearch = QtGui.QAction(parent=MainWindow) self.actionSearch = QtGui.QAction(parent=MainWindow)
self.actionSearch.setObjectName("actionSearch") self.actionSearch.setObjectName("actionSearch")
self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow) self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow)
@ -646,13 +524,9 @@ class Ui_MainWindow(object):
self.actionResume = QtGui.QAction(parent=MainWindow) self.actionResume = QtGui.QAction(parent=MainWindow)
self.actionResume.setObjectName("actionResume") self.actionResume.setObjectName("actionResume")
self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow) self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Wikipedia.setObjectName( self.actionSearch_title_in_Wikipedia.setObjectName("actionSearch_title_in_Wikipedia")
"actionSearch_title_in_Wikipedia"
)
self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow) self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Songfacts.setObjectName( self.actionSearch_title_in_Songfacts.setObjectName("actionSearch_title_in_Songfacts")
"actionSearch_title_in_Songfacts"
)
self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow) self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow)
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows") self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
self.actionReplace_files = QtGui.QAction(parent=MainWindow) self.actionReplace_files = QtGui.QAction(parent=MainWindow)
@ -707,7 +581,7 @@ class Ui_MainWindow(object):
self.retranslateUi(MainWindow) self.retranslateUi(MainWindow)
self.tabPlaylist.setCurrentIndex(-1) self.tabPlaylist.setCurrentIndex(-1)
self.tabInfolist.setCurrentIndex(-1) self.tabInfolist.setCurrentIndex(-1)
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow): def retranslateUi(self, MainWindow):
@ -749,58 +623,38 @@ class Ui_MainWindow(object):
self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z")) self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z"))
self.actionStop.setText(_translate("MainWindow", "S&top")) self.actionStop.setText(_translate("MainWindow", "S&top"))
self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S")) self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S"))
self.action_Clear_selection.setText( self.action_Clear_selection.setText(_translate("MainWindow", "Clear &selection"))
_translate("MainWindow", "Clear &selection")
)
self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc")) self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc"))
self.action_Resume_previous.setText( self.action_Resume_previous.setText(_translate("MainWindow", "&Resume previous"))
_translate("MainWindow", "&Resume previous")
)
self.actionE_xit.setText(_translate("MainWindow", "E&xit")) self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
self.actionTest.setText(_translate("MainWindow", "&Test")) self.actionTest.setText(_translate("MainWindow", "&Test"))
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen...")) self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
self.actionNewPlaylist.setText(_translate("MainWindow", "&New...")) self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
self.actionTestFunction.setText(_translate("MainWindow", "&Test function")) self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
self.actionSkipToFade.setText( self.actionSkipToFade.setText(_translate("MainWindow", "&Skip to start of fade"))
_translate("MainWindow", "&Skip to start of fade")
)
self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track")) self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track"))
self.actionClosePlaylist.setText(_translate("MainWindow", "&Close")) self.actionClosePlaylist.setText(_translate("MainWindow", "&Close"))
self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename...")) self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename..."))
self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te...")) self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te..."))
self.actionMoveSelected.setText( self.actionMoveSelected.setText(_translate("MainWindow", "Mo&ve selected tracks to..."))
_translate("MainWindow", "Mo&ve selected tracks to...")
)
self.actionExport_playlist.setText(_translate("MainWindow", "E&xport...")) self.actionExport_playlist.setText(_translate("MainWindow", "E&xport..."))
self.actionSetNext.setText(_translate("MainWindow", "Set &next")) self.actionSetNext.setText(_translate("MainWindow", "Set &next"))
self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N")) self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N"))
self.actionSelect_next_track.setText( self.actionSelect_next_track.setText(_translate("MainWindow", "Select next track"))
_translate("MainWindow", "Select next track")
)
self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J")) self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J"))
self.actionSelect_previous_track.setText( self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track"))
_translate("MainWindow", "Select previous track")
)
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K")) self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
self.actionSelect_played_tracks.setText( self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
_translate("MainWindow", "Select played tracks") self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to..."))
)
self.actionMoveUnplayed.setText(
_translate("MainWindow", "Move &unplayed tracks to...")
)
self.actionAdd_note.setText(_translate("MainWindow", "Add note...")) self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T")) self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls")) self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
self.actionImport.setText(_translate("MainWindow", "Import track...")) self.actionImport.setText(_translate("MainWindow", "Import track..."))
self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I")) self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I"))
self.actionDownload_CSV_of_played_tracks.setText( self.actionDownload_CSV_of_played_tracks.setText(_translate("MainWindow", "Download CSV of played tracks..."))
_translate("MainWindow", "Download CSV of played tracks...")
)
self.actionSearch.setText(_translate("MainWindow", "Search...")) self.actionSearch.setText(_translate("MainWindow", "Search..."))
self.actionSearch.setShortcut(_translate("MainWindow", "/")) self.actionSearch.setShortcut(_translate("MainWindow", "/"))
self.actionInsertSectionHeader.setText( self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert &section header..."))
_translate("MainWindow", "Insert &section header...")
)
self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H")) self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H"))
self.actionRemove.setText(_translate("MainWindow", "&Remove track")) self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
self.actionFind_next.setText(_translate("MainWindow", "Find next")) self.actionFind_next.setText(_translate("MainWindow", "Find next"))
@ -808,12 +662,8 @@ class Ui_MainWindow(object):
self.actionFind_previous.setText(_translate("MainWindow", "Find previous")) self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
self.actionFind_previous.setShortcut(_translate("MainWindow", "P")) self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
self.action_About.setText(_translate("MainWindow", "&About")) self.action_About.setText(_translate("MainWindow", "&About"))
self.actionSave_as_template.setText( self.actionSave_as_template.setText(_translate("MainWindow", "Save as template..."))
_translate("MainWindow", "Save as template...") self.actionNew_from_template.setText(_translate("MainWindow", "New from template..."))
)
self.actionNew_from_template.setText(
_translate("MainWindow", "New from template...")
)
self.actionDebug.setText(_translate("MainWindow", "Debug")) self.actionDebug.setText(_translate("MainWindow", "Debug"))
self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1...")) self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1..."))
self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving")) self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving"))
@ -822,23 +672,11 @@ class Ui_MainWindow(object):
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V")) self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
self.actionResume.setText(_translate("MainWindow", "Resume")) self.actionResume.setText(_translate("MainWindow", "Resume"))
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R")) self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
self.actionSearch_title_in_Wikipedia.setText( self.actionSearch_title_in_Wikipedia.setText(_translate("MainWindow", "Search title in Wikipedia"))
_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_Wikipedia.setShortcut( self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S"))
_translate("MainWindow", "Ctrl+W") self.actionSelect_duplicate_rows.setText(_translate("MainWindow", "Select duplicate rows..."))
)
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...")) self.actionReplace_files.setText(_translate("MainWindow", "Replace files..."))
from infotabs import InfoTabs from infotabs import InfoTabs
from pyqtgraph import PlotWidget from pyqtgraph import PlotWidget

View File

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

View File

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