subclient

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

subclient.py (9424B)


      1 #!/usr/bin/env python3
      2 # -*- coding: utf-8 -*-
      3 
      4 import configparser
      5 import curses
      6 import sys
      7 from datetime import timedelta
      8 from subclient import subsonic
      9 from subclient import player
     10 from subclient import helpers
     11 
     12 __version__ = 'v0.0.1'
     13 
     14 
     15 class SubClient:
     16     UP = -1
     17     DOWN = 1
     18     OUT = -1
     19     IN = 1
     20 
     21     ARTISTS = 0
     22     ALBUMS = 1
     23     SONGS = 2
     24 
     25     def __init__(self):
     26         self.config = configparser.ConfigParser()
     27         try:
     28             config_file = helpers.get_config_file()
     29         except FileNotFoundError as e:
     30             print(e, file=sys.stderr)
     31             sys.exit(1)
     32         else:
     33             self.config.read(config_file)
     34 
     35         self.subsonic = subsonic.Subsonic(self.config['subclient'])
     36 
     37         self.player = player.Player(self.subsonic)
     38 
     39         self.stdscr = None
     40         self.listwin = None
     41         self.infowin = None
     42         self.height = 0
     43         self.width = 0
     44         self.listwin_height = 0
     45         self.infowin_height = 0
     46         self.init_curses()
     47 
     48         self.nav_list = self.subsonic.get_artists()
     49         self.nav_list_type = self.ARTISTS
     50         self.nav_selected = 0
     51         self.nav_top = 0
     52         self.nav_bottom = len(self.nav_list)
     53         self.nav_max_lines = self.listwin_height
     54         self.nav_pages = self.nav_bottom // self.nav_max_lines
     55 
     56         self.artist = None
     57         self.playlist = []
     58 
     59         self.info_updater = helpers.Job(timedelta(seconds=1), self.update_info)
     60 
     61     def init_curses(self):
     62         self.stdscr = curses.initscr()
     63         self.height, self.width = self.stdscr.getmaxyx()
     64         # leave room for the info window
     65         self.listwin_height = int(self.height * 0.8)
     66         self.infowin_height = self.height - self.listwin_height
     67         self.listwin = curses.newwin(self.listwin_height, self.width, 0, 0)
     68         self.infowin = curses.newwin(5, self.width, self.listwin_height + 1, 0)
     69         self.listwin.keypad(True)
     70         self.infowin.border(0)
     71 
     72         curses.curs_set(0)  # disable cursor
     73         curses.noecho()
     74         curses.cbreak()
     75 
     76     def run(self):
     77         # read chars until we exit, then clean
     78         try:
     79             self.refresh(['list', 'info'])
     80             self.input_loop()
     81         except KeyboardInterrupt:
     82             pass
     83         finally:
     84             if self.info_updater.is_alive():
     85                 self.info_updater.stop()
     86             self.player.exit()
     87             curses.endwin()
     88 
     89     def input_loop(self):
     90         while True:
     91             c = self.listwin.getkey()
     92             if c == 'q':  # quit
     93                 break
     94             elif c == 'j':
     95                 self.nav_scroll(self.DOWN)
     96                 self.refresh(['list'])
     97             elif c == 'k':
     98                 self.nav_scroll(self.UP)
     99                 self.refresh(['list'])
    100             elif c == 'f':
    101                 self.nav_page(self.DOWN)
    102                 self.refresh(['list'])
    103             elif c == 'b':
    104                 self.nav_page(self.UP)
    105                 self.refresh(['list'])
    106             elif c == 'l':
    107                 self.nav_in_out(self.IN)
    108                 self.refresh(['list'])
    109             elif c == 'h':
    110                 self.nav_in_out(self.OUT)
    111                 self.refresh(['list'])
    112             elif c == 'n':
    113                 self.player.play_next()
    114                 self.refresh(['info'])
    115             elif c == 'p':
    116                 self.player.play_prev()
    117                 self.refresh(['info'])
    118             elif c == ' ':
    119                 self.player.set_pause(not self.player.is_paused())
    120                 self.refresh(['info'])
    121             elif c in [curses.KEY_ENTER, '\r', '\n']:
    122                 if self.nav_list_type == self.SONGS:
    123                     self.start_player()
    124                     if not self.info_updater.is_alive():
    125                         self.info_updater.start()
    126 
    127     def load_list(self):
    128         for i, t in enumerate(self.nav_list[self.nav_top:self.nav_top +
    129                                             self.nav_max_lines]):
    130             if i == self.nav_selected:
    131                 self.listwin.addstr(i, 0, str(t), curses.A_REVERSE)
    132             else:
    133                 self.listwin.addstr(i, 0, str(t))
    134 
    135     def nav_scroll(self, direction):
    136         # next cursor position
    137         next_line = self.nav_selected + direction
    138 
    139         # scroll up direction
    140         if direction == self.UP:
    141             # with overflow
    142             # current cursor position is 0 so we are at the first line,
    143             # but top position is greater than 0, so we can scroll up.
    144             # so we substract 1 from nav_top
    145             if self.nav_top > 0 and self.nav_selected == 0:
    146                 self.nav_top += direction
    147                 return
    148             # without overflow
    149             # current cursor position is greater than 0
    150             # so we just adjust the selected line.
    151             if self.nav_selected > 0:
    152                 self.nav_selected = next_line
    153                 return
    154 
    155         # Scroll in down direction
    156         if direction == self.DOWN:
    157             # With overflow
    158             # next cursor position reaches max lines, so we increment the top
    159             # but we take care to not increase if we reach end of list (top
    160             # + one page < than len(list) aka bottom)
    161             if (next_line == self.nav_max_lines
    162                     and self.nav_top + self.nav_max_lines < self.nav_bottom):
    163                 self.nav_top += direction
    164                 return
    165             # Without overflow
    166             # next cursor position is above max lines, so we increment the
    167             # selected line
    168             if next_line < self.nav_max_lines and next_line < self.nav_bottom:
    169                 self.nav_selected = next_line
    170                 return
    171 
    172     def nav_page(self, direction):
    173         current_page = (self.nav_top + self.nav_selected) // self.nav_max_lines
    174         # Page up
    175         # we test current page >= 0 instead of just > 0 because we can be in
    176         # a situation when the calculated current page is already 0 and compare
    177         # > 0 would prevent going up that last bit.  So we actuall allow to go
    178         # negative and correct afterwards.
    179         if (direction == self.UP) and (current_page >= 0):
    180             self.nav_top -= self.nav_max_lines
    181             # top cannot be negative
    182             if self.nav_top < 0:
    183                 self.nav_top = 0
    184             return
    185         # Page down
    186         # if current page is not last page we increment top by a full page
    187         if (direction == self.DOWN) and (current_page < self.nav_pages):
    188             self.nav_top += self.nav_max_lines
    189             # the last page is probably shorter, so adjust for that
    190             if (self.nav_top + self.nav_max_lines) > self.nav_bottom:
    191                 self.nav_top = self.nav_bottom - self.nav_max_lines
    192             return
    193 
    194     def nav_in_out(self, direction):
    195         self.nav_list_type += direction
    196         if self.nav_list_type > self.SONGS:
    197             self.nav_list_type = self.SONGS
    198             return
    199         if self.nav_list_type < self.ARTISTS:
    200             self.nav_list_type = self.ARTISTS
    201             return
    202 
    203         # we keep track of the artist, which is the only thing we need really
    204         # if is None we get it from the current list, and we reset it to None
    205         # when we go back to the artists list
    206         if self.nav_list_type == self.ARTISTS:
    207             self.nav_list = self.subsonic.get_artists()
    208             if self.artist is not None:
    209                 self.artist = None
    210         elif self.nav_list_type == self.ALBUMS:
    211             if self.artist is None:
    212                 self.artist = self.nav_list[self.nav_top + self.nav_selected]
    213             self.nav_list = self.subsonic.get_albums_from_artist(self.artist)
    214         elif self.nav_list_type == self.SONGS:
    215             album = self.nav_list[self.nav_top + self.nav_selected]
    216             self.nav_list = self.subsonic.get_songs_from_album(album)
    217 
    218         self.nav_top = 0
    219         self.nav_selected = 0
    220         self.nav_bottom = len(self.nav_list)
    221         self.nav_pages = self.nav_bottom // self.nav_max_lines
    222 
    223     def start_player(self):
    224         self.playlist = self.nav_list[self.nav_top +
    225                                       self.nav_selected:self.nav_bottom]
    226         self.player.play(self.playlist)
    227 
    228     def get_info(self):
    229         list_info = self.player.get_list_info()
    230         song_info = self.player.get_song_info()
    231         duration_info = self.player.get_duration_info()
    232         if self.player.is_paused() or self.player.is_idle():
    233             icon = '|| '
    234         else:
    235             icon = '>  '
    236 
    237         if song_info is not None:
    238             self.infowin.addnstr(1, 2, icon + list_info, self.width - 3)
    239             self.infowin.addnstr(2, 2, song_info, self.width - 3)
    240             self.infowin.addnstr(3, 2, duration_info, self.width - 3)
    241 
    242     def update_info(self):
    243         if not self.player.is_paused():
    244             self.refresh(['info'])
    245 
    246     def refresh(self, win):
    247         if 'list' in win:
    248             self.listwin.erase()
    249             self.load_list()
    250             self.listwin.noutrefresh()
    251 
    252         if 'info' in win:
    253             self.infowin.addstr(1, 2, " " * (self.width - 3))
    254             self.infowin.addstr(2, 2, " " * (self.width - 3))
    255             self.infowin.addstr(3, 2, " " * (self.width - 3))
    256             self.get_info()
    257             self.infowin.noutrefresh()
    258 
    259         curses.doupdate()
    260 
    261 
    262 def main():
    263     subclient = SubClient()
    264     subclient.run()
    265 
    266 
    267 if __name__ == "__main__":
    268     main()