Merge branch 'serve-local-files'

This commit is contained in:
Sumner Evans
2019-08-03 16:53:15 -06:00
4 changed files with 147 additions and 23 deletions

View File

@@ -146,6 +146,35 @@ class LibremsonicApp(Gtk.Application):
# it exists.
self.state.load()
self.last_play_queue_update = 0
def time_observer(value):
self.state.song_progress = value
GLib.idle_add(
self.window.player_controls.update_scrubber,
self.state.song_progress,
self.state.current_song.duration,
)
if not value:
self.last_play_queue_update = 0
elif self.last_play_queue_update + 15 <= value:
self.save_play_queue()
def on_track_end():
GLib.idle_add(self.on_next_track)
self.mpv_player = MPVPlayer(
time_observer,
on_track_end,
self.state.config,
)
self.chromecast_player = ChromecastPlayer(
time_observer,
on_track_end,
self.state.config,
)
self.player = self.mpv_player
# If there is no current server, show the dialog to select a server.
if (self.state.config.current_server is None
or self.state.config.current_server < 0):
@@ -181,6 +210,7 @@ class LibremsonicApp(Gtk.Application):
self.play_song(self.state.play_queue[current_idx + 1], reset=True)
def on_prev_track(self, action, params):
# TODO there is a bug where you can't go back multiple songs fast
current_idx = self.state.play_queue.index(self.state.current_song.id)
# Go back to the beginning of the song if we are past 5 seconds.
# Otherwise, go to the previous song.
@@ -297,6 +327,7 @@ class LibremsonicApp(Gtk.Application):
return True
def on_app_shutdown(self, app):
CacheManager.should_exit = True
self.player.pause()
self.chromecast_player.shutdown()
self.mpv_player.shutdown()
@@ -320,6 +351,9 @@ class LibremsonicApp(Gtk.Application):
def update_play_state_from_server(self):
# TODO make this non-blocking eventually (need to make everything in
# loading state)
self.player.pause()
self.state.playing = False
play_queue = CacheManager.get_play_queue()
self.state.play_queue = [s.id for s in play_queue.entry]
self.state.song_progress = play_queue.position / 1000
@@ -327,6 +361,8 @@ class LibremsonicApp(Gtk.Application):
current_song_idx = self.state.play_queue.index(str(play_queue.current))
self.state.current_song = play_queue.entry[current_song_idx]
self.player.reset()
self.update_window()
def play_song(
@@ -339,10 +375,10 @@ class LibremsonicApp(Gtk.Application):
# Do this the old fashioned way so that we can have access to ``reset``
# in the callback.
def do_play_song(song: Child):
# TODO force stream for now and force mp3 while getting chromecast working.
# TODO force mp3 while getting chromecast working.
uri, stream = CacheManager.get_song_filename_or_stream(
song,
force_stream=True,
force_stream=(self.player == self.chromecast_player),
format='mp3',
)
@@ -353,6 +389,7 @@ class LibremsonicApp(Gtk.Application):
# Prevent it from doing the thing where it continually loads
# songs when it has to download.
if reset:
self.player.reset()
self.state.song_progress = 0
# If streaming, also download the song.

View File

@@ -4,7 +4,8 @@ import threading
import shutil
import json
import hashlib
from collections import defaultdict, namedtuple
from collections import defaultdict
from time import sleep
from concurrent.futures import ThreadPoolExecutor, Future
from enum import EnumMeta, Enum
@@ -68,6 +69,7 @@ class SongCacheStatus(Enum):
class CacheManager(metaclass=Singleton):
executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=50)
should_exit: bool = False
class CacheEncoder(json.JSONEncoder):
def default(self, obj):
@@ -94,7 +96,6 @@ class CacheManager(metaclass=Singleton):
server: Server
browse_by_tags: bool
download_move_lock = threading.Lock()
download_set_lock = threading.Lock()
current_downloads: Set[Path] = set()
@@ -201,25 +202,39 @@ class CacheManager(metaclass=Singleton):
abs_path = self.calculate_abs_path(relative_path)
download_path = self.calculate_download_path(relative_path)
if not abs_path.exists() or force:
print(abs_path, 'not found. Downloading...')
resource_downloading = False
with self.download_set_lock:
if abs_path in self.current_downloads:
resource_downloading = True
self.current_downloads.add(abs_path)
os.makedirs(download_path.parent, exist_ok=True)
before_download()
self.save_file(download_path, download_fn())
if resource_downloading:
print(abs_path, 'already being downloaded.')
# The resource is already being downloaded. Busy loop until
# it has completed. Then, just return the path to the
# resource.
while abs_path in self.current_downloads:
sleep(0.5)
# Move the file to its cache download location. We need a lock
# here just in case we fired two downloads of the same asset
# for some reason.
with self.download_move_lock:
return str(abs_path)
else:
print(abs_path, 'not found. Downloading...')
os.makedirs(download_path.parent, exist_ok=True)
before_download()
self.save_file(download_path, download_fn())
# Move the file to its cache download location.
os.makedirs(abs_path.parent, exist_ok=True)
if download_path.exists():
shutil.move(download_path, abs_path)
with self.download_set_lock:
self.current_downloads.discard(abs_path)
with self.download_set_lock:
self.current_downloads.discard(abs_path)
print(abs_path, 'downloaded. Returning.')
return str(abs_path)
@@ -447,6 +462,9 @@ class CacheManager(metaclass=Singleton):
# TODO handle application close somehow. I think we will need to
# raise some sort of an exception, not sure.
def do_download_song(song_id):
if CacheManager.should_exit:
return
# Do the actual download.
song_details_future = CacheManager.get_song_details(song_id)
song = song_details_future.result()

