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()