-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -100,6 +100,7 @@ | |
from jukebox.NvManager import nv_manager | ||
from .playcontentcallback import PlayContentCallbacks, PlayCardState | ||
from .coverart_cache_manager import CoverartCacheManager | ||
from .play_position_tracker import PlayPositionTracker | ||
|
||
logger = logging.getLogger('jb.PlayerMPD') | ||
cfg = jukebox.cfghandler.get_handler('jukebox') | ||
|
@@ -193,6 +194,9 @@ def __init__(self): | |
# Change this to last_played_folder and shutdown_state (for restoring) | ||
self.music_player_status['player_status']['last_played_folder'] = '' | ||
|
||
play_position_tracker_file = cfg.getn('playermpd', 'play_position_tracker_file', default='../../shared/settings/play_positions.json') | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
hoffie
Author
Owner
|
||
self.play_position_tracker = PlayPositionTracker(path=play_position_tracker_file) | ||
|
||
self.old_song = None | ||
self.mpd_status = {} | ||
self.mpd_status_poll_interval = 0.25 | ||
|
@@ -270,6 +274,7 @@ def _mpd_status_poll(self): | |
self.current_folder_status["LOOP"] = "OFF" | ||
self.current_folder_status["SINGLE"] = "OFF" | ||
|
||
self.play_position_tracker.handle_mpd_status(self.mpd_status) | ||
# Delete the volume key to avoid confusion | ||
# Volume is published via the 'volume' component! | ||
try: | ||
|
@@ -308,11 +313,13 @@ def update_wait(self): | |
def play(self): | ||
with self.mpd_lock: | ||
self.mpd_client.play() | ||
self.play_position_tracker.flush() | ||
|
||
@plugs.tag | ||
def stop(self): | ||
with self.mpd_lock: | ||
self.mpd_client.stop() | ||
self.play_position_tracker.flush() | ||
|
||
@plugs.tag | ||
def pause(self, state: int = 1): | ||
|
@@ -323,24 +330,28 @@ def pause(self, state: int = 1): | |
""" | ||
with self.mpd_lock: | ||
self.mpd_client.pause(state) | ||
self.play_position_tracker.flush() | ||
|
||
@plugs.tag | ||
def prev(self): | ||
logger.debug("Prev") | ||
with self.mpd_lock: | ||
self.mpd_client.previous() | ||
self.play_position_tracker.flush() | ||
|
||
@plugs.tag | ||
def next(self): | ||
"""Play next track in current playlist""" | ||
logger.debug("Next") | ||
with self.mpd_lock: | ||
self.mpd_client.next() | ||
self.play_position_tracker.flush() | ||
|
||
@plugs.tag | ||
def seek(self, new_time): | ||
with self.mpd_lock: | ||
self.mpd_client.seekcur(new_time) | ||
self.play_position_tracker.flush() | ||
|
||
@plugs.tag | ||
def rewind(self): | ||
|
@@ -351,6 +362,7 @@ def rewind(self): | |
logger.debug("Rewind") | ||
with self.mpd_lock: | ||
self.mpd_client.play(1) | ||
self.play_position_tracker.flush() | ||
|
||
@plugs.tag | ||
def replay(self): | ||
|
@@ -367,6 +379,7 @@ def toggle(self): | |
"""Toggle pause state, i.e. do a pause / resume depending on current state""" | ||
with self.mpd_lock: | ||
self.mpd_client.pause() | ||
self.play_position_tracker.flush() | ||
|
||
@plugs.tag | ||
def replay_if_stopped(self): | ||
|
@@ -466,11 +479,33 @@ def move(self): | |
|
||
@plugs.tag | ||
def play_single(self, song_url): | ||
play_target = ('single', song_url) | ||
with self.mpd_lock: | ||
if self._play_or_pause_current(play_target): | ||
return | ||
self.mpd_client.clear() | ||
self.mpd_client.addid(song_url) | ||
self._mpd_restore_saved_position(play_target) | ||
self.mpd_client.play() | ||
|
||
def _play_or_pause_current(self, play_target): | ||
if self.play_position_tracker.is_current_play_target(play_target): | ||
if self.mpd_status['state'] == 'play': | ||
# Do nothing | ||
return True | ||
if self.mpd_status['state'] == 'pause': | ||
logger.debug('Unpausing as the play target is identical') | ||
self.mpd_client.play() | ||
return True | ||
return False | ||
|
||
def _mpd_restore_saved_position(self, play_target): | ||
logger.debug(f'Restoring saved position for {play_target}') | ||
playlist_position = self.play_position_tracker.get_playlist_position_by_play_target(play_target) or 0 | ||
seek_position = self.play_position_tracker.get_seek_position_by_play_target(play_target) or 0 | ||
self.play_position_tracker.set_current_play_target(play_target) | ||
self.mpd_client.seek(playlist_position, seek_position) | ||
|
||
@plugs.tag | ||
def resume(self): | ||
with self.mpd_lock: | ||
|
@@ -482,11 +517,14 @@ def resume(self): | |
@plugs.tag | ||
def play_card(self, folder: str, recursive: bool = False): | ||
""" | ||
Main entry point for trigger music playing from RFID reader. Decodes second swipe options before playing folder content | ||
Deprecated (?) main entry point for trigger music playing from RFID reader. | ||
This comment has been minimized.
Sorry, something went wrong.
pabera
|
||
Decodes second swipe options before playing folder content | ||
Checks for second (or multiple) trigger of the same folder and calls first swipe / second swipe action | ||
accordingly. | ||
Note: The Web UI currently uses play_single/album/folder directly. | ||
:param folder: Folder path relative to music library path | ||
:param recursive: Add folder recursively | ||
""" | ||
|
@@ -587,8 +625,11 @@ def play_folder(self, folder: str, recursive: bool = False) -> None: | |
:param recursive: Add folder recursively | ||
""" | ||
# TODO: This changes the current state -> Need to save last state | ||
play_target = ('folder', folder, recursive) | ||
with self.mpd_lock: | ||
logger.info(f"Play folder: '{folder}'") | ||
if self._play_or_pause_current(play_target): | ||
return | ||
self.mpd_client.clear() | ||
|
||
plc = playlistgenerator.PlaylistCollector(components.player.get_music_library_path()) | ||
|
@@ -608,6 +649,7 @@ def play_folder(self, folder: str, recursive: bool = False) -> None: | |
if self.current_folder_status is None: | ||
self.current_folder_status = self.music_player_status['audio_folder_status'][folder] = {} | ||
|
||
self._mpd_restore_saved_position(play_target) | ||
self.mpd_client.play() | ||
|
||
@plugs.tag | ||
|
@@ -621,10 +663,14 @@ def play_album(self, albumartist: str, album: str): | |
:param albumartist: Artist of the Album provided by MPD database | ||
:param album: Album name provided by MPD database | ||
""" | ||
play_target = ('album', albumartist, album) | ||
with self.mpd_lock: | ||
logger.info(f"Play album: '{album}' by '{albumartist}") | ||
if self._play_or_pause_current(play_target): | ||
return | ||
self.mpd_client.clear() | ||
self.mpd_retry_with_mutex(self.mpd_client.findadd, 'albumartist', albumartist, 'album', album) | ||
self._mpd_restore_saved_position(play_target) | ||
self.mpd_client.play() | ||
|
||
@plugs.tag | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
""" | ||
Keeps track of playlist and in-song position for played single tracks, | ||
albums or folders. | ||
Syncs to disk every FLUSH_INTERVAL seconds. | ||
Provides methods to retrieve the stored values to resume playing. | ||
""" | ||
import time | ||
import os | ||
import logging | ||
import threading | ||
import json | ||
|
||
|
||
NO_SEEK_IF_NEAR_START_END_CUTOFF = 5 | ||
FLUSH_INTERVAL = 30 | ||
This comment has been minimized.
Sorry, something went wrong.
pabera
|
||
|
||
logger = logging.getLogger('jb.PlayerMPD.PlayPositionTracker') | ||
|
||
|
||
def play_target_to_key(play_target): | ||
return '|'.join([str(x) for x in play_target]) | ||
|
||
|
||
class PlayPositionTracker: | ||
flush_interval = 30 | ||
_last_flush_timestamp = 0 | ||
|
||
def __init__(self, path): | ||
self._lock = threading.RLock() | ||
self._path = path | ||
self._tmp_path = path + '.tmp' | ||
self._current_play_target = None | ||
with self._lock: | ||
self._load() | ||
|
||
def _load(self): | ||
logger.debug(f'Loading from {self._path}') | ||
try: | ||
with open(self._path) as f: | ||
d = json.load(f) | ||
except FileNotFoundError: | ||
logger.debug('File not found, assuming empty list') | ||
self._play_targets = {} | ||
self.flush() | ||
return | ||
self._play_targets = d['positions_by_play_target'] | ||
logger.debug(f'Loaded {len(self._play_targets.keys())} saved target play positions') | ||
|
||
def set_current_play_target(self, play_target): | ||
with self._lock: | ||
self._current_play_target = play_target_to_key(play_target) | ||
|
||
def is_current_play_target(self, play_target): | ||
return self._current_play_target == play_target | ||
|
||
def get_playlist_position_by_play_target(self, play_target): | ||
return self._play_targets.get(play_target_to_key(play_target), {}).get('playlist_position') | ||
|
||
def get_seek_position_by_play_target(self, play_target): | ||
return self._play_targets.get(play_target_to_key(play_target), {}).get('seek_position') | ||
|
||
def handle_mpd_status(self, status): | ||
if not self._current_play_target: | ||
return | ||
playlist_position = status.get('song') | ||
if playlist_position is not None: | ||
with self._lock: | ||
if self._current_play_target not in self._play_targets: | ||
self._play_targets[self._current_play_target] = {} | ||
self._play_targets[self._current_play_target]['playlist_position'] = playlist_position | ||
elapsed = status.get('elapsed') | ||
duration = status.get('duration') | ||
if elapsed is not None: | ||
elapsed = float(elapsed) | ||
duration = float(duration) | ||
if (elapsed < NO_SEEK_IF_NEAR_START_END_CUTOFF or | ||
((duration - elapsed) < NO_SEEK_IF_NEAR_START_END_CUTOFF)): | ||
# restart song next time: | ||
elapsed = 0 | ||
with self._lock: | ||
if self._current_play_target not in self._play_targets: | ||
self._play_targets[self._current_play_target] = {} | ||
self._play_targets[self._current_play_target]['seek_position'] = elapsed | ||
self._flush_if_necessary() | ||
|
||
def _flush_if_necessary(self): | ||
now = time.time() | ||
if self._last_flush_timestamp + FLUSH_INTERVAL < now: | ||
return self.flush() | ||
with self._lock: | ||
self._dirty = True | ||
|
||
def flush(self): | ||
This comment has been minimized.
Sorry, something went wrong.
pabera
|
||
with self._lock: | ||
self._dirty = False | ||
self._last_flush_timestamp = time.time() | ||
with open(self._tmp_path, 'w') as f: | ||
json.dump({ | ||
'positions_by_play_target': self._play_targets, | ||
}, f, indent=2, sort_keys=True) | ||
os.rename(self._tmp_path, self._path) | ||
logger.debug(f'Flushed state to {self._path}') | ||
|
||
def __del__(self): | ||
self.flush() |
I like the fact your outsourced the entire logic to a different class. I do believe we should keep all relevant logic within this class, including this definition. (We have solved the problem similarly for CoverartCacheManager)