159 lines
4.1 KiB
Python
159 lines
4.1 KiB
Python
# import os
|
|
import threading
|
|
import vlc
|
|
#
|
|
from config import Config
|
|
from datetime import datetime
|
|
from helpers import file_is_readable
|
|
from typing import Optional
|
|
from time import sleep
|
|
|
|
from log import log
|
|
|
|
lock = threading.Lock()
|
|
|
|
|
|
class Music:
|
|
"""
|
|
Manage the playing of music tracks
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
# self.current_track_start_time = None
|
|
# self.fading = 0
|
|
self.VLC = vlc.Instance()
|
|
self.player = None
|
|
# self.track_path = None
|
|
self.max_volume = Config.VOLUME_VLC_DEFAULT
|
|
|
|
def fade(self) -> 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
|
|
|
|
thread = threading.Thread(target=self._fade)
|
|
thread.start()
|
|
|
|
def _fade(self) -> None:
|
|
"""
|
|
Implementation of fading the current track in a separate thread.
|
|
"""
|
|
|
|
# Take a copy of current player to allow another track to be
|
|
# started without interfering here
|
|
with lock:
|
|
p = self.player
|
|
self.player = None
|
|
|
|
# Sanity check
|
|
if not p:
|
|
return
|
|
|
|
fade_time = Config.FADE_TIME / 1000
|
|
steps = Config.FADE_STEPS
|
|
sleep_time = fade_time / steps
|
|
|
|
# We reduce volume by one mesure first, then by two measures,
|
|
# then three, and so on.
|
|
# The sum of the arithmetic sequence 1, 2, 3, ..n is
|
|
# (n**2 + n) / 2
|
|
total_measures_count = (steps**2 + steps) / 2
|
|
|
|
measures_to_reduce_by = 0
|
|
for i in range(1, steps + 1):
|
|
measures_to_reduce_by += i
|
|
volume_factor = 1 - (
|
|
measures_to_reduce_by / total_measures_count)
|
|
p.audio_set_volume(int(self.max_volume * volume_factor))
|
|
sleep(sleep_time)
|
|
|
|
with lock:
|
|
p.stop()
|
|
log.debug(f"Releasing player {p=}")
|
|
p.release()
|
|
|
|
def get_playtime(self) -> Optional[int]:
|
|
"""Return elapsed play time"""
|
|
|
|
if not self.player:
|
|
return None
|
|
|
|
return self.player.get_time()
|
|
|
|
def get_position(self) -> Optional[float]:
|
|
"""Return current position"""
|
|
|
|
if not self.player:
|
|
return None
|
|
return self.player.get_position()
|
|
|
|
def play(self, path: str,
|
|
position: Optional[float] = None) -> Optional[int]:
|
|
"""
|
|
Start playing the track at path.
|
|
|
|
Log and return if path not found.
|
|
"""
|
|
|
|
if not file_is_readable(path):
|
|
log.error(f"play({path}): path not readable")
|
|
return None
|
|
|
|
status = -1
|
|
self.track_path = path
|
|
|
|
if Config.COLON_IN_PATH_FIX:
|
|
media = self.VLC.media_new_path(path)
|
|
self.player = media.player_new_from_media()
|
|
else:
|
|
self.player = self.VLC.media_player_new(path)
|
|
if self.player:
|
|
self.player.audio_set_volume(self.max_volume)
|
|
self.current_track_start_time = datetime.now()
|
|
status = self.player.play()
|
|
if position:
|
|
self.player.set_position(position)
|
|
|
|
return status
|
|
|
|
#
|
|
# def set_position(self, ms):
|
|
# """Set current play time in milliseconds from start"""
|
|
#
|
|
# with lock:
|
|
# return self.player.set_time(ms)
|
|
|
|
def set_volume(self, volume, set_default=True):
|
|
"""Set maximum volume used for player"""
|
|
|
|
if not self.player:
|
|
return
|
|
|
|
if set_default:
|
|
self.max_volume = volume
|
|
|
|
self.player.audio_set_volume(volume)
|
|
|
|
def stop(self) -> float:
|
|
"""Immediately stop playing"""
|
|
|
|
with lock:
|
|
if not self.player:
|
|
return 0.0
|
|
|
|
position = self.player.get_position()
|
|
self.player.stop()
|
|
self.player.release()
|
|
# Ensure we don't reference player after release
|
|
self.player = None
|
|
return position
|