Compare commits

..

5 Commits

Author SHA1 Message Date
Keith Edmunds
b8fcc79f8e Black formatting and mypy fixups 2024-08-04 17:18:08 +01:00
Keith Edmunds
27012a9658 Fix non-release of player when at natural end of track 2024-08-04 11:57:46 +01:00
Keith Edmunds
1d5fe3e57e Update black 2024-08-04 11:51:53 +01:00
Keith Edmunds
40cad1c98f Fix resource leak
After around 1.5h of operation, we'd get messages such as:

vlcpulse audio output error: PulseAudio server connection failure: Connection terminated

Tracked down to not correctly releasing vlc player resources when
track had finished playing. Fixed now, and much simplified the fadeout
code as well.
2024-08-02 18:35:33 +01:00
Keith Edmunds
5f5bb27a5f . 2024-08-02 18:35:33 +01:00
5 changed files with 242 additions and 210 deletions

View File

@ -6,7 +6,6 @@ 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
@ -19,9 +18,7 @@ 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
@ -37,18 +34,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_LOG_CB = ctypes.CFUNCTYPE( # VLC logging is very noisy so comment out unless needed
None, # VLC_LOG_CB = ctypes.CFUNCTYPE(
ctypes.c_void_p, # None,
ctypes.c_int, # ctypes.c_void_p,
ctypes.c_void_p, # ctypes.c_int,
ctypes.c_char_p, # ctypes.c_void_p,
ctypes.c_void_p, # ctypes.c_char_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":
@ -138,8 +135,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
self.audio_segment = audio[self.start_ms : self.end_ms] audio_segment = audio[self.start_ms : self.end_ms]
self.graph_array = np.array(self.audio_segment.get_array_of_samples()) self.graph_array = np.array(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)
@ -181,11 +178,14 @@ class _FadeCurve:
# Update region position # Update region position
if self.region: if self.region:
log.debug("issue223: _FadeCurve: update region") # Next line is very noisy
# 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(QRunnable): class _FadeTrack(QThread):
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,6 +201,7 @@ class _FadeTrack(QRunnable):
# 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))
@ -212,7 +213,10 @@ class _FadeTrack(QRunnable):
) )
sleep(1 / Config.FADEOUT_STEPS_PER_SECOND) sleep(1 / Config.FADEOUT_STEPS_PER_SECOND)
self.player.stop() self.finished.emit()
vlc_instance = VLCManager().vlc_instance
class _Music: class _Music:
@ -221,38 +225,37 @@ class _Music:
""" """
def __init__(self, name: str) -> None: def __init__(self, name: str) -> None:
self.VLC = vlc.Instance() vlc_instance.set_user_agent(name, name)
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_LOG_CB # VLC logging very noisy so comment out unless needed
def log_callback(data, level, ctx, fmt, args): # @VLC_LOG_CB
try: # def log_callback(data, level, ctx, fmt, args):
# Create a ctypes string buffer to hold the formatted message # try:
buf = ctypes.create_string_buffer(1024) # # Create a ctypes string buffer to hold the formatted message
# 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(self.VLC, self.log_callback, None) # vlc.libvlc_log_set(vlc_instance, 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"""
@ -275,27 +278,6 @@ 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.
@ -310,23 +292,10 @@ 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
if fade_seconds <= 0: self.fader_worker = _FadeTrack(self.player, fade_seconds=fade_seconds)
self.stop() self.fader_worker.finished.connect(self.player.release)
return self.fader_worker.start()
# 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 self.start_dt = None
else:
log.error("_Music: failed to allocate QThreadPool")
def get_playtime(self) -> int: def get_playtime(self) -> int:
""" """
@ -388,17 +357,16 @@ class _Music:
log.error(f"play({path}): path not readable") log.error(f"play({path}): path not readable")
return None return None
media = self.VLC.media_new_path(path) self.player = vlc.MediaPlayer(vlc_instance, path)
if media is None: if self.player is None:
log.error(f"_Music:play: failed to create media ({path=})") log.error(f"_Music:play: failed to create MediaPlayer ({path=})")
show_warning(None, "Error loading file", f"Cannot play file ({path})") show_warning(
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.player.play()
self.set_volume(self.max_volume) self.set_volume(self.max_volume)
self.player_count += 1
log.debug(f"_Music.play: {self.player_count=}")
if position: if position:
self.player.set_position(position) self.player.set_position(position)
@ -410,16 +378,14 @@ class _Music:
# Pipewire for sound (which may be irrelevant). # Pipewire for sound (which may be irrelevant).
# It has been known for the volume to need correcting more # It has been known for the volume to need correcting more
# than once in the first 200mS. # than once in the first 200mS.
for _ in range(3): # Update August 2024: This no longer seems to be an issue
if self.player: # for _ in range(3):
volume = self.player.audio_get_volume() # if self.player:
if volume < Config.VLC_VOLUME_DEFAULT: # volume = self.player.audio_get_volume()
self.set_volume(Config.VLC_VOLUME_DEFAULT) # if volume < Config.VLC_VOLUME_DEFAULT:
log.error(f"Reset from {volume=}") # self.set_volume(Config.VLC_VOLUME_DEFAULT)
sleep(0.1) # log.error(f"Reset from {volume=}")
else: # sleep(0.1)
log.error("_Music:play: failed to create media player")
show_warning(None, "Media player", "Unable to create media player")
def set_position(self, position: float) -> None: def set_position(self, position: float) -> None:
""" """
@ -456,6 +422,21 @@ 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
@ -514,7 +495,9 @@ 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([a.lastplayed for a in playlist_row.track.playdates]) self.lastplayed = max(
[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
@ -544,6 +527,7 @@ 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:
@ -564,11 +548,15 @@ class RowAndTrack:
if self.end_of_track_signalled: if self.end_of_track_signalled:
return return
if not self.player.is_playing(): if self.music.is_playing():
return
self.start_time = None self.start_time = None
if self.fade_graph: if self.fade_graph:
self.fade_graph.clear() self.fade_graph.clear()
self.signal_end_of_track() # Ensure that player is released
self.music.fade(0)
self.signals.track_ended_signal.emit()
self.end_of_track_signalled = True self.end_of_track_signalled = True
def create_fade_graph(self) -> None: def create_fade_graph(self) -> None:
@ -596,16 +584,16 @@ class RowAndTrack:
""" """
if enable: if enable:
self.player.set_volume(volume=Config.VLC_VOLUME_DROP3db, set_default=False) self.music.set_volume(volume=Config.VLC_VOLUME_DROP3db, set_default=False)
else: else:
self.player.set_volume(volume=Config.VLC_VOLUME_DEFAULT, set_default=False) self.music.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.player.get_position() self.resume_marker = self.music.get_position()
self.player.fade(fade_seconds) self.music.fade(fade_seconds)
self.signal_end_of_track() self.signals.track_ended_signal.emit()
def is_playing(self) -> bool: def is_playing(self) -> bool:
""" """
@ -615,21 +603,21 @@ class RowAndTrack:
if self.start_time is None: if self.start_time is None:
return False return False
return self.player.is_playing() return self.music.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.player.adjust_by_ms(ms * -1) self.music.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.player.adjust_by_ms(ms) self.music.adjust_by_ms(ms)
def play(self, position: Optional[float] = None) -> None: def play(self, position: Optional[float] = None) -> None:
"""Play track""" """Play track"""
@ -639,8 +627,7 @@ class RowAndTrack:
self.start_time = now self.start_time = now
# Initialise player # Initialise player
self.player = _Music(name=Config.VLC_MAIN_PLAYER_NAME) self.music.play(self.path, start_time=now, position=position)
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)
@ -658,7 +645,7 @@ class RowAndTrack:
Restart player Restart player
""" """
self.player.adjust_by_ms(self.time_playing() * -1) self.music.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]
@ -693,19 +680,12 @@ 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.player.get_position() self.resume_marker = self.music.get_position()
self.fade(fade_seconds) self.fade(fade_seconds)
# Reset fade graph # Reset fade graph
@ -720,7 +700,7 @@ class RowAndTrack:
if self.start_time is None: if self.start_time is None:
return 0 return 0
return self.player.get_playtime() return self.music.get_playtime()
def time_remaining_intro(self) -> int: def time_remaining_intro(self) -> int:
""" """

View File

@ -316,6 +316,7 @@ 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"""
@ -659,6 +660,10 @@ class Window(QMainWindow, Ui_MainWindow):
- Enable controls - Enable controls
""" """
if track_sequence.current:
# Dereference the fade curve so it can be garbage collected
track_sequence.current.fade_graph = None
# Reset track_sequence objects # Reset track_sequence objects
track_sequence.previous = track_sequence.current track_sequence.previous = track_sequence.current
track_sequence.current = None track_sequence.current = None
@ -669,7 +674,6 @@ 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")
@ -680,6 +684,10 @@ 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"""
@ -1053,7 +1061,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"""
@ -1065,7 +1073,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:
""" """
@ -1151,6 +1159,7 @@ 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
@ -1159,7 +1168,9 @@ 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(f"issue223: play_next: set up fade_graph, {track_sequence.current.title=}") log.debug(
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()
@ -1200,7 +1211,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.path: if track_sequence.next.track_id:
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
) )
@ -1412,7 +1423,8 @@ class Window(QMainWindow, Ui_MainWindow):
return return
# Return if no saved position # Return if no saved position
if not track_sequence.previous.resume_marker: resume_marker = track_sequence.previous.resume_marker
if not resume_marker:
log.error("No previous track position") log.error("No previous track position")
return return
@ -1421,7 +1433,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(track_sequence.next.resume_marker) self.play_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
@ -1623,6 +1635,7 @@ 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,7 +884,9 @@ 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(f"{self}: move_track_add_note({new_row_number=}, {existing_rat=}, {note=}") log.debug(
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:
@ -911,7 +913,9 @@ 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(f"{self}: move_track_to_header({header_row_number=}, {existing_rat=}, {note=}") log.debug(
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:
@ -968,7 +972,9 @@ class PlaylistModel(QAbstractTableModel):
# Sanity check # Sanity check
if not track_sequence.previous: if not track_sequence.previous:
log.error(f"{self}: playlistmodel:previous_track_ended called with no current track") log.error(
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(
@ -1243,7 +1249,9 @@ 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(f"{self}: Error saving data: {row_number=}, {column=}, {value=}") log.error(
f"{self}: Error saving data: {row_number=}, {column=}, {value=}"
)
return False return False
if playlist_row.track_id: if playlist_row.track_id:
@ -1294,7 +1302,9 @@ 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(shortlist_rows.values(), key=attrgetter(attr_name)) for playlist_row in sorted(
shortlist_rows.values(), key=attrgetter(attr_name)
)
] ]
self.move_rows(sorted_list, min(sorted_list)) self.move_rows(sorted_list, min(sorted_list))

29
app/vlcmanager.py Normal file
View File

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