def __init__(self, app): self.app = app self.username = urwid.Edit( edit_text=settings.get('username', 'play_settings') or '') self.password = urwid.Edit( mask='*', edit_text=settings.get('password', 'play_settings') or '') self.device_id = urwid.Edit( edit_text=settings.get('device_id', 'play_settings') or '') self.download_tracks = urwid.CheckBox( 'Download tracks before playback', state=settings.get('download_tracks', 'play_settings') or False) self.equalizer = Equalizer() super(SettingsPage, self).__init__([ urwid.ListBox( urwid.SimpleListWalker([ urwid.Text('Settings'), urwid.Divider(' '), urwid.Text('Username'), urwid.AttrWrap(self.username, 'input', 'input_focus'), urwid.Divider(' '), urwid.Text('Password'), urwid.AttrWrap(self.password, 'input', 'input_focus'), urwid.Divider(' '), urwid.Text('Device ID'), urwid.AttrWrap(self.device_id, 'input', 'input_focus'), urwid.Divider(' '), self.download_tracks, urwid.Divider(' '), urwid.AttrWrap(urwid.Button('Save', on_press=self.on_save), 'input', 'input_focus'), urwid.Divider(u'\u2500'), self.equalizer, ])) ])
def _play(self): """ Pick current track from a queue and requests media stream URL. Completes in background. """ track = self.queue.get_current_track() if track is None: return self._is_loading = True self.broadcast_state() self.track_changed.fire(track) if settings.get('download_tracks', 'play_settings') or \ settings.get_is_file_cached(track.filename): path = settings.get_cached_file_path(track.filename) if path is None: logger.debug('Track %s not in cache, downloading...', track.store_id) track.get_url(callback=self._download_track) else: logger.debug('Track %s in cache, playing', track.store_id) self._play_ready(path, None, track) else: logger.debug('Starting to stream %s', track.store_id) track.get_url(callback=self._play_ready)
def log_in(self, use_token=True): """ Called when this page is shown. Request user authorization. """ authtoken, device_id, username, password = [ settings.get(key, "play_settings") for key in ('authtoken', 'device_id', 'username', 'password') ] if self._login_notification: self._login_notification.close() if use_token and authtoken: self._login_notification = notification_area.notify( 'Using cached auth token...') gp.use_authtoken_async(authtoken, device_id, callback=self.on_check_authtoken) elif username and password and device_id: self._login_notification = notification_area.notify( 'Logging in...') gp.login_async(username, password, device_id, callback=self.on_login) else: self._login_notification = notification_area.notify( 'Please set your credentials on the settings page.')
def main(): """ Application entrypoint. This function is required to allow Clay to be ran as application when installed via setuptools. """ parser = argparse.ArgumentParser( prog=meta.APP_NAME, description=meta.DESCRIPTION, epilog="This project is neither affiliated nor endorsed by Google.") parser.add_argument("-v", "--version", action=MultilineVersionAction) keybinds_group = parser.add_mutually_exclusive_group() keybinds_group.add_argument( "--with-x-keybinds", help="define global X keybinds (requires Keybinder and PyGObject)", action='store_true') keybinds_group.add_argument( "--without-x-keybinds", help="Don't define global keybinds (overrides configuration file)", action='store_true') args = parser.parse_args() if args.version: exit(0) if (args.with_x_keybinds or settings.get('x_keybinds', 'clay_settings')) \ and not args.without_x_keybinds: player.enable_xorg_bindings() # Create a 256 colour palette. palette = [(name, '', '', '', res['foreground'], res['background']) for name, res in settings.colours_config.items()] try: from setproctitle import setproctitle except ImportError: pass else: setproctitle('clay') # Run the actual program app_widget = AppWidget() loop = urwid.MainLoop(app_widget, palette) app_widget.set_loop(loop) loop.screen.set_terminal_properties(256) loop.run()
def _parse_x_hotkeys(self): """ Reads out them configuration file and parses them into hotkeys readable by GTK. """ hotkey_default_config = settings.get_default_config_section( 'hotkeys', 'x_hotkeys') mod_key = settings.get('mod_key', 'hotkeys') hotkeys = {} for action in hotkey_default_config: key_seq = settings.get(action, 'hotkeys', 'x_hotkeys') for key in key_seq.split(', '): hotkey = key.split(' + ') if hotkey[0].strip() == 'mod': hotkey[0] = mod_key hotkey = [self._to_gtk_modifier(key) for key in hotkey] hotkeys[action] = ''.join(hotkey) return hotkeys
def _parse_hotkeys(self): """ Reads out the configuration file and parse them into a hotkeys for urwid. """ hotkey_config = settings.get_default_config_section( 'hotkeys', 'clay_hotkeys') mod_key = settings.get('mod_key', 'hotkeys') hotkeys = {} for hotkey_name, hotkey_dict in hotkey_config.items(): hotkeys[hotkey_name] = {} for action in hotkey_dict.keys(): key_seq = settings.get(action, 'hotkeys', 'clay_hotkeys', hotkey_name) for key in key_seq.split(', '): hotkey = key.split(' + ') if hotkey[0].strip() == 'mod': hotkey[0] = mod_key hotkeys[hotkey_name][' '.join(hotkey)] = action return hotkeys
class PlayBar(urwid.Pile): """ A widget that shows currently played track, playback progress and flags. """ _unicode = settings.get('unicode', 'clay_settings') ROTATING = u'|' u'/' u'\u2014' u'\\' RATING_ICONS = { 0: ' ', 1: u'\U0001F593' if _unicode else '-', 4: u'\U0001F592' if _unicode else '+', 5: u'\U0001F592' if _unicode else '+' } def __init__(self, app): # super(PlayBar, self).__init__(*args, **kwargs) self.app = app self.rotating_index = 0 self.text = urwid.Text('', align=urwid.LEFT) self.flags = [] self.progressbar = ProgressBar() self.shuffle_el = urwid.AttrWrap(urwid.Text(u' \u22cd SHUF '), 'flag') self.repeat_el = urwid.AttrWrap(urwid.Text(u' \u27f2 REP '), 'flag') self.infobar = urwid.Columns([ self.text, ('pack', self.shuffle_el), # ('pack', urwid.Text(' ')), ('pack', self.repeat_el) ]) super(PlayBar, self).__init__([ ('pack', self.progressbar), ('pack', self.infobar), ]) self.update() player.media_position_changed += self.update player.media_state_changed += self.update player.track_changed += self.update player.playback_flags_changed += self.update def get_rotating_bar(self): """ Return a spinner char for current rotating_index. """ return PlayBar.ROTATING[self.rotating_index % len(PlayBar.ROTATING)] @staticmethod def get_style(): """ Return the style for current playback state. """ if player.is_loading or player.is_playing: return 'title-playing' return 'title-idle' def get_text(self): """ Return text for display in this bar. """ track = player.get_current_track() if track is None: return u'{} {}'.format(meta.APP_NAME, meta.VERSION_WITH_CODENAME) progress = player.get_play_progress_seconds() total = player.get_length_seconds() return ( self.get_style(), u' {} {} - {} {} [{:02d}:{:02d} / {:02d}:{:02d}]'.format( # u'|>' if player.is_playing else u'||', # self.get_rotating_bar(), u'\u2505' if player.is_loading else u'\u25B6' if player.is_playing else u'\u25A0', track.artist, track.title, self.RATING_ICONS[track.rating], progress // 60, progress % 60, total // 60, total % 60, )) def update(self, *_): """ Force update of this widget. Called when something unrelated to completion value changes, e.g. current track or playback flags. """ self.text.set_text(self.get_text()) self.progressbar.set_progress(player.get_play_progress()) self.progressbar.set_done_style('progressbar_done' if player.is_playing else 'progressbar_done_paused') self.shuffle_el.attr = 'flag-active' \ if player.get_is_random() \ else 'flag' self.repeat_el.attr = 'flag-active' \ if player.get_is_repeat_one() \ else 'flag' self.app.redraw() def tick(self): """ Increase rotating index & trigger redraw. """ self.rotating_index += 1 self.update()
class SongListItem(urwid.Pile): """ Widget that represents single song item. """ _unicode = settings.get('unicode', 'clay_settings') signals = [ 'activate', 'play', 'append-requested', 'unappend-requested', 'station-requested', 'context-menu-requested' ] STATE_IDLE = 0 STATE_LOADING = 1 STATE_PLAYING = 2 STATE_PAUSED = 3 LINE1_ATTRS = { STATE_IDLE: ('line1', 'line1_focus'), STATE_LOADING: ('line1_active', 'line1_active_focus'), STATE_PLAYING: ('line1_active', 'line1_active_focus'), STATE_PAUSED: ('line1_active', 'line1_active_focus') } LINE2_ATTRS = { STATE_IDLE: ('line2', 'line2_focus'), STATE_LOADING: ('line2', 'line2_focus'), STATE_PLAYING: ('line2', 'line2_focus'), STATE_PAUSED: ('line2', 'line2_focus') } STATE_ICONS = {0: ' ', 1: u'\u2505', 2: u'\u25B6', 3: u'\u25A0'} RATING_ICONS = { 0: ' ', 1: u'\U0001F593' if _unicode else '-', 2: u'\U0001F593' if _unicode else '2', 3: u'\U0001F593' if _unicode else '3', 4: u'\U0001F593' if _unicode else '4', 5: u'\U0001F592' if _unicode else '+' } EXPLICIT_ICONS = { 0: ' ', # not actually used? 1: u'\U0001F174' if _unicode else '[E]', 2: ' ', 3: ' ' } def __init__(self, track): self.track = track self.rating = self.RATING_ICONS[track.rating] self.explicit = self.EXPLICIT_ICONS[track.explicit_rating] self.index = 0 self.state = SongListItem.STATE_IDLE self.line1_left = urwid.SelectableIcon('', cursor_position=1000) self.line1_left.set_layout('left', 'clip', None) self.line1_right = urwid.Text('x') self.line1 = urwid.Columns([ self.line1_left, ('pack', self.line1_right), ('pack', urwid.Text(' ')) ]) self.line2 = urwid.Text('', wrap='clip') self.line1_wrap = urwid.AttrWrap(self.line1, 'line1') self.line2_wrap = urwid.AttrWrap(self.line2, 'line2') self.content = urwid.Pile( [self.line1_wrap, self.line2_wrap, urwid.Text('')]) self.is_focused = False super(SongListItem, self).__init__([self.content]) self.update_text() def set_state(self, state): """ Set state for this song. Possible choices are: - :attr:`.SongListItem.STATE_IDLE` - :attr:`.SongListItem.STATE_LOADING` - :attr:`.SongListItem.STATE_PLAYING` - :attr:`.SongListItem.STATE_PAUSED` """ self.state = state self.update_text() @staticmethod def get_state_icon(state): """ Get icon char for specific state. """ return SongListItem.STATE_ICONS[state] def update_text(self): """ Update text of this item from the attached track. """ self.line1_left.set_text( u'{index:3d} {icon} {title} [{minutes:02d}:{seconds:02d}]'.format( index=self.index + 1, icon=self.get_state_icon(self.state), title=self.track.title, minutes=self.track.duration // (1000 * 60), seconds=(self.track.duration // 1000) % 60, )) if settings.get_is_file_cached(self.track.filename): self.line1_right.set_text(u' \u25bc Cached') else: self.line1_right.set_text(u'') self.line1_right.set_text(u'{explicit} {rating}'.format( explicit=self.explicit, rating=self.rating)) self.line2.set_text(u' {} \u2015 {}'.format( self.track.artist, self.track.album_name)) self.line1_wrap.set_attr( SongListItem.LINE1_ATTRS[self.state][self.is_focused]) self.line2_wrap.set_attr( SongListItem.LINE2_ATTRS[self.state][self.is_focused]) @property def full_title(self): """ Return song artist and title. """ return u'{} - {} {}'.format(self.track.artist, self.track.title, self.rating) def keypress(self, size, key): """ Handle keypress. """ return hotkey_manager.keypress("library_item", self, super(SongListItem, self), size, key) def mouse_event(self, size, event, button, col, row, focus): """ Handle mouse event. """ if button == 1 and focus: urwid.emit_signal(self, 'activate', self) return None return super(SongListItem, self).mouse_event(size, event, button, col, row, focus) def thumbs_up(self): """ Thumb the currently selected song up. """ self.track.rate_song((0 if self.track.rating == 5 else 5)) def thumbs_down(self): """ Thumb the currently selected song down. """ self.track.rate_song((0 if self.track.rating == 1 else 1)) def _send_signal(self, signal): urwid.emit_signal(self, signal, self) def activate(self): """ Add the entire list to queue and begin playing """ self._send_signal("activate") def play(self): """ Play this song. """ self._send_signal("play") def append(self): """ Add this song to the queue. """ self._send_signal("append-requested") self.play() def unappend(self): """ Remove this song from the queue. """ if not self.is_currently_played: self._send_signal("unappend-requested") def request_station(self): """ Create a Google Play Music radio for this song. """ self._send_signal("station-requested") def show_context_menu(self): """ Display the context menu for this song. """ self._send_signal("context-menu-requested") @property def is_currently_played(self): """ Return ``True`` if song is in state :attr:`.SongListItem.STATE_PLAYING` or :attr:`.SongListItem.STATE_PAUSED`. """ return self.state in (SongListItem.STATE_LOADING, SongListItem.STATE_PLAYING, SongListItem.STATE_PAUSED) def set_index(self, index): """ Set numeric index for this item. """ self.index = index self.update_text() def render(self, size, focus=False): """ Render widget & set focused state. """ self.is_focused = focus self.update_text() return super(SongListItem, self).render(size, focus)