Compare commits

..

No commits in common. "b8fcc79f8eab0b87bf68a4b6faacbe31c69374ff" and "50d1e8bd4ac24d8ca84a09e40b864b168facdf5f" have entirely different histories.

5 changed files with 210 additions and 242 deletions

View File

@ -6,6 +6,7 @@ from dataclasses import dataclass, field
import datetime as dt import datetime as dt
from enum import auto, Enum from enum import auto, Enum
import platform import platform
import threading
from time import sleep from time import sleep
from typing import Any, Optional, NamedTuple from typing import Any, Optional, NamedTuple
@ -18,7 +19,9 @@ import vlc # type: ignore
from PyQt6.QtCore import ( from PyQt6.QtCore import (
pyqtSignal, pyqtSignal,
QObject, QObject,
QRunnable,
QThread, QThread,
QThreadPool,
) )
from pyqtgraph import PlotWidget from pyqtgraph import PlotWidget
from pyqtgraph.graphicsItems.PlotDataItem import PlotDataItem # type: ignore from pyqtgraph.graphicsItems.PlotDataItem import PlotDataItem # type: ignore
@ -34,18 +37,18 @@ from helpers import (
show_warning, show_warning,
singleton, singleton,
) )
from vlcmanager import VLCManager
lock = threading.Lock()
# Define the VLC callback function type # Define the VLC callback function type
# VLC logging is very noisy so comment out unless needed VLC_LOG_CB = ctypes.CFUNCTYPE(
# VLC_LOG_CB = ctypes.CFUNCTYPE( None,
# None, ctypes.c_void_p,
# ctypes.c_void_p, ctypes.c_int,
# ctypes.c_int, ctypes.c_void_p,
# ctypes.c_void_p, ctypes.c_char_p,
# ctypes.c_char_p, ctypes.c_void_p,
# ctypes.c_void_p, )
# )
# Determine the correct C library for vsnprintf based on the platform # Determine the correct C library for vsnprintf based on the platform
if platform.system() == "Windows": if platform.system() == "Windows":
@ -135,8 +138,8 @@ class _FadeCurve:
0, track_fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1 0, track_fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1
) )
self.end_ms: int = track_silence_at self.end_ms: int = track_silence_at
audio_segment = audio[self.start_ms : self.end_ms] self.audio_segment = audio[self.start_ms : self.end_ms]
self.graph_array = np.array(audio_segment.get_array_of_samples()) self.graph_array = np.array(self.audio_segment.get_array_of_samples())
# Calculate the factor to map milliseconds of track to array # Calculate the factor to map milliseconds of track to array
self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms) self.ms_to_array_factor = len(self.graph_array) / (self.end_ms - self.start_ms)
@ -178,14 +181,11 @@ class _FadeCurve:
# Update region position # Update region position
if self.region: if self.region:
# Next line is very noisy log.debug("issue223: _FadeCurve: update region")
# log.debug("issue223: _FadeCurve: update region")
self.region.setRegion([0, ms_of_graph * self.ms_to_array_factor]) self.region.setRegion([0, ms_of_graph * self.ms_to_array_factor])
class _FadeTrack(QThread): class _FadeTrack(QRunnable):
finished = pyqtSignal()
def __init__(self, player: vlc.MediaPlayer, fade_seconds: int) -> None: def __init__(self, player: vlc.MediaPlayer, fade_seconds: int) -> None:
super().__init__() super().__init__()
self.player = player self.player = player
@ -201,22 +201,18 @@ class _FadeTrack(QThread):
# Reduce volume logarithmically # Reduce volume logarithmically
total_steps = self.fade_seconds * Config.FADEOUT_STEPS_PER_SECOND total_steps = self.fade_seconds * Config.FADEOUT_STEPS_PER_SECOND
if total_steps > 0: db_reduction_per_step = Config.FADEOUT_DB / total_steps
db_reduction_per_step = Config.FADEOUT_DB / total_steps reduction_factor_per_step = pow(10, (db_reduction_per_step / 20))
reduction_factor_per_step = pow(10, (db_reduction_per_step / 20))
volume = self.player.audio_get_volume() volume = self.player.audio_get_volume()
for i in range(1, total_steps + 1): for i in range(1, total_steps + 1):
self.player.audio_set_volume( self.player.audio_set_volume(
int(volume * pow(reduction_factor_per_step, i)) int(volume * pow(reduction_factor_per_step, i))
) )
sleep(1 / Config.FADEOUT_STEPS_PER_SECOND) sleep(1 / Config.FADEOUT_STEPS_PER_SECOND)
self.finished.emit() self.player.stop()
vlc_instance = VLCManager().vlc_instance
class _Music: class _Music:
@ -225,37 +221,38 @@ class _Music:
""" """
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
vlc_instance.set_user_agent(name, name) self.VLC = vlc.Instance()
self.VLC.set_user_agent(name, name)
self.player: Optional[vlc.MediaPlayer] = None self.player: Optional[vlc.MediaPlayer] = None
self.name = name self.name = name
self.max_volume: int = Config.VLC_VOLUME_DEFAULT self.max_volume: int = Config.VLC_VOLUME_DEFAULT
self.start_dt: Optional[dt.datetime] = None self.start_dt: Optional[dt.datetime] = None
self.player_count: int = 0
# Set up logging # Set up logging
# self._set_vlc_log() self._set_vlc_log()
# VLC logging very noisy so comment out unless needed @VLC_LOG_CB
# @VLC_LOG_CB def log_callback(data, level, ctx, fmt, args):
# def log_callback(data, level, ctx, fmt, args): try:
# try: # Create a ctypes string buffer to hold the formatted message
# # Create a ctypes string buffer to hold the formatted message buf = ctypes.create_string_buffer(1024)
# buf = ctypes.create_string_buffer(1024)
# # Use vsnprintf to format the string with the va_list # Use vsnprintf to format the string with the va_list
# libc.vsnprintf(buf, len(buf), fmt, args) libc.vsnprintf(buf, len(buf), fmt, args)
# # Decode the formatted message # Decode the formatted message
# message = buf.value.decode("utf-8", errors="replace") message = buf.value.decode("utf-8", errors="replace")
# log.debug("VLC: " + message) log.debug("VLC: " + message)
# except Exception as e: except Exception as e:
# log.error(f"Error in VLC log callback: {e}") log.error(f"Error in VLC log callback: {e}")
# def _set_vlc_log(self): def _set_vlc_log(self):
# try: try:
# vlc.libvlc_log_set(vlc_instance, self.log_callback, None) vlc.libvlc_log_set(self.VLC, self.log_callback, None)
# log.debug("VLC logging set up successfully") log.debug("VLC logging set up successfully")
# except Exception as e: except Exception as e:
# log.error(f"Failed to set up VLC logging: {e}") log.error(f"Failed to set up VLC logging: {e}")
def adjust_by_ms(self, ms: int) -> None: def adjust_by_ms(self, ms: int) -> None:
"""Move player position by ms milliseconds""" """Move player position by ms milliseconds"""
@ -278,6 +275,27 @@ class _Music:
else: else:
self.start_dt = dt.datetime.now() - dt.timedelta(milliseconds=ms) self.start_dt = dt.datetime.now() - 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()
self.player_count -= 1
log.debug(f"_Music.stop: Releasing player {p=}, {self.player_count=}")
p = None
def fade(self, fade_seconds: int) -> None: def fade(self, fade_seconds: int) -> None:
""" """
Fade the currently playing track. Fade the currently playing track.
@ -292,10 +310,23 @@ class _Music:
if not self.player.get_position() > 0 and self.player.is_playing(): if not self.player.get_position() > 0 and self.player.is_playing():
return return
self.fader_worker = _FadeTrack(self.player, fade_seconds=fade_seconds) if fade_seconds <= 0:
self.fader_worker.finished.connect(self.player.release) self.stop()
self.fader_worker.start() return
self.start_dt = None
# 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()
if pool:
fader = _FadeTrack(p, fade_seconds=fade_seconds)
pool.start(fader)
self.start_dt = None
else:
log.error("_Music: failed to allocate QThreadPool")
def get_playtime(self) -> int: def get_playtime(self) -> int:
""" """
@ -357,35 +388,38 @@ class _Music:
log.error(f"play({path}): path not readable") log.error(f"play({path}): path not readable")
return None return None
self.player = vlc.MediaPlayer(vlc_instance, path) media = self.VLC.media_new_path(path)
if self.player is None: if media is None:
log.error(f"_Music:play: failed to create MediaPlayer ({path=})") log.error(f"_Music:play: failed to create media ({path=})")
show_warning( show_warning(None, "Error loading file", f"Cannot play file ({path})")
None, "Error creating MediaPlayer", f"Cannot play file ({path})"
)
return return
self.player = media.player_new_from_media()
if self.player:
_ = self.player.play()
self.set_volume(self.max_volume)
self.player_count += 1
log.debug(f"_Music.play: {self.player_count=}")
_ = self.player.play() if position:
self.set_volume(self.max_volume) self.player.set_position(position)
self.start_dt = start_time
if position: # For as-yet unknown reasons. sometimes the volume gets
self.player.set_position(position) # reset to zero within 200mS or so of starting play. This
self.start_dt = start_time # only happened since moving to Debian 12, which uses
# Pipewire for sound (which may be irrelevant).
# For as-yet unknown reasons. sometimes the volume gets # It has been known for the volume to need correcting more
# reset to zero within 200mS or so of starting play. This # than once in the first 200mS.
# only happened since moving to Debian 12, which uses for _ in range(3):
# Pipewire for sound (which may be irrelevant). if self.player:
# It has been known for the volume to need correcting more volume = self.player.audio_get_volume()
# than once in the first 200mS. if volume < Config.VLC_VOLUME_DEFAULT:
# Update August 2024: This no longer seems to be an issue self.set_volume(Config.VLC_VOLUME_DEFAULT)
# for _ in range(3): log.error(f"Reset from {volume=}")
# if self.player: sleep(0.1)
# volume = self.player.audio_get_volume() else:
# if volume < Config.VLC_VOLUME_DEFAULT: log.error("_Music:play: failed to create media player")
# self.set_volume(Config.VLC_VOLUME_DEFAULT) show_warning(None, "Media player", "Unable to create media player")
# log.error(f"Reset from {volume=}")
# sleep(0.1)
def set_position(self, position: float) -> None: def set_position(self, position: float) -> None:
""" """
@ -422,21 +456,6 @@ class _Music:
log.debug(f"Reset from {volume=}") log.debug(f"Reset from {volume=}")
sleep(0.1) sleep(0.1)
def stop(self) -> None:
"""Immediately stop playing"""
log.debug(f"Music[{self.name}].stop()")
self.start_dt = None
if not self.player:
return
if self.player.is_playing():
self.player.stop()
self.player.release()
self.player = None
@singleton @singleton
@dataclass @dataclass
@ -495,9 +514,7 @@ class RowAndTrack:
self.fade_at = playlist_row.track.fade_at self.fade_at = playlist_row.track.fade_at
self.intro = playlist_row.track.intro self.intro = playlist_row.track.intro
if playlist_row.track.playdates: if playlist_row.track.playdates:
self.lastplayed = max( self.lastplayed = max([a.lastplayed for a in playlist_row.track.playdates])
[a.lastplayed for a in playlist_row.track.playdates]
)
else: else:
self.lastplayed = Config.EPOCH self.lastplayed = Config.EPOCH
self.path = playlist_row.track.path self.path = playlist_row.track.path
@ -527,7 +544,6 @@ class RowAndTrack:
self.start_time: Optional[dt.datetime] = None self.start_time: Optional[dt.datetime] = None
# Other object initialisation # Other object initialisation
self.music = _Music(name=Config.VLC_MAIN_PLAYER_NAME)
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
def __repr__(self) -> str: def __repr__(self) -> str:
@ -548,16 +564,12 @@ class RowAndTrack:
if self.end_of_track_signalled: if self.end_of_track_signalled:
return return
if self.music.is_playing(): if not self.player.is_playing():
return self.start_time = None
if self.fade_graph:
self.start_time = None self.fade_graph.clear()
if self.fade_graph: self.signal_end_of_track()
self.fade_graph.clear() self.end_of_track_signalled = True
# Ensure that player is released
self.music.fade(0)
self.signals.track_ended_signal.emit()
self.end_of_track_signalled = True
def create_fade_graph(self) -> None: def create_fade_graph(self) -> None:
""" """
@ -584,16 +596,16 @@ class RowAndTrack:
""" """
if enable: if enable:
self.music.set_volume(volume=Config.VLC_VOLUME_DROP3db, set_default=False) self.player.set_volume(volume=Config.VLC_VOLUME_DROP3db, set_default=False)
else: else:
self.music.set_volume(volume=Config.VLC_VOLUME_DEFAULT, set_default=False) self.player.set_volume(volume=Config.VLC_VOLUME_DEFAULT, set_default=False)
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None: def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
"""Fade music""" """Fade music"""
self.resume_marker = self.music.get_position() self.resume_marker = self.player.get_position()
self.music.fade(fade_seconds) self.player.fade(fade_seconds)
self.signals.track_ended_signal.emit() self.signal_end_of_track()
def is_playing(self) -> bool: def is_playing(self) -> bool:
""" """
@ -603,21 +615,21 @@ class RowAndTrack:
if self.start_time is None: if self.start_time is None:
return False return False
return self.music.is_playing() return self.player.is_playing()
def move_back(self, ms: int = Config.PREVIEW_BACK_MS) -> None: def move_back(self, ms: int = Config.PREVIEW_BACK_MS) -> None:
""" """
Rewind player by ms milliseconds Rewind player by ms milliseconds
""" """
self.music.adjust_by_ms(ms * -1) self.player.adjust_by_ms(ms * -1)
def move_forward(self, ms: int = Config.PREVIEW_ADVANCE_MS) -> None: def move_forward(self, ms: int = Config.PREVIEW_ADVANCE_MS) -> None:
""" """
Rewind player by ms milliseconds Rewind player by ms milliseconds
""" """
self.music.adjust_by_ms(ms) self.player.adjust_by_ms(ms)
def play(self, position: Optional[float] = None) -> None: def play(self, position: Optional[float] = None) -> None:
"""Play track""" """Play track"""
@ -627,7 +639,8 @@ class RowAndTrack:
self.start_time = now self.start_time = now
# Initialise player # Initialise player
self.music.play(self.path, start_time=now, position=position) self.player = _Music(name=Config.VLC_MAIN_PLAYER_NAME)
self.player.play(self.path, start_time=now, position=position)
self.end_time = now + dt.timedelta(milliseconds=self.duration) self.end_time = now + dt.timedelta(milliseconds=self.duration)
@ -645,7 +658,7 @@ class RowAndTrack:
Restart player Restart player
""" """
self.music.adjust_by_ms(self.time_playing() * -1) self.player.adjust_by_ms(self.time_playing() * -1)
def set_forecast_start_time( def set_forecast_start_time(
self, modified_rows: list[int], start: Optional[dt.datetime] self, modified_rows: list[int], start: Optional[dt.datetime]
@ -680,12 +693,19 @@ class RowAndTrack:
return new_start_time return new_start_time
def signal_end_of_track(self) -> None:
"""
Send end of track signal unless we are a preview player
"""
self.signals.track_ended_signal.emit()
def stop(self, fade_seconds: int = 0) -> None: def stop(self, fade_seconds: int = 0) -> None:
""" """
Stop this track playing Stop this track playing
""" """
self.resume_marker = self.music.get_position() self.resume_marker = self.player.get_position()
self.fade(fade_seconds) self.fade(fade_seconds)
# Reset fade graph # Reset fade graph
@ -700,7 +720,7 @@ class RowAndTrack:
if self.start_time is None: if self.start_time is None:
return 0 return 0
return self.music.get_playtime() return self.player.get_playtime()
def time_remaining_intro(self) -> int: def time_remaining_intro(self) -> int:
""" """

View File

@ -316,7 +316,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.action_quicklog.activated.connect(self.quicklog) self.action_quicklog.activated.connect(self.quicklog)
self.load_last_playlists() self.load_last_playlists()
self.stop_autoplay = False
def about(self) -> None: def about(self) -> None:
"""Get git tag and database name""" """Get git tag and database name"""
@ -660,13 +659,9 @@ class Window(QMainWindow, Ui_MainWindow):
- Enable controls - Enable controls
""" """
if track_sequence.current: # Reset track_sequence objects
# Dereference the fade curve so it can be garbage collected track_sequence.previous = track_sequence.current
track_sequence.current.fade_graph = None track_sequence.current = None
# Reset track_sequence objects
track_sequence.previous = track_sequence.current
track_sequence.current = None
# Tell model previous track has finished # Tell model previous track has finished
self.active_proxy_model().previous_track_ended() self.active_proxy_model().previous_track_ended()
@ -674,6 +669,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Reset clocks # Reset clocks
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
self.frame_silent.setStyleSheet("") self.frame_silent.setStyleSheet("")
self.label_elapsed_timer.setText("00:00 / 00:00")
self.label_fade_timer.setText("00:00") self.label_fade_timer.setText("00:00")
self.label_silent_timer.setText("00:00") self.label_silent_timer.setText("00:00")
@ -684,10 +680,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.catch_return_key = False self.catch_return_key = False
self.show_status_message("Play controls: Enabled", 0) self.show_status_message("Play controls: Enabled", 0)
# autoplay
# if not self.stop_autoplay:
# self.play_next()
def export_playlist_tab(self) -> None: def export_playlist_tab(self) -> None:
"""Export the current playlist to an m3u file""" """Export the current playlist to an m3u file"""
@ -1061,7 +1053,7 @@ class Window(QMainWindow, Ui_MainWindow):
if Config.USE_INTERNAL_BROWSER: if Config.USE_INTERNAL_BROWSER:
self.tabInfolist.open_tab(url, title) self.tabInfolist.open_tab(url, title)
else: else:
webbrowser.get("browser").open_new_tab(url) webbrowser.get('browser').open_new_tab(url)
def open_wikipedia_browser(self, title: str) -> None: def open_wikipedia_browser(self, title: str) -> None:
"""Search Wikipedia for title""" """Search Wikipedia for title"""
@ -1073,7 +1065,7 @@ class Window(QMainWindow, Ui_MainWindow):
if Config.USE_INTERNAL_BROWSER: if Config.USE_INTERNAL_BROWSER:
self.tabInfolist.open_tab(url, title) self.tabInfolist.open_tab(url, title)
else: else:
webbrowser.get("browser").open_new_tab(url) webbrowser.get('browser').open_new_tab(url)
def paste_rows(self) -> None: def paste_rows(self) -> None:
""" """
@ -1159,7 +1151,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnDrop3db.setChecked(False) self.btnDrop3db.setChecked(False)
# Play (new) current track # Play (new) current track
log.info(f"Play: {track_sequence.current.title}")
track_sequence.current.play(position) track_sequence.current.play(position)
# Update clocks now, don't wait for next tick # Update clocks now, don't wait for next tick
@ -1168,9 +1159,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Show closing volume graph # Show closing volume graph
if track_sequence.current.fade_graph: if track_sequence.current.fade_graph:
log.debug( log.debug(f"issue223: play_next: set up fade_graph, {track_sequence.current.title=}")
f"issue223: play_next: set up fade_graph, {track_sequence.current.title=}"
)
track_sequence.current.fade_graph.GraphWidget = self.widgetFadeVolume track_sequence.current.fade_graph.GraphWidget = self.widgetFadeVolume
track_sequence.current.fade_graph.clear() track_sequence.current.fade_graph.clear()
track_sequence.current.fade_graph.plot() track_sequence.current.fade_graph.plot()
@ -1211,7 +1200,7 @@ class Window(QMainWindow, Ui_MainWindow):
if not track_info: if not track_info:
# Otherwise get track_id to next track to play # Otherwise get track_id to next track to play
if track_sequence.next: if track_sequence.next:
if track_sequence.next.track_id: if track_sequence.next.path:
track_info = TrackInfo( track_info = TrackInfo(
track_sequence.next.track_id, track_sequence.next.row_number track_sequence.next.track_id, track_sequence.next.row_number
) )
@ -1423,8 +1412,7 @@ class Window(QMainWindow, Ui_MainWindow):
return return
# Return if no saved position # Return if no saved position
resume_marker = track_sequence.previous.resume_marker if not track_sequence.previous.resume_marker:
if not resume_marker:
log.error("No previous track position") log.error("No previous track position")
return return
@ -1433,7 +1421,7 @@ class Window(QMainWindow, Ui_MainWindow):
track_sequence.set_next(track_sequence.previous) track_sequence.set_next(track_sequence.previous)
# Now resume playing the now-next track # Now resume playing the now-next track
self.play_next(resume_marker) self.play_next(track_sequence.next.resume_marker)
# Adjust track info so that clocks and graph are correct. # Adjust track info so that clocks and graph are correct.
# We need to fake the start time to reflect where we resumed the # We need to fake the start time to reflect where we resumed the
@ -1635,7 +1623,6 @@ class Window(QMainWindow, Ui_MainWindow):
def stop(self) -> None: def stop(self) -> None:
"""Stop playing immediately""" """Stop playing immediately"""
self.stop_autoplay = True
if track_sequence.current: if track_sequence.current:
track_sequence.current.stop() track_sequence.current.stop()

View File

@ -561,7 +561,7 @@ class PlaylistModel(QAbstractTableModel):
for a in self.playlist_rows.values() for a in self.playlist_rows.values()
if not a.played and a.track_id is not None if not a.played and a.track_id is not None
] ]
# log.debug(f"{self}: get_unplayed_rows() returned: {result=}") log.debug(f"{self}: get_unplayed_rows() returned: {result=}")
return result return result
def headerData( def headerData(
@ -884,9 +884,7 @@ class PlaylistModel(QAbstractTableModel):
Move existing_rat track to new_row_number and append note to any existing note Move existing_rat track to new_row_number and append note to any existing note
""" """
log.debug( log.debug(f"{self}: move_track_add_note({new_row_number=}, {existing_rat=}, {note=}")
f"{self}: move_track_add_note({new_row_number=}, {existing_rat=}, {note=}"
)
if note: if note:
with db.Session() as session: with db.Session() as session:
@ -913,9 +911,7 @@ class PlaylistModel(QAbstractTableModel):
Add the existing_rat track details to the existing header at header_row_number Add the existing_rat track details to the existing header at header_row_number
""" """
log.debug( log.debug(f"{self}: move_track_to_header({header_row_number=}, {existing_rat=}, {note=}")
f"{self}: move_track_to_header({header_row_number=}, {existing_rat=}, {note=}"
)
if existing_rat.track_id: if existing_rat.track_id:
if note and existing_rat.note: if note and existing_rat.note:
@ -972,9 +968,7 @@ class PlaylistModel(QAbstractTableModel):
# Sanity check # Sanity check
if not track_sequence.previous: if not track_sequence.previous:
log.error( log.error(f"{self}: playlistmodel:previous_track_ended called with no current track")
f"{self}: playlistmodel:previous_track_ended called with no current track"
)
return return
if track_sequence.previous.row_number is None: if track_sequence.previous.row_number is None:
log.error( log.error(
@ -1249,9 +1243,7 @@ class PlaylistModel(QAbstractTableModel):
PlaylistRows, self.playlist_rows[row_number].playlistrow_id PlaylistRows, self.playlist_rows[row_number].playlistrow_id
) )
if not playlist_row: if not playlist_row:
log.error( log.error(f"{self}: Error saving data: {row_number=}, {column=}, {value=}")
f"{self}: Error saving data: {row_number=}, {column=}, {value=}"
)
return False return False
if playlist_row.track_id: if playlist_row.track_id:
@ -1302,9 +1294,7 @@ class PlaylistModel(QAbstractTableModel):
shortlist_rows = {k: self.playlist_rows[k] for k in row_numbers} shortlist_rows = {k: self.playlist_rows[k] for k in row_numbers}
sorted_list = [ sorted_list = [
playlist_row.row_number playlist_row.row_number
for playlist_row in sorted( for playlist_row in sorted(shortlist_rows.values(), key=attrgetter(attr_name))
shortlist_rows.values(), key=attrgetter(attr_name)
)
] ]
self.move_rows(sorted_list, min(sorted_list)) self.move_rows(sorted_list, min(sorted_list))

View File

@ -1,29 +0,0 @@
# Standard library imports
# PyQt imports
# Third party imports
import vlc # type: ignore
# App imports
class VLCManager:
"""
Singleton class to ensure we only ever have one vlc Instance
"""
__instance = None
def __init__(self) -> None:
if VLCManager.__instance is None:
self.vlc_instance = vlc.Instance()
VLCManager.__instance = self
else:
raise Exception("Attempted to create a second VLCManager instance")
@staticmethod
def get_instance() -> vlc.Instance:
if VLCManager.__instance is None:
VLCManager()
return VLCManager.__instance

104
poetry.lock generated
View File

@ -81,33 +81,33 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]] [[package]]
name = "black" name = "black"
version = "24.8.0" version = "24.4.2"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
{file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
{file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
{file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
{file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
{file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
{file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
{file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
{file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
{file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
{file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
{file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
{file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
{file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
{file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
{file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
{file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
{file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
{file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
{file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
{file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
{file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
] ]
[package.dependencies] [package.dependencies]
@ -864,43 +864,43 @@ files = [
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "1.11.1" version = "1.10.1"
description = "Optional static typing for Python" description = "Optional static typing for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"},
{file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"},
{file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"},
{file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"},
{file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"},
{file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"},
{file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"},
{file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"},
{file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"},
{file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"},
{file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"},
{file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"},
{file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"},
{file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"},
{file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"},
{file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"},
{file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"},
{file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"},
{file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"},
{file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"},
{file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"},
{file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"},
{file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"},
{file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"},
{file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"},
{file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"},
{file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"},
] ]
[package.dependencies] [package.dependencies]
mypy-extensions = ">=1.0.0" mypy-extensions = ">=1.0.0"
typing-extensions = ">=4.6.0" typing-extensions = ">=4.1.0"
[package.extras] [package.extras]
dmypy = ["psutil (>=4.0)"] dmypy = ["psutil (>=4.0)"]