def _parse_hotkeys(self): """ Reads out the configuration file and parse them into a hotkeys for urwid. """ hotkey_config = settings_manager.get_default_config_section('hotkeys') mod_key = settings_manager.get('mod_key', 'clay_settings') hotkeys = {} for hotkey_name, hotkey_dict in hotkey_config.items(): hotkeys[hotkey_name] = {} for action, sequence in hotkey_dict.items(): key_seq = settings_manager.get(action, 'hotkeys', hotkey_name) if key_seq is None: key_seq = sequence 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
def __init__(self, app): self.app = app self.username = urwid.Edit( edit_text=settings_manager.get('username', 'play_settings') or '') self.password = urwid.Edit( mask='*', edit_text=settings_manager.get('password', 'play_settings') or '') self.device_id = urwid.Edit( edit_text=settings_manager.get('device_id', 'play_settings') or '') self.download_tracks = urwid.CheckBox( 'Download tracks before playback', state=settings_manager.get('download_tracks', 'play_settings') or False) 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') ])) ])
def log_in(self, use_token=True): """ Called when this page is shown. Request user authorization. """ authtoken, device_id, username, password = [ settings_manager.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 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._loading = True self.broadcast_state() self.track_changed.fire(track) if settings_manager.get('download_tracks', 'play_settings') or \ settings_manager.get_is_file_cached(track.filename): path = settings_manager.get_cached_file_path(track.filename) if path is None: logger.debug('Track %s not in cache, downloading...', track.id) track.get_url(callback=self._download_track) else: logger.debug('Track %s in cache, playing', track.id) self._play_ready(path, None, track) else: logger.debug('Starting to stream %s', track.id) track.get_url(callback=self._play_ready)
class Icons: """ Icons used to indicate the state, rating or how explicit a song is. """ _unicode = settings_manager.get('unicode', 'clay_settings') state = [' ', u'\u2505', u'\u25B6', u'\u25A0'] if _unicode: ratings = [' ', '\U0001F593', '\U0001F593', '\U0001F593', '\U0001F593', '\U0001F592'] explicit = [' ', u'\U0001F174', ' ', ''] else: ratings = [' ', '-', '2', '3', '4', '+'] explicit = [' ', '[E]', ' ', ' ']
""" Clipboard utils. """ import shlex, subprocess from clay.core import settings_manager from clay.ui.urwid.notifications import notification_area cmd = settings_manager.get('copy_command', 'clay_settings') COMMAND = shlex.split(cmd) if cmd is not None else None def copy(text): """ Copy text to clipboard. Return True on success. """ try: if COMMAND is None: return proc = subprocess.Popen(COMMAND, stdin=subprocess.PIPE) proc.communicate(text.encode('utf-8')) except FileNotFoundError: notification_area.notify('Failed to copy text to clipboard. ' 'Please install "%s"' % COMMAND[0]) except Exception as e: notification_area.notify('Failed to copy text to clipboard. ' 'Unknown error: "%s".' % e)
class SongListItem(urwid.Pile): """ Widget that represents single song item. """ _unicode = settings_manager.get('unicode', 'clay_settings') signals = [ 'activate', 'play', 'append-requested', 'unappend-requested', 'clear-queue', '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_manager.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("song_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 clear_queue(self): """ Removes all the songs from the queue. """ self._send_signal("clear-queue") def play(self): """ Play this song. """ self._send_signal("play") def append(self): """ Add this song to the queue. """ self._send_signal("append-requested") 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)
class PlayBar(urwid.Pile): """ A widget that shows currently played track, playback progress and flags. """ _unicode = settings_manager.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.repeat_one_el = urwid.AttrWrap(urwid.Text(' 1 ONE'), 'flag') self.infobar = urwid.Columns([ self.text, ('pack', self.shuffle_el), ('pack', self.repeat_one_el), ('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.media_state_stopped += self.stop 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.loading or player.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.play_progress_seconds total = player.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.loading else u'\u25B6' if player.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.play_progress) self.progressbar.set_done_style('progressbar_done' if player. playing else 'progressbar_done_paused') self.shuffle_el.attr = 'flag-active' \ if player.random \ else 'flag' self.repeat_one_el.attr = 'flag-active' \ if player.repeat_one \ else 'flag' self.repeat_el.attr = 'flag-active' \ if player.repeat_queue \ else 'flag' self.app.redraw() def stop(self, *_): """ Force update of this widget. Only runs when the queue is entirely cleared. """ self.text.set_text("") self.progressbar.set_progress(0) self.progressbar.set_done_style('progressbar_done') self.shuffle_el.attr = 'flag-active' \ if player.random \ else 'flag' self.repeat_el.attr = 'flag-active' \ if player.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_manager.get('unicode', 'clay_settings') signals = [ 'activate', 'play', 'append-requested', 'unappend-requested', 'clear-queue', 'station-requested', 'context-menu-requested' ] def __init__(self, track): self.track = track self.rating = Icons.ratings[track.rating] self.explicit = Icons.explicit[track.explicit_rating] self.index = 0 self.state = States.idle self.line1 = _Line1() 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:`States.idle` - :attr:`States.loading` - :attr:`States.playing` - :attr:`States.paused` """ self.state = state self.update_text() @staticmethod def get_state_icon(state): """ Get icon char for specific state. """ return Icons.state[state.value] def update_text(self): """ Update text of this item from the attached track. """ self.line1.update_text(self) self.line2.update_text(self) @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("song_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 clear_queue(self): """ Removes all the songs from the queue. """ self.set_state(States.idle) self.is_focused = False self._send_signal("clear-queue") def play(self): """ Play this song. """ self._send_signal("play") def append(self): """ Add this song to the queue. """ self._send_signal("append-requested") 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:`.States.playing` or :attr:`.States.paused`. """ return self.state in (States.loading, States.playing, States.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)
""" On-screen display stuff. """ from pydbus import SessionBus, Variant from clay.core import meta, logger, settings_manager from gi.repository import GLib NOTIFICATION_BUS_NAME = ".Notifications" BASE_NAME = "org.freedesktop" ENABLED = settings_manager.get('desktop_notifications', 'clay_settings') class _OSDManager(object): """ Manages OSD notifications via DBus. """ def __init__(self): if not ENABLED: self._actions = {} return self._last_id = 0 self.bus = SessionBus() self.notifications = None self.bus.watch_name(BASE_NAME + NOTIFICATION_BUS_NAME, name_appeared=self._register_bus_name, name_vanished=self._deregister_bus_name) self._register_bus_name(None) self._actions = {"default": lambda *args: None}