subclient

Subsonic ncurses client
git clone https://git.e1e0.net/subclient.git
Log | Files | Refs | README | LICENSE

commit 392c2e9b07819f310b868e07191098bfa41d7a54
parent b354f301e77376b7860ca9f192bbf8327d19a7fc
Author: Paco Esteban <paco@e1e0.net>
Date:   Wed, 12 Jan 2022 20:35:26 +0100

pass stream urls to mpv instead of downloaded files

Remove all the cache stuff while here, as it's not neede anymore.

This is a bit of a hack, as the library I'm using does not expose the
stream url to the client.  So I did 2 things, first change the method
the library interacts with the API from POST to GET.  The library says
you don't do that, but I need it to pass auth info to the mpv process.

Later I created a method that calls to private methods of the libsonic
library to get the full generated url with all the fields I need.

I pass that to mpv which is more than capable to ask for the stream and
play it.  This simplifies greatly the song retrieval process and makes
the playing experience more "natural" specially at the beginning of play
when nothing was cached.

Diffstat:
MREADME.md | 17+----------------
Msubclient/helpers.py | 8--------
Msubclient/player.py | 23++---------------------
Msubclient/subclient.py | 20--------------------
Msubclient/subsonic.py | 9++++++---
5 files changed, 9 insertions(+), 68 deletions(-)

diff --git a/README.md b/README.md @@ -33,24 +33,9 @@ The config file looks like this: url = https://my.subsonic.server.tld username = myuser password = mypassword -max_cache = 524288000 ``` -All config items are mandatory. The first 3 are pretty self explanatory, the -last one `max_cache` is the max size in bytes of the cache folder. See the -next section for more info. - -## Cache -`subclient` downloads the music files from the subsonic server and stores them -on the`$XDG_CACHE_HOME/subclient` or, if `$XDG_CACHE_HOME` is not defined, on -`~/.cache/subclient`. This is a bit primitive for now. It downloads all the -files from the one you pick till the end of the album. I may refine this in -the future, but it works for me for now. - -On start, it launches a thread that runs a cache clean routine every 60 seconds -(this timing might change in the future). If the size of the cache folder is -bigger than `max_cache` defined on the config file, it deletes 3 random files -until the disk usage is below that threshold. +All config items are mandatory. ## Usage On start you'll be presented with a list of artists that `subclient` gets from diff --git a/subclient/helpers.py b/subclient/helpers.py @@ -20,14 +20,6 @@ def format_duration(seconds): return time.strftime(time_format, ty_res) -def get_cache_folder(): - cache_home = os.environ.get('XDG_CACHE_HOME') - if cache_home: - return cache_home + '/subclient' - else: - return HOME + '/.cache/subclient' - - def get_config_file(): config_home = os.environ.get('XDG_CONFIG_HOME') if config_home: diff --git a/subclient/player.py b/subclient/player.py @@ -1,6 +1,5 @@ from python_mpv_jsonipc import MPV from subclient import helpers -import os class Player: @@ -8,30 +7,12 @@ class Player: self.mpv = MPV(ipc_socket='/tmp/subclient-mpv-socket', audio_display='no') self.orig = origin - self.cache_dir = helpers.get_cache_folder() def play(self, playlist): self.stop() for s in playlist: - filename = self.download_stream(s) - self.mpv.loadfile(filename, 'append-play') - - def download_stream(self, song): - if not os.path.isdir(self.cache_dir): - os.makedirs(self.cache_dir) - - stream = self.orig.get_song_stream(song) - filename = f'{self.cache_dir}/{song.id}' - - if not os.path.isfile(filename): - with open(filename, 'wb') as fp: - while True: - chunk = stream.read(512 * 1024) - if not chunk: - break - fp.write(chunk) - - return filename + song_url = self.orig.get_song_stream_url(s) + self.mpv.loadfile(song_url, 'append-play') def exit(self): self.mpv.terminate() diff --git a/subclient/subclient.py b/subclient/subclient.py @@ -3,8 +3,6 @@ import configparser import curses -import os -import random import sys from datetime import timedelta from subclient import subsonic @@ -59,9 +57,6 @@ class SubClient: self.playlist = [] self.info_updater = helpers.Job(timedelta(seconds=1), self.update_info) - self.cache_cleaner = helpers.Job(timedelta(seconds=60), - self.clean_cache) - self.cache_cleaner.start() def init_curses(self): self.stdscr = curses.initscr() @@ -88,8 +83,6 @@ class SubClient: finally: if self.info_updater.is_alive(): self.info_updater.stop() - if self.cache_cleaner.is_alive(): - self.cache_cleaner.stop() self.player.exit() curses.endwin() @@ -250,19 +243,6 @@ class SubClient: if not self.player.is_paused(): self.refresh(['info']) - def clean_cache(self): - cache_folder = helpers.get_cache_folder() - cache_folder_size = sum( - os.path.getsize(f'{cache_folder}/{f}') - for f in os.listdir(cache_folder) - if os.path.isfile(f'{cache_folder}/{f}')) - if cache_folder_size > int(self.config['subclient']['max_cache']): - all_files = os.listdir(cache_folder) - pl_files = [x.id for x in self.playlist] - to_delete = list(set(all_files) - set(pl_files)) - for f in random.sample(to_delete, 3): - os.remove(f'{cache_folder}/{f}') - def refresh(self, win): if 'list' in win: self.listwin.erase() diff --git a/subclient/subsonic.py b/subclient/subsonic.py @@ -38,7 +38,8 @@ class Subsonic: self.s = libsonic.Connection(config['url'], config['username'], config['password'], - port=443) + port=443, + useGET=True) def get_artists(self): artists = [] @@ -56,5 +57,7 @@ class Subsonic: songs = self.s.getAlbum(album.id)['album']['song'] return [Song(**s) for s in songs] - def get_song_stream(self, song): - return self.s.stream(song.id, tformat='raw') + def get_song_stream_url(self, song): + q = self.s._getQueryDict({'id': song.id, 'format': 'raw'}) + req = self.s._getRequest('stream.view', q) + return req.get_full_url()