subclient

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

commit 42e51ec5f95795bb43d507a0fbdc1951b0c19c2c
parent 0aa392a1a735b6b84a559aa0295e7b48f9246b2a
Author: Paco Esteban <paco@e1e0.net>
Date:   Thu, 21 Oct 2021 21:07:30 +0200

implement now playing with time and all

Diffstat:
Msubclient/helpers.py | 2++
Msubclient/player.py | 29+++++++++++++++++++++++++----
Msubclient/subclient.py | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
3 files changed, 99 insertions(+), 14 deletions(-)

diff --git a/subclient/helpers.py b/subclient/helpers.py @@ -12,6 +12,8 @@ def format_duration(seconds): :return: String of the form "%H:%M:%S" :rtype: string """ + if seconds is None: + return "-" time_format = "%H:%M:%S" if seconds > 3600 else "%M:%S" ty_res = time.gmtime(seconds) return time.strftime(time_format, ty_res) diff --git a/subclient/player.py b/subclient/player.py @@ -15,7 +15,6 @@ class Player: for s in playlist: filename = self.download_stream(s) self.mpv.loadfile(filename, 'append-play') - # self.mpv.wait_for_property('eof-reached') def download_stream(self, song): if not os.path.isdir(self.cache_dir): @@ -41,9 +40,6 @@ class Player: def is_paused(self): return self.mpv.pause - def is_idle(self): - return self.mpv.core_idle - def set_pause(self, state): self.mpv.pause = state @@ -58,3 +54,28 @@ class Player: def seek(self, duration='+5'): self.mpv.command('seek', duration) + + def get_song_info(self): + title = self.mpv.media_title + if title is None: + return ' ' + metadata = self.mpv.metadata + artist = ' ' + if metadata is not None: + artist = self.mpv.metadata.get('album_artist', ' ') + return f'{artist} :: {title}' + + def get_list_info(self): + playlist_total = self.mpv.playlist_count + playlist_pos = self.mpv.playlist_pos_1 + if playlist_total is None or playlist_pos is None: + return ' ' + return f'{playlist_pos} of {playlist_total}' + + def get_duration_info(self): + raw_duration = self.mpv.duration + if raw_duration is None: + return ' ' + duration = helpers.format_duration(raw_duration) + time_pos = helpers.format_duration(self.mpv.time_pos) + return f'{time_pos} / {duration}' diff --git a/subclient/subclient.py b/subclient/subclient.py @@ -4,6 +4,8 @@ import configparser import curses import sys +import threading +from datetime import timedelta from subclient import subsonic from subclient import player from subclient import helpers @@ -12,6 +14,25 @@ from subclient import helpers __version__ = 'v0.0.1' +class Updater(threading.Thread): + def __init__(self, interval, execute, *args, **kwargs): + threading.Thread.__init__(self) + self.daemon = False + self.stopped = threading.Event() + self.interval = interval + self.execute = execute + self.args = args + self.kwargs = kwargs + + def stop(self): + self.stopped.set() + self.join() + + def run(self): + while not self.stopped.wait(self.interval.total_seconds()): + self.execute(*self.args, **self.kwargs) + + class SubClient: UP = -1 DOWN = 1 @@ -43,6 +64,7 @@ class SubClient: self.width = 0 self.listwin_height = 0 self.infowin_height = 0 + self.info_updater = Updater(timedelta(seconds=1), self.update_info) self.init_curses() self.nav_list = self.subsonic.get_artists() @@ -63,9 +85,10 @@ class SubClient: self.infowin_height = self.height - self.listwin_height self.listwin = curses.newwin(self.listwin_height, self.width, 0, 0) - self.infowin = curses.newwin(3, self.width, - self.listwin_height + 2, 0) + self.infowin = curses.newwin(6, self.width, + self.listwin_height + 1, 0) self.listwin.keypad(True) + self.infowin.border(0) curses.curs_set(0) # disable cursor curses.noecho() @@ -74,44 +97,51 @@ class SubClient: def run(self): # read chars until we exit, then clean try: + self.refresh(['list', 'info']) self.input_loop() except KeyboardInterrupt: pass finally: + self.info_updater.stop() self.player.exit() curses.endwin() def input_loop(self): while True: - self.listwin.erase() - self.load_list() - self.refresh() - c = self.listwin.getkey() if c == 'q': # quit break elif c == 'j': self.nav_scroll(self.DOWN) + self.refresh(['list']) elif c == 'k': self.nav_scroll(self.UP) + self.refresh(['list']) elif c == 'f': self.nav_page(self.DOWN) + self.refresh(['list']) elif c == 'b': self.nav_page(self.UP) + self.refresh(['list']) elif c == 'l': self.nav_in_out(self.IN) + self.refresh(['list']) elif c == 'h': self.nav_in_out(self.OUT) + self.refresh(['list']) elif c == 'n': self.player.play_next() + self.refresh(['info']) elif c == 'p': self.player.play_prev() + self.refresh(['info']) elif c == ' ': self.player.set_pause(not self.player.is_paused()) + self.refresh(['info']) elif c in [curses.KEY_ENTER, '\r', '\n']: if self.nav_list_type == self.SONGS: - playlist = self.nav_list[self.nav_top + self.nav_selected:self.nav_bottom] - self.player.play(playlist) + self.start_player() + self.info_updater.start() def load_list(self): for i, t in enumerate( @@ -203,8 +233,40 @@ class SubClient: self.nav_bottom = len(self.nav_list) self.nav_pages = self.nav_bottom // self.nav_max_lines - def refresh(self): - self.listwin.noutrefresh() + def start_player(self): + playlist = self.nav_list[self.nav_top + self.nav_selected:self.nav_bottom] + self.player.play(playlist) + + def get_info(self): + list_info = self.player.get_list_info() + song_info = self.player.get_song_info() + duration_info = self.player.get_duration_info() + if self.player.is_paused(): + icon = '|| ' + else: + icon = '> ' + + if song_info is not None: + self.infowin.addnstr(1, 2, icon + list_info, self.width - 3) + self.infowin.addnstr(2, 2, song_info, self.width - 3) + self.infowin.addnstr(3, 2, duration_info, self.width - 3) + + def update_info(self): + self.refresh(['info']) + + def refresh(self, win): + if 'list' in win: + self.listwin.erase() + self.load_list() + self.listwin.noutrefresh() + + if 'info' in win: + self.infowin.addstr(1, 2, " " * (self.width - 3)) + self.infowin.addstr(2, 2, " " * (self.width - 3)) + self.infowin.addstr(3, 2, " " * (self.width - 3)) + self.get_info() + self.infowin.noutrefresh() + curses.doupdate()