View File

@@ -80,7 +80,7 @@ class Server:
def _get(self, url, **params):
params = {**self._get_params(), **params}
print(f'[START] post: {url}')
print(f'[START] get: {url}')
# Deal with datetime parameters (convert to milliseconds since 1970)
for k, v in params.items():
@@ -92,7 +92,7 @@ class Server:
if result.status_code != 200:
raise Exception(f'Fail! {result.status_code}')
print(f'[FINISH] post: {url}')
print(f'[FINISH] get: {url}')
return result
def _get_json(
@@ -101,8 +101,8 @@ class Server:
**params: Union[None, str, datetime, int, List[int]],
) -> Response:
"""
Make a post to a *Sonic REST API. Handle all types of errors including
*Sonic ``<error>`` responses.
Make a get request to a *Sonic REST API. Handle all types of errors
including *Sonic ``<error>`` responses.
:returns: a Response containing all of the data of the
response, deserialized

View File

@@ -1,3 +1,7 @@
import threading
from urllib.parse import urlparse, quote
import socket
from http.server import HTTPServer, SimpleHTTPRequestHandler
from typing import Callable, List, Any
from time import sleep
from concurrent.futures import ThreadPoolExecutor, Future
@@ -5,6 +9,7 @@ from concurrent.futures import ThreadPoolExecutor, Future
import pychromecast
import mpv
from libremsonic.config import AppConfiguration
from libremsonic.cache_manager import CacheManager
from libremsonic.server.api_objects import Child
@@ -24,10 +29,12 @@ class Player:
on_timepos_change: Callable[[float], None],
on_track_end: Callable[[], None],
on_player_event: Callable[[PlayerEvent], None],
config: AppConfiguration,
):
self.on_timepos_change = on_timepos_change
self.on_track_end = on_track_end
self.on_player_event = on_player_event
self.config = config
self._song_loaded = False
@property
@@ -46,6 +53,10 @@ class Player:
def volume(self, value):
return self._set_volume(value)
def reset(self):
raise NotImplementedError(
'reset must be implemented by implementor of Player')
def play_media(self, file_or_url, progress, song):
raise NotImplementedError(
'play_media must be implemented by implementor of Player')
@@ -88,23 +99,34 @@ class MPVPlayer(Player):
super().__init__(*args)
self.mpv = mpv.MPV()
self.had_progress_value = False
self.progress_value_lock = threading.Lock()
self.progress_value_count = 0
@self.mpv.property_observer('time-pos')
def time_observer(_name, value):
self.on_timepos_change(value)
if value is None and self.had_progress_value:
if value is None and self.progress_value_count > 1:
self.on_track_end()
self.had_progress_value = False
with self.progress_value_lock:
self.progress_value_count = 0
if value:
self.had_progress_value = True
with self.progress_value_lock:
self.progress_value_count += 1
def _is_playing(self):
return not self.mpv.pause
def reset(self):
self._song_loaded = False
with self.progress_value_lock:
self.progress_value_count = 0
def play_media(self, file_or_url, progress, song):
self.had_progress_value = False
with self.progress_value_lock:
self.progress_value_count = 0
self.mpv.pause = False
self.mpv.command(
'loadfile',
@@ -155,6 +177,28 @@ class ChromecastPlayer(Player):
cast_status_listener = CastStatusListener()
media_status_listener = MediaStatusListener()
class ServerThread(threading.Thread):
def __init__(self, host, port, directory):
super().__init__()
self.host = host
self.port = port
self.directory = directory
def generate_handler(self, directory):
class ServerHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=directory, **kwargs)
return ServerHandler
def run(self):
self.server = HTTPServer(
(self.host, self.port),
self.generate_handler(self.directory),
)
# TODO figure out how to make this stop when the app closes.
self.server.serve_forever()
@classmethod
def get_chromecasts(self) -> Future:
def do_get_chromecasts():
@@ -180,6 +224,21 @@ class ChromecastPlayer(Player):
ChromecastPlayer.cast_status_listener.on_new_cast_status = self.on_new_cast_status
ChromecastPlayer.media_status_listener.on_new_media_status = self.on_new_media_status
# Set host_ip
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
self.host_ip = s.getsockname()[0]
s.close()
# TODO make the port come from the app config
self.server_thread = ChromecastPlayer.ServerThread(
'0.0.0.0',
8080,
self.config.cache_location,
)
self.server_thread.daemon = True
self.server_thread.start()
def on_new_cast_status(self, status):
print('new cast status')
print(status)
@@ -239,7 +298,17 @@ class ChromecastPlayer(Player):
def _is_playing(self):
return self.chromecast.media_controller.status.player_is_playing
def reset(self):
self._song_loaded = False
def play_media(self, file_or_url: str, progress: float, song: Child):
stream_scheme = urlparse(file_or_url).scheme
if not stream_scheme:
# TODO make this come from the app config
strlen = len('/home/sumner/.local/share/libremsonic/')
file_or_url = file_or_url[strlen:]
file_or_url = f'http://{self.host_ip}:8080/{quote(file_or_url)}'
cover_art_url = CacheManager.get_cover_art_url(song.id, 1000)
self.chromecast.media_controller.play_media(
file_or_url,