def __init__(self, library, player, pattern_filename): super(SongInfo, self).__init__() self._pattern_filename = pattern_filename self.set_visible_window(False) align = Align(halign=Gtk.Align.START, valign=Gtk.Align.START) label = Gtk.Label() label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) label.set_track_visited_links(False) label.set_selectable(True) align.add(label) label.set_alignment(0.0, 0.0) self._label = label connect_destroy(library, 'changed', self._on_library_changed, player) connect_destroy(player, 'song-started', self._on_song_started) label.connect('populate-popup', self._on_label_popup, player, library) self.connect('button-press-event', self._on_button_press_event, player, library) try: with open(self._pattern_filename, "rb") as h: self._pattern = h.read().strip().decode("utf-8") except (EnvironmentError, UnicodeDecodeError): pass self._compiled = XMLFromMarkupPattern(self._pattern) align.show_all() self.add(align)
def pack(self, songpane): self._main_box.pack1(self, True, False) self._rh_box = rhbox = Gtk.VBox(spacing=6) align = Align(self._sb_box, left=0, right=6, top=6) rhbox.pack_start(align, False, True, 0) rhbox.pack_start(songpane, True, True, 0) self._main_box.pack2(rhbox, True, False) rhbox.show() align.show_all() return self._main_box
class SoundcloudBrowser(Browser, util.InstanceTracker): background = False __librarian = None __filter = None name = _("Soundcloud Browser") accelerated_name = _("Sound_cloud") keys = ["Soundcloud"] priority = 30 uses_main_library = False headers = ("artist ~people title genre ~#length ~mtime ~bitrate date " "website comment ~rating " "~#playback_count ~#favoritings_count ~#likes_count").split() @enum class ModelIndex(int): TYPE, ICON_NAME, NAME, QUERY, ALWAYS_ENABLE = range(5) login_state = State.LOGGED_OUT STAR = [tag for tag in headers if not tag.startswith("~#")] @classmethod def _init(klass, library): klass.__librarian = library.librarian klass.filters = [ (_("Search"), (FilterType.SEARCH, Icons.EDIT_FIND, "", True)), # TODO: support for ~#rating=!None etc (#1940) (_("Favorites"), (FilterType.FAVORITES, Icons.FAVORITE, "#(rating = 1.0)", False)), (_("My tracks"), (FilterType.MINE, Icons.MEDIA_RECORD, "soundcloud_user_id=%s", False)), ] try: if klass.library: return except AttributeError: pass klass.api_client = SoundcloudApiClient() klass.library = SoundcloudLibrary(klass.api_client, app.player) @classmethod def _destroy(klass): klass.__librarian = None klass.filters = {} klass.library.destroy() klass.library = None def __inhibit(self): self.view.get_selection().handler_block(self.__changed_sig) def __uninhibit(self): self.view.get_selection().handler_unblock(self.__changed_sig) def __destroy(self, *args): print_d(f"Destroying Soundcloud Browser {self}") if not self.instances(): self._destroy() def __init__(self, library): print_d(f"Creating Soundcloud Browser {self}") super().__init__(spacing=12) self.set_orientation(Gtk.Orientation.VERTICAL) if not self.instances(): self._init(library) self._register_instance() self.connect('destroy', self.__destroy) self.connect('uri-received', self.__handle_incoming_uri) connect_destroy(self.api_client, 'authenticated', self.__on_authenticated) connect_destroy(self.library, 'changed', self.__changed) self.login_state = (State.LOGGED_IN if self.online else State.LOGGED_OUT) self._create_searchbar(self.library) vbox = Gtk.VBox() vbox.pack_start(self._create_footer(), False, False, 6) vbox.pack_start(self._create_category_widget(), True, True, 0) vbox.pack_start(self.create_login_button(), False, False, 6) vbox.show() pane = qltk.ConfigRHPaned("browsers", "soundcloud_pos", 0.4) pane.show() pane.pack1(vbox, resize=False, shrink=False) self._songs_box = songs_box = Gtk.VBox(spacing=6) songs_box.pack_start(self._searchbox, False, True, 0) songs_box.show() pane.pack2(songs_box, resize=True, shrink=False) self.pack_start(pane, True, True, 0) self.show() def Menu(self, songs, library, items): return SongsMenu(library, songs, download=True, items=items) @property def online(self): return self.api_client.online def _create_footer(self): hbox = Gtk.HBox() button = Gtk.Button(always_show_image=True, relief=Gtk.ReliefStyle.NONE) button.connect('clicked', lambda _: website(SITE_URL)) button.set_tooltip_text(_("Go to %s" % SITE_URL)) button.add(self._logo_image) hbox.pack_start(button, True, True, 6) hbox.show_all() return hbox def _create_searchbar(self, library): completion = LibraryTagCompletion(library) self.accelerators = Gtk.AccelGroup() search = SearchBarBox(completion=completion, validator=SoundcloudQuery.validator, accel_group=self.accelerators, timeout=3000) self.__searchbar = search search.connect('query-changed', self.__query_changed) def focus(widget, *args): qltk.get_top_parent(widget).songlist.grab_focus() search.connect('focus-out', focus) self._searchbox = Align(search, left=0, right=6, top=6) self._searchbox.show_all() def update_connect_button(self): but = self.login_button but.set_sensitive(False) tooltip, icon = self._login_state_data[self.login_state] but.set_tooltip_text(tooltip) child = but.get_child() if child: print_d("Removing old image...") but.remove(child) but.add(icon if icon else Gtk.Label(tooltip)) but.get_child().show() but.set_sensitive(True) but.show() def create_login_button(self): def clicked_login(*args): # TODO: use a magic enum next() method, or similar state = self.login_state if state == State.LOGGED_IN: self.api_client.log_out() # Reset the selection, lest it get stuck... sel = self.view.get_selection() sel.unselect_all() first_path = self.view.get_model()[0].path.copy() self.view.set_cursor(first_path) sel.select_path(first_path) self._refresh_online_filters() self.login_state = State.LOGGED_OUT elif state == State.LOGGING_IN: dialog = EnterAuthCodeDialog(app.window) value = dialog.run(clipboard=True) if value: self.login_state = State.LOGGED_IN print_d("Got a user token value of '%s'" % value) self.api_client.get_tokens(value) elif state == State.LOGGED_OUT: self.api_client.authenticate_user() self.login_state = State.LOGGING_IN self.update_connect_button() hbox = Gtk.HBox() self.login_button = login = Gtk.Button(always_show_image=True, relief=Gtk.ReliefStyle.NONE) self.update_connect_button() login.connect('clicked', clicked_login) hbox.pack_start(login, True, False, 0) hbox.show_all() return hbox def _create_category_widget(self): scrolled_window = ScrolledWindow() scrolled_window.show() scrolled_window.set_shadow_type(Gtk.ShadowType.IN) self.view = view = RCMHintedTreeView() view.show() view.set_headers_visible(False) scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) scrolled_window.add(view) model = Gtk.ListStore(int, str, str, str, bool) filters = self.filters for (i, (name, data)) in enumerate(filters): filter_type, icon, query, always = data enabled = always model.append(row=[filter_type, icon, name, query, enabled]) def search_func(model, column, key, iter, data): return key.lower() not in model[iter][column].lower() view.set_search_column(self.ModelIndex.NAME) view.set_search_equal_func(search_func, None) column = Gtk.TreeViewColumn("Songs") column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) renderpb = Gtk.CellRendererPixbuf() renderpb.props.xpad = 6 renderpb.props.ypad = 6 column.pack_start(renderpb, False) column.add_attribute(renderpb, "icon-name", self.ModelIndex.ICON_NAME) render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) def cdf(column, cell, model, iter_, user_data): on = (self.login_state == State.LOGGED_IN or model[iter_][self.ModelIndex.ALWAYS_ENABLE]) cell.set_sensitive(on) column.set_cell_data_func(render, cdf) column.set_cell_data_func(renderpb, cdf) view.append_column(column) column.pack_start(render, True) column.add_attribute(render, "text", self.ModelIndex.NAME) view.set_model(model) selection = view.get_selection() def select_func(sel, model, path, value): return (self.login_state == State.LOGGED_IN or model[model.get_iter(path)][self.ModelIndex.ALWAYS_ENABLE]) selection.set_select_function(select_func) selection.select_iter(model.get_iter_first()) self._refresh_online_filters() self.__changed_sig = connect_destroy(selection, 'changed', DeferredSignal(self._on_select)) return scrolled_window def _on_select(self, sel): model, paths = sel.get_selected_rows() if not paths: return row = model[paths[0]] query_text = row[self.ModelIndex.QUERY] filter_type = row[self.ModelIndex.TYPE] if filter_type == FilterType.SEARCH: self.__searchbar.set_enabled() elif filter_type == FilterType.FAVORITES: print_d("Getting favorites...") self.api_client.get_favorites() self.__searchbar.set_enabled(False) elif filter_type == FilterType.MINE: print_d("Getting user tracks...") self.api_client.get_my_tracks() self.__searchbar.set_enabled(False) query_text = query_text % self.api_client.user_id self.__searchbar.set_text(query_text) self.activate() def pack(self, songpane): container = Gtk.VBox() container.add(self) self._songs_box.add(songpane) return container def unpack(self, container, songpane): self._songs_box.remove(songpane) container.remove(self) def __changed(self, library, songs): print_d("Updating view") self.activate() def __query_changed(self, bar, text, restore=False): try: self.__filter = SoundcloudQuery(text, self.STAR) self.library.query_with_refresh(self.__filter) except SoundcloudQuery.Error as e: print_d("Couldn't parse query: %s" % e) else: print_d("Got terms from query: %s" % (self.__filter.terms, )) if not restore: self.activate() def __get_selected_libraries(self): """Returns the libraries to search in depending on the filter selection""" return [self.library] def restore(self): filter_type = config.getint("browsers", "soundcloud_selection", FilterType.SEARCH) model = self.view.get_model() it = model.get_iter_first() while it: if model.get_value(it, 0) == filter_type: break it = model.iter_next(it) if filter_type == FilterType.SEARCH: self.__searchbar.set_enabled() self.__inhibit() self.view.get_selection().select_iter(it) self.__uninhibit() text = config.gettext("browsers", "query_text") self.__searchbar.set_text(text) self.__query_changed(None, text, restore=True) def __get_filter(self): return self.__filter or SoundcloudQuery("") def can_filter_text(self): return True def filter_text(self, text): model = self.view.get_model() it = model.get_iter_first() selected = False while it: typ = model.get_value(it, 0) if typ == FilterType.SEARCH: search_it = it elif ((typ == FilterType.FAVORITES and text == "#(rating = 1.0)") or (typ == FilterType.MINE and text == "soundcloud_user_id=%s" % self.api_client.user_id)): self.view.get_selection().select_iter(it) selected = True break it = model.iter_next(it) if not selected: # We don't want the selection to be cleared, so inhibit # the selection callback method self.__inhibit() self.view.get_selection().select_iter(search_it) self.__uninhibit() self.__searchbar.set_enabled() self.__searchbar.set_text(text) self.__query_changed(None, text) if SoundcloudQuery(text).is_parsable: self.activate() else: print_d("Not parsable: %s" % text) def get_filter_text(self): return self.__searchbar.get_text() def activate(self): print_d("Refreshing browser for query \"%r\"" % self.__filter) songs = self.library.query(self.get_filter_text()) self.songs_selected(songs) def active_filter(self, song): for lib in self.__get_selected_libraries(): if song in lib: filter_ = self.__get_filter() if filter_: return filter_.search(song) return True else: return False def save(self): text = self.__searchbar.get_text() config.settext("browsers", "query_text", text) self.api_client.save_auth() model, paths = self.view.get_selection().get_selected_rows() if paths: row = model[paths[0]] filter_type = row[self.ModelIndex.TYPE] config.set("browsers", "soundcloud_selection", filter_type) def _refresh_online_filters(self): model = self.view.get_model() for row in model: model.row_changed(row.path, model.get_iter(row.path)) def __handle_incoming_uri(self, obj, uri): if not PROCESS_QL_URLS: print_w("Processing of quodlibet:// URLs is disabled. (%s)" % uri) return uri = urlparse(uri) if (uri.scheme == 'quodlibet' and uri.netloc == 'callbacks' and uri.path == '/soundcloud'): try: code = parse_qs(uri.query)["code"][0] except IndexError: print_w("Malformed response in callback URI: %s" % uri) return print_d("Processing Soundcloud callback (%s)" % (uri, )) self.api_client.get_tokens(code) else: print_w("Unknown URL format (%s)" % (uri, )) def __on_authenticated(self, obj, data): name = data.username self.login_state = State.LOGGED_IN self.update_connect_button() self.activate() msg = Message(Gtk.MessageType.INFO, app.window, _("Connected"), _("Quod Libet is now connected, %s!") % name) msg.run() @cached_property def _logo_image(self): return WebImage( "https://developers.soundcloud.com/assets/logo_black.png", 104, 16) @cached_property def _login_state_data(self): """Login-state-based data for configuring actions (e.g. the button)""" return { State.LOGGED_IN: (_("Log out of %s") % SOUNDCLOUD_NAME, sc_btn_image('disconnect-l', 140, 29)), State.LOGGING_IN: (_("Enter code…"), None), State.LOGGED_OUT: (_("Log in to %s") % SOUNDCLOUD_NAME, sc_btn_image('connect-l', 124, 29)), }
class InternetRadio(Browser, util.InstanceTracker): __stations = None __fav_stations = None __librarian = None __filter = None name = _("Internet Radio") accelerated_name = _("_Internet Radio") keys = ["InternetRadio"] priority = 16 uses_main_library = False headers = "title artist ~people grouping genre website ~format " \ "channel-mode".split() TYPE, ICON_NAME, KEY, NAME = range(4) TYPE_FILTER, TYPE_ALL, TYPE_FAV, TYPE_SEP, TYPE_NOCAT = range(5) STAR = ["artist", "title", "website", "genre", "comment"] @classmethod def _init(klass, library): klass.__librarian = library.librarian klass.__stations = SongLibrary("iradio-remote") klass.__stations.load(STATIONS_ALL) klass.__fav_stations = SongLibrary("iradio") klass.__fav_stations.load(STATIONS_FAV) klass.filters = GenreFilter() @classmethod def _destroy(klass): if klass.__stations.dirty: klass.__stations.save() klass.__stations.destroy() klass.__stations = None if klass.__fav_stations.dirty: klass.__fav_stations.save() klass.__fav_stations.destroy() klass.__fav_stations = None klass.__librarian = None klass.filters = None def finalize(self, restored): if not restored: # Select "All Stations" by default def sel_all(row): return row[self.TYPE] == self.TYPE_ALL self.view.select_by_func(sel_all, one=True) def __inhibit(self): self.view.get_selection().handler_block(self.__changed_sig) def __uninhibit(self): self.view.get_selection().handler_unblock(self.__changed_sig) def __destroy(self, *args): if not self.instances(): self._destroy() def __init__(self, library): super(InternetRadio, self).__init__(spacing=12) self.set_orientation(Gtk.Orientation.VERTICAL) if not self.instances(): self._init(library) self._register_instance() self.connect('destroy', self.__destroy) completion = LibraryTagCompletion(self.__stations) self.accelerators = Gtk.AccelGroup() self.__searchbar = search = SearchBarBox(completion=completion, accel_group=self.accelerators) search.connect('query-changed', self.__filter_changed) menu = Gtk.Menu() new_item = MenuItem(_(u"_New Station…"), Icons.LIST_ADD) new_item.connect('activate', self.__add) menu.append(new_item) update_item = MenuItem(_("_Update Stations"), Icons.VIEW_REFRESH) update_item.connect('activate', self.__update) menu.append(update_item) menu.show_all() button = MenuButton(SymbolicIconImage(Icons.EMBLEM_SYSTEM, Gtk.IconSize.MENU), arrow=True) button.set_menu(menu) def focus(widget, *args): qltk.get_top_parent(widget).songlist.grab_focus() search.connect('focus-out', focus) # treeview scrolled_window = ScrolledWindow() scrolled_window.show() scrolled_window.set_shadow_type(Gtk.ShadowType.IN) self.view = view = AllTreeView() view.show() view.set_headers_visible(False) scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) scrolled_window.add(view) model = Gtk.ListStore(int, str, str, str) model.append( row=[self.TYPE_ALL, Icons.FOLDER, "__all", _("All Stations")]) model.append(row=[self.TYPE_SEP, Icons.FOLDER, "", ""]) #Translators: Favorite radio stations model.append( row=[self.TYPE_FAV, Icons.FOLDER, "__fav", _("Favorites")]) model.append(row=[self.TYPE_SEP, Icons.FOLDER, "", ""]) filters = self.filters for text, k in sorted([(filters.text(k), k) for k in filters.keys()]): model.append(row=[self.TYPE_FILTER, Icons.EDIT_FIND, k, text]) model.append( row=[self.TYPE_NOCAT, Icons.FOLDER, "nocat", _("No Category")]) def separator(model, iter, data): return model[iter][self.TYPE] == self.TYPE_SEP view.set_row_separator_func(separator, None) def search_func(model, column, key, iter, data): return key.lower() not in model[iter][column].lower() view.set_search_column(self.NAME) view.set_search_equal_func(search_func, None) column = Gtk.TreeViewColumn("genres") column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) renderpb = Gtk.CellRendererPixbuf() renderpb.props.xpad = 3 column.pack_start(renderpb, False) column.add_attribute(renderpb, "icon-name", self.ICON_NAME) render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) view.append_column(column) column.pack_start(render, True) column.add_attribute(render, "text", self.NAME) view.set_model(model) # selection selection = view.get_selection() selection.set_mode(Gtk.SelectionMode.MULTIPLE) self.__changed_sig = connect_destroy( selection, 'changed', util.DeferredSignal(lambda x: self.activate())) box = Gtk.HBox(spacing=6) box.pack_start(search, True, True, 0) box.pack_start(button, False, True, 0) self._searchbox = Align(box, left=0, right=6, top=6) self._searchbox.show_all() def qbar_response(infobar, response_id): if response_id == infobar.RESPONSE_LOAD: self.__update() self.qbar = QuestionBar() self.qbar.connect("response", qbar_response) if self._is_library_empty(): self.qbar.show() pane = qltk.ConfigRHPaned("browsers", "internetradio_pos", 0.4) pane.show() pane.pack1(scrolled_window, resize=False, shrink=False) songbox = Gtk.VBox(spacing=6) songbox.pack_start(self._searchbox, False, True, 0) self._songpane_container = Gtk.VBox() self._songpane_container.show() songbox.pack_start(self._songpane_container, True, True, 0) songbox.pack_start(self.qbar, False, True, 0) songbox.show() pane.pack2(songbox, resize=True, shrink=False) self.pack_start(pane, True, True, 0) self.show() def _is_library_empty(self): return not len(self.__stations) and not len(self.__fav_stations) def pack(self, songpane): container = Gtk.VBox() container.add(self) self._songpane_container.add(songpane) return container def unpack(self, container, songpane): self._songpane_container.remove(songpane) container.remove(self) def __update(self, *args): self.qbar.hide() copool.add(download_taglist, self.__update_done, cofuncid="radio-load", funcid="radio-load") def __update_done(self, stations): if not stations: print_w("Loading remote station list failed.") return # filter stations based on quality, listenercount def filter_stations(station): peak = station.get("~#listenerpeak", 0) if peak < 10: return False aac = "AAC" in station("~format") bitrate = station("~#bitrate", 50) if (aac and bitrate < 40) or (not aac and bitrate < 60): return False return True stations = filter(filter_stations, stations) # group them based on the title groups = {} for s in stations: key = s("~title~artist") groups.setdefault(key, []).append(s) # keep at most 2 URLs for each group stations = [] for key, sub in groups.items(): sub.sort(key=lambda s: s.get("~#listenerpeak", 0), reverse=True) stations.extend(sub[:2]) # only keep the ones in at least one category all_ = [self.filters.query(k) for k in self.filters.keys()] assert all_ anycat_filter = reduce(lambda x, y: x | y, all_) stations = list(filter(anycat_filter.search, stations)) # remove listenerpeak for s in stations: s.pop("~#listenerpeak", None) # update the libraries stations = dict(((s.key, s) for s in stations)) # don't add ones that are in the fav list for fav in self.__fav_stations.keys(): stations.pop(fav, None) # separate o, n = set(self.__stations.keys()), set(stations) to_add, to_change, to_remove = n - o, o & n, o - n del o, n # migrate stats to_change = [stations.pop(k) for k in to_change] for new in to_change: old = self.__stations[new.key] # clear everything except stats AudioFile.reload(old) # add new metadata except stats for k in (x for x in new.keys() if x not in MIGRATE): old[k] = new[k] to_add = [stations.pop(k) for k in to_add] to_remove = [self.__stations[k] for k in to_remove] self.__stations.remove(to_remove) self.__stations.changed(to_change) self.__stations.add(to_add) def __filter_changed(self, bar, text, restore=False): self.__filter = Query(text, self.STAR) if not restore: self.activate() def __get_selected_libraries(self): """Returns the libraries to search in depending on the filter selection""" selection = self.view.get_selection() model, rows = selection.get_selected_rows() types = [model[row][self.TYPE] for row in rows] libs = [self.__fav_stations] if types != [self.TYPE_FAV]: libs.append(self.__stations) return libs def __get_selection_filter(self): """Returns a filter object for the current selection or None if nothing should be filtered""" selection = self.view.get_selection() model, rows = selection.get_selected_rows() filter_ = None for row in rows: type_ = model[row][self.TYPE] if type_ == self.TYPE_FILTER: key = model[row][self.KEY] current_filter = self.filters.query(key) if current_filter: if filter_: filter_ |= current_filter else: filter_ = current_filter elif type_ == self.TYPE_NOCAT: # if notcat is selected, combine all filters, negate and merge all_ = [self.filters.query(k) for k in self.filters.keys()] nocat_filter = all_ and -reduce(lambda x, y: x | y, all_) if nocat_filter: if filter_: filter_ |= nocat_filter else: filter_ = nocat_filter elif type_ == self.TYPE_ALL: filter_ = None break return filter_ def unfilter(self): self.filter_text("") def __add_fav(self, songs): songs = [s for s in songs if s in self.__stations] type(self).__librarian.move(songs, self.__stations, self.__fav_stations) def __remove_fav(self, songs): songs = [s for s in songs if s in self.__fav_stations] type(self).__librarian.move(songs, self.__fav_stations, self.__stations) def __add(self, button): parent = qltk.get_top_parent(self) uri = (AddNewStation(parent).run(clipboard=True) or "").strip() if uri != "": self.__add_station(uri) def __add_station(self, uri): try: irfs = _get_stations_from(uri) except EnvironmentError as e: print_d("Got %s from %s" % (e, uri)) msg = ("Couldn't add URL: <b>%s</b>)\n\n<tt>%s</tt>" % (escape(str(e)), escape(uri))) ErrorMessage(None, _("Unable to add station"), msg).run() return if not irfs: ErrorMessage( None, _("No stations found"), _("No Internet radio stations were found at %s.") % util.escape(uri)).run() return irfs = set(irfs) - set(self.__fav_stations) if not irfs: WarningMessage( None, _("Unable to add station"), _("All stations listed are already in your library.")).run() if irfs: self.__fav_stations.add(irfs) def Menu(self, songs, library, items): in_fav = False in_all = False for song in songs: if song in self.__fav_stations: in_fav = True elif song in self.__stations: in_all = True if in_fav and in_all: break iradio_items = [] button = MenuItem(_("Add to Favorites"), Icons.LIST_ADD) button.set_sensitive(in_all) connect_obj(button, 'activate', self.__add_fav, songs) iradio_items.append(button) button = MenuItem(_("Remove from Favorites"), Icons.LIST_REMOVE) button.set_sensitive(in_fav) connect_obj(button, 'activate', self.__remove_fav, songs) iradio_items.append(button) items.append(iradio_items) menu = SongsMenu(self.__librarian, songs, playlists=False, remove=True, queue=False, items=items) return menu def restore(self): text = config.gettext("browsers", "query_text") self.__searchbar.set_text(text) if Query(text).is_parsable: self.__filter_changed(self.__searchbar, text, restore=True) keys = config.get("browsers", "radio").splitlines() def select_func(row): return row[self.TYPE] != self.TYPE_SEP and row[self.KEY] in keys self.__inhibit() view = self.view if not view.select_by_func(select_func): for row in view.get_model(): if row[self.TYPE] == self.TYPE_FAV: view.set_cursor(row.path) break self.__uninhibit() def __get_filter(self): filter_ = self.__get_selection_filter() text_filter = self.__filter or Query("") if filter_: filter_ &= text_filter else: filter_ = text_filter return filter_ def can_filter_text(self): return True def filter_text(self, text): self.__searchbar.set_text(text) if Query(text).is_parsable: self.__filter_changed(self.__searchbar, text) self.activate() def get_filter_text(self): return self.__searchbar.get_text() def activate(self): filter_ = self.__get_filter() libs = self.__get_selected_libraries() songs = filter_.filter(itertools.chain(*libs)) self.songs_selected(songs) def active_filter(self, song): for lib in self.__get_selected_libraries(): if song in lib: break else: return False filter_ = self.__get_filter() if filter_: return filter_.search(song) return True def save(self): text = self.__searchbar.get_text() config.settext("browsers", "query_text", text) selection = self.view.get_selection() model, rows = selection.get_selected_rows() names = filter(None, [model[row][self.KEY] for row in rows]) config.set("browsers", "radio", "\n".join(names)) def scroll(self, song): # nothing we care about if song not in self.__stations and song not in self.__fav_stations: return path = None for row in self.view.get_model(): if row[self.TYPE] == self.TYPE_FILTER: if self.filters.query(row[self.KEY]).search(song): path = row.path break else: # in case nothing matches, select all path = (0, ) self.view.set_cursor(path) self.view.scroll_to_cell(path, use_align=True, row_align=0.5) def status_text(self, count, time=None): return numeric_phrase("%(count)d station", "%(count)d stations", count, 'count')
class InternetRadio(Browser, util.InstanceTracker): __stations = None __fav_stations = None __librarian = None __filter = None name = _("Internet Radio") accelerated_name = _("_Internet Radio") keys = ["InternetRadio"] priority = 16 uses_main_library = False headers = "title artist ~people grouping genre website ~format " "channel-mode".split() TYPE, ICON_NAME, KEY, NAME = range(4) TYPE_FILTER, TYPE_ALL, TYPE_FAV, TYPE_SEP, TYPE_NOCAT = range(5) STAR = ["artist", "title", "website", "genre", "comment"] @classmethod def _init(klass, library): klass.__librarian = library.librarian klass.__stations = SongLibrary("iradio-remote") klass.__stations.load(STATIONS_ALL) klass.__fav_stations = SongLibrary("iradio") klass.__fav_stations.load(STATIONS_FAV) klass.filters = GenreFilter() @classmethod def _destroy(klass): if klass.__stations.dirty: klass.__stations.save() klass.__stations.destroy() klass.__stations = None if klass.__fav_stations.dirty: klass.__fav_stations.save() klass.__fav_stations.destroy() klass.__fav_stations = None klass.__librarian = None klass.filters = None def __inhibit(self): self.view.get_selection().handler_block(self.__changed_sig) def __uninhibit(self): self.view.get_selection().handler_unblock(self.__changed_sig) def __destroy(self, *args): if not self.instances(): self._destroy() def __init__(self, library): super(InternetRadio, self).__init__(spacing=12) self.set_orientation(Gtk.Orientation.VERTICAL) if not self.instances(): self._init(library) self._register_instance() self.connect("destroy", self.__destroy) completion = LibraryTagCompletion(self.__stations) self.accelerators = Gtk.AccelGroup() self.__searchbar = search = SearchBarBox(completion=completion, accel_group=self.accelerators) search.connect("query-changed", self.__filter_changed) menu = Gtk.Menu() new_item = MenuItem(_(u"_New Station…"), Icons.LIST_ADD) new_item.connect("activate", self.__add) menu.append(new_item) update_item = MenuItem(_("_Update Stations"), Icons.VIEW_REFRESH) update_item.connect("activate", self.__update) menu.append(update_item) menu.show_all() button = MenuButton(SymbolicIconImage(Icons.EMBLEM_SYSTEM, Gtk.IconSize.MENU), arrow=True) button.set_menu(menu) def focus(widget, *args): qltk.get_top_parent(widget).songlist.grab_focus() search.connect("focus-out", focus) # treeview scrolled_window = ScrolledWindow() scrolled_window.show() scrolled_window.set_shadow_type(Gtk.ShadowType.IN) self.view = view = AllTreeView() view.show() view.set_headers_visible(False) scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) scrolled_window.add(view) model = Gtk.ListStore(int, str, str, str) model.append(row=[self.TYPE_ALL, Icons.FOLDER, "__all", _("All Stations")]) model.append(row=[self.TYPE_SEP, Icons.FOLDER, "", ""]) # Translators: Favorite radio stations model.append(row=[self.TYPE_FAV, Icons.FOLDER, "__fav", _("Favorites")]) model.append(row=[self.TYPE_SEP, Icons.FOLDER, "", ""]) filters = self.filters for text, k in sorted([(filters.text(k), k) for k in filters.keys()]): model.append(row=[self.TYPE_FILTER, Icons.EDIT_FIND, k, text]) model.append(row=[self.TYPE_NOCAT, Icons.FOLDER, "nocat", _("No Category")]) def separator(model, iter, data): return model[iter][self.TYPE] == self.TYPE_SEP view.set_row_separator_func(separator, None) def search_func(model, column, key, iter, data): return key.lower() not in model[iter][column].lower() view.set_search_column(self.NAME) view.set_search_equal_func(search_func, None) column = Gtk.TreeViewColumn("genres") column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) renderpb = Gtk.CellRendererPixbuf() renderpb.props.xpad = 3 column.pack_start(renderpb, False) column.add_attribute(renderpb, "icon-name", self.ICON_NAME) render = Gtk.CellRendererText() render.set_property("ellipsize", Pango.EllipsizeMode.END) view.append_column(column) column.pack_start(render, True) column.add_attribute(render, "text", self.NAME) view.set_model(model) # selection selection = view.get_selection() selection.set_mode(Gtk.SelectionMode.MULTIPLE) self.__changed_sig = connect_destroy(selection, "changed", util.DeferredSignal(lambda x: self.activate())) box = Gtk.HBox(spacing=6) box.pack_start(search, True, True, 0) box.pack_start(button, False, True, 0) self._searchbox = Align(box, left=0, right=6, top=6) self._searchbox.show_all() def qbar_response(infobar, response_id): if response_id == infobar.RESPONSE_LOAD: self.__update() self.qbar = QuestionBar() self.qbar.connect("response", qbar_response) if self._is_library_empty(): self.qbar.show() pane = qltk.ConfigRHPaned("browsers", "internetradio_pos", 0.4) pane.show() pane.pack1(scrolled_window, resize=False, shrink=False) songbox = Gtk.VBox(spacing=6) songbox.pack_start(self._searchbox, False, True, 0) self._songpane_container = Gtk.VBox() self._songpane_container.show() songbox.pack_start(self._songpane_container, True, True, 0) songbox.pack_start(self.qbar, False, True, 0) songbox.show() pane.pack2(songbox, resize=True, shrink=False) self.pack_start(pane, True, True, 0) self.show() def _is_library_empty(self): return not len(self.__stations) and not len(self.__fav_stations) def pack(self, songpane): container = Gtk.VBox() container.add(self) self._songpane_container.add(songpane) return container def unpack(self, container, songpane): self._songpane_container.remove(songpane) container.remove(self) def __update(self, *args): self.qbar.hide() copool.add(download_taglist, self.__update_done, cofuncid="radio-load", funcid="radio-load") def __update_done(self, stations): if not stations: print_w("Loading remote station list failed.") return # filter stations based on quality, listenercount def filter_stations(station): peak = station.get("~#listenerpeak", 0) if peak < 10: return False aac = "AAC" in station("~format") bitrate = station("~#bitrate", 50) if (aac and bitrate < 40) or (not aac and bitrate < 60): return False return True stations = filter(filter_stations, stations) # group them based on the title groups = {} for s in stations: key = s("~title~artist") groups.setdefault(key, []).append(s) # keep at most 2 URLs for each group stations = [] for key, sub in groups.iteritems(): sub.sort(key=lambda s: s.get("~#listenerpeak", 0), reverse=True) stations.extend(sub[:2]) # only keep the ones in at least one category all_ = [self.filters.query(k) for k in self.filters.keys()] assert all_ anycat_filter = reduce(lambda x, y: x | y, all_) stations = filter(anycat_filter.search, stations) # remove listenerpeak for s in stations: s.pop("~#listenerpeak", None) # update the libraries stations = dict(((s.key, s) for s in stations)) # don't add ones that are in the fav list for fav in self.__fav_stations.iterkeys(): stations.pop(fav, None) # separate o, n = set(self.__stations.iterkeys()), set(stations) to_add, to_change, to_remove = n - o, o & n, o - n del o, n # migrate stats to_change = [stations.pop(k) for k in to_change] for new in to_change: old = self.__stations[new.key] # clear everything except stats AudioFile.reload(old) # add new metadata except stats for k in (x for x in new.iterkeys() if x not in MIGRATE): old[k] = new[k] to_add = [stations.pop(k) for k in to_add] to_remove = [self.__stations[k] for k in to_remove] self.__stations.remove(to_remove) self.__stations.changed(to_change) self.__stations.add(to_add) def __filter_changed(self, bar, text, restore=False): self.__filter = Query(text, self.STAR) if not restore: self.activate() def __get_selected_libraries(self): """Returns the libraries to search in depending on the filter selection""" selection = self.view.get_selection() model, rows = selection.get_selected_rows() types = [model[row][self.TYPE] for row in rows] libs = [self.__fav_stations] if types != [self.TYPE_FAV]: libs.append(self.__stations) return libs def __get_selection_filter(self): """Retuns a filter object for the current selection or None if nothing should be filtered""" selection = self.view.get_selection() model, rows = selection.get_selected_rows() filter_ = None for row in rows: type_ = model[row][self.TYPE] if type_ == self.TYPE_FILTER: key = model[row][self.KEY] current_filter = self.filters.query(key) if current_filter: if filter_: filter_ |= current_filter else: filter_ = current_filter elif type_ == self.TYPE_NOCAT: # if notcat is selected, combine all filters, negate and merge all_ = [self.filters.query(k) for k in self.filters.keys()] nocat_filter = all_ and -reduce(lambda x, y: x | y, all_) if nocat_filter: if filter_: filter_ |= nocat_filter else: filter_ = nocat_filter elif type_ == self.TYPE_ALL: filter_ = None break return filter_ def __add_fav(self, songs): songs = [s for s in songs if s in self.__stations] type(self).__librarian.move(songs, self.__stations, self.__fav_stations) def __remove_fav(self, songs): songs = [s for s in songs if s in self.__fav_stations] type(self).__librarian.move(songs, self.__fav_stations, self.__stations) def __add(self, button): parent = qltk.get_top_parent(self) uri = (AddNewStation(parent).run(clipboard=True) or "").strip() if uri != "": self.__add_station(uri) def __add_station(self, uri): irfs = add_station(uri) if not irfs: qltk.ErrorMessage( None, _("No stations found"), _("No Internet radio stations were found at %s.") % util.escape(uri) ).run() return irfs = filter(lambda station: station not in self.__fav_stations, irfs) if not irfs: qltk.WarningMessage( None, _("Unable to add station"), _("All stations listed are already in your library.") ).run() if irfs: self.__fav_stations.add(irfs) def Menu(self, songs, library, items): in_fav = False in_all = False for song in songs: if song in self.__fav_stations: in_fav = True elif song in self.__stations: in_all = True if in_fav and in_all: break iradio_items = [] button = MenuItem(_("Add to Favorites"), Icons.LIST_ADD) button.set_sensitive(in_all) connect_obj(button, "activate", self.__add_fav, songs) iradio_items.append(button) button = MenuItem(_("Remove from Favorites"), Icons.LIST_REMOVE) button.set_sensitive(in_fav) connect_obj(button, "activate", self.__remove_fav, songs) iradio_items.append(button) items.append(iradio_items) menu = SongsMenu(self.__librarian, songs, playlists=False, remove=True, queue=False, devices=False, items=items) return menu def restore(self): text = config.get("browsers", "query_text").decode("utf-8") self.__searchbar.set_text(text) if Query.is_parsable(text): self.__filter_changed(self.__searchbar, text, restore=True) keys = config.get("browsers", "radio").splitlines() def select_func(row): return row[self.TYPE] != self.TYPE_SEP and row[self.KEY] in keys self.__inhibit() view = self.view if not view.select_by_func(select_func): for row in view.get_model(): if row[self.TYPE] == self.TYPE_FAV: view.set_cursor(row.path) break self.__uninhibit() def __get_filter(self): filter_ = self.__get_selection_filter() text_filter = self.__filter or Query("") if filter_: filter_ &= text_filter else: filter_ = text_filter return filter_ def can_filter_text(self): return True def filter_text(self, text): self.__searchbar.set_text(text) if Query.is_parsable(text): self.__filter_changed(self.__searchbar, text) self.activate() def get_filter_text(self): return self.__searchbar.get_text() def activate(self): filter_ = self.__get_filter() libs = self.__get_selected_libraries() songs = filter_.filter(itertools.chain(*libs)) self.songs_selected(songs) def active_filter(self, song): for lib in self.__get_selected_libraries(): if song in lib: break else: return False filter_ = self.__get_filter() if filter_: return filter_.search(song) return True def save(self): text = self.__searchbar.get_text().encode("utf-8") config.set("browsers", "query_text", text) selection = self.view.get_selection() model, rows = selection.get_selected_rows() names = filter(None, [model[row][self.KEY] for row in rows]) config.set("browsers", "radio", "\n".join(names)) def scroll(self, song): # nothing we care about if song not in self.__stations and song not in self.__fav_stations: return path = None for row in self.view.get_model(): if row[self.TYPE] == self.TYPE_FILTER: if self.filters.query(row[self.KEY]).search(song): path = row.path break else: # in case nothing matches, select all path = (0,) self.view.set_cursor(path) self.view.scroll_to_cell(path, use_align=True, row_align=0.5) def statusbar(self, i): return ngettext("%(count)d station", "%(count)d stations", i)
def add_sidebar_to_layout(self, widget): print_d("Recreating sidebar") align = Align(widget, top=6, bottom=3) self.__paned.pack2(align, shrink=True) align.show_all()
class SoundcloudBrowser(Browser, util.InstanceTracker): background = False __librarian = None __filter = None name = _("Soundcloud Browser") accelerated_name = _("Sound_cloud") keys = ["Soundcloud"] priority = 30 uses_main_library = False headers = ("artist ~people title genre ~#length ~mtime ~bitrate date " "website comment ~rating " "~#playback_count ~#favoritings_count ~#likes_count").split() @enum class ModelIndex(int): TYPE, ICON_NAME, NAME, QUERY, ALWAYS_ENABLE = range(5) login_state = State.LOGGED_OUT STAR = [tag for tag in headers if not tag.startswith("~#")] @classmethod def _init(klass, library): klass.__librarian = library.librarian klass.filters = [ (_("Search"), (FilterType.SEARCH, Icons.EDIT_FIND, "", True)), # TODO: support for ~#rating=!None etc (#1940) (_("Favorites"), (FilterType.FAVORITES, Icons.FAVORITE, "#(rating = 1.0)", False)), (_("My tracks"), (FilterType.MINE, Icons.MEDIA_RECORD, "soundcloud_user_id=%s", False)), ] try: if klass.library: return except AttributeError: pass klass.api_client = SoundcloudApiClient() klass.library = SoundcloudLibrary(klass.api_client, app.player) @classmethod def _destroy(klass): klass.__librarian = None klass.filters = {} klass.library.destroy() klass.library = None def __inhibit(self): self.view.get_selection().handler_block(self.__changed_sig) def __uninhibit(self): self.view.get_selection().handler_unblock(self.__changed_sig) def __destroy(self, *args): self.api_client.disconnect(self.__auth_sig) if not self.instances(): self._destroy() def __init__(self, library): print_d("Creating Soundcloud Browser") super(SoundcloudBrowser, self).__init__(spacing=12) self.set_orientation(Gtk.Orientation.VERTICAL) if not self.instances(): self._init(library) self._register_instance() self.connect('destroy', self.__destroy) self.connect('uri-received', self.__handle_incoming_uri) self.__auth_sig = self.api_client.connect('authenticated', self.__on_authenticated) connect_destroy(self.library, 'changed', self.__changed) self.login_state = (State.LOGGED_IN if self.online else State.LOGGED_OUT) self._create_searchbar(self.library) vbox = Gtk.VBox() vbox.pack_start(self._create_footer(), False, False, 6) vbox.pack_start(self._create_category_widget(), True, True, 0) vbox.pack_start(self.create_login_button(), False, False, 6) vbox.show() pane = qltk.ConfigRHPaned("browsers", "soundcloud_pos", 0.4) pane.show() pane.pack1(vbox, resize=False, shrink=False) self._songs_box = songs_box = Gtk.VBox(spacing=6) songs_box.pack_start(self._searchbox, False, True, 0) songs_box.show() pane.pack2(songs_box, resize=True, shrink=False) self.pack_start(pane, True, True, 0) self.show() @property def online(self): return self.api_client.online def _create_footer(self): hbox = Gtk.HBox() button = Gtk.Button(always_show_image=True, relief=Gtk.ReliefStyle.NONE) button.connect('clicked', lambda _: website(SITE_URL)) button.set_tooltip_text(_("Go to %s" % SITE_URL)) button.add(self._logo_image) hbox.pack_start(button, True, True, 6) hbox.show_all() return hbox def _create_searchbar(self, library): completion = LibraryTagCompletion(library) self.accelerators = Gtk.AccelGroup() search = SearchBarBox(completion=completion, validator=SoundcloudQuery.validator, accel_group=self.accelerators, timeout=3000) self.__searchbar = search search.connect('query-changed', self.__query_changed) def focus(widget, *args): qltk.get_top_parent(widget).songlist.grab_focus() search.connect('focus-out', focus) self._searchbox = Align(search, left=0, right=6, top=6) self._searchbox.show_all() def update_connect_button(self): but = self.login_button but.set_sensitive(False) tooltip, icon = self._login_state_data[self.login_state] but.set_tooltip_text(tooltip) child = but.get_child() if child: print_d("Removing old image...") but.remove(child) but.add(icon if icon else Gtk.Label(tooltip)) but.get_child().show() but.set_sensitive(True) but.show() def create_login_button(self): def clicked_login(*args): # TODO: use a magic enum next() method, or similar state = self.login_state if state == State.LOGGED_IN: self.api_client.log_out() # Reset the selection, lest it get stuck... sel = self.view.get_selection() sel.unselect_all() first_path = self.view.get_model()[0].path.copy() self.view.set_cursor(first_path) sel.select_path(first_path) self._refresh_online_filters() self.login_state = State.LOGGED_OUT elif state == State.LOGGING_IN: dialog = EnterAuthCodeDialog(app.window) value = dialog.run(clipboard=True) if value: self.login_state = State.LOGGED_IN print_d("Got a user token value of '%s'" % value) self.api_client.get_token(value) elif state == State.LOGGED_OUT: self.api_client.authenticate_user() self.login_state = State.LOGGING_IN self.update_connect_button() hbox = Gtk.HBox() self.login_button = login = Gtk.Button(always_show_image=True, relief=Gtk.ReliefStyle.NONE) self.update_connect_button() login.connect('clicked', clicked_login) hbox.pack_start(login, True, False, 0) hbox.show_all() return hbox def _create_category_widget(self): scrolled_window = ScrolledWindow() scrolled_window.show() scrolled_window.set_shadow_type(Gtk.ShadowType.IN) self.view = view = RCMHintedTreeView() view.show() view.set_headers_visible(False) scrolled_window.set_policy( Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) scrolled_window.add(view) model = Gtk.ListStore(int, str, str, str, bool) filters = self.filters for (i, (name, data)) in enumerate(filters): filter_type, icon, query, always = data enabled = always model.append(row=[filter_type, icon, name, query, enabled]) def search_func(model, column, key, iter, data): return key.lower() not in model[iter][column].lower() view.set_search_column(self.ModelIndex.NAME) view.set_search_equal_func(search_func, None) column = Gtk.TreeViewColumn("Songs") column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) renderpb = Gtk.CellRendererPixbuf() renderpb.props.xpad = 6 renderpb.props.ypad = 6 column.pack_start(renderpb, False) column.add_attribute(renderpb, "icon-name", self.ModelIndex.ICON_NAME) render = Gtk.CellRendererText() render.set_property('ellipsize', Pango.EllipsizeMode.END) def cdf(column, cell, model, iter_, user_data): on = (self.login_state == State.LOGGED_IN or model[iter_][self.ModelIndex.ALWAYS_ENABLE]) cell.set_sensitive(on) column.set_cell_data_func(render, cdf) column.set_cell_data_func(renderpb, cdf) view.append_column(column) column.pack_start(render, True) column.add_attribute(render, "text", self.ModelIndex.NAME) view.set_model(model) selection = view.get_selection() def select_func(sel, model, path, value): return (self.login_state == State.LOGGED_IN or model[model.get_iter(path)][self.ModelIndex.ALWAYS_ENABLE]) selection.set_select_function(select_func) self._refresh_online_filters() self.__changed_sig = connect_destroy(selection, 'changed', DeferredSignal(self._on_select)) return scrolled_window def _on_select(self, sel): model, paths = sel.get_selected_rows() if not paths: return row = model[paths[0]] query_text = row[self.ModelIndex.QUERY] filter_type = row[self.ModelIndex.TYPE] if filter_type == FilterType.SEARCH: self.__searchbar.set_enabled() elif filter_type == FilterType.FAVORITES: print_d("Getting favorites...") self.api_client.get_favorites() self.__searchbar.set_enabled(False) elif filter_type == FilterType.MINE: print_d("Getting user tracks...") self.api_client.get_my_tracks() self.__searchbar.set_enabled(False) query_text = query_text % self.api_client.user_id self.__searchbar.set_text(query_text) self.activate() def pack(self, songpane): container = Gtk.VBox() container.add(self) self._songs_box.add(songpane) return container def unpack(self, container, songpane): self._songs_box.remove(songpane) container.remove(self) def __changed(self, library, songs): print_d("Updating view") self.activate() def __query_changed(self, bar, text, restore=False): try: self.__filter = SoundcloudQuery(text, self.STAR) self.library.query_with_refresh(text) except SoundcloudQuery.error as e: print_d("Couldn't parse query: %s" % e) else: print_d("Got terms from query: %s" % (self.__filter.terms,)) if not restore: self.activate() def __get_selected_libraries(self): """Returns the libraries to search in depending on the filter selection""" return [self.library] def restore(self): text = config.gettext("browsers", "query_text") self.__searchbar.set_text(text) self.__query_changed(None, text, restore=True) def __get_filter(self): return self.__filter or SoundcloudQuery("") def can_filter_text(self): return True def filter_text(self, text): self.__searchbar.set_text(text) if SoundcloudQuery(text).is_parsable: self.activate() else: print_d("Not parsable: %s" % text) def get_filter_text(self): return self.__searchbar.get_text() def activate(self): print_d("Refreshing browser for query \"%r\"" % self.__filter) songs = self.library.query(self.get_filter_text()) self.songs_selected(songs) def active_filter(self, song): for lib in self.__get_selected_libraries(): if song in lib: filter_ = self.__get_filter() if filter_: return filter_.search(song) return True else: return False def save(self): text = self.__searchbar.get_text() config.settext("browsers", "query_text", text) self.api_client.save_auth() def _refresh_online_filters(self): model = self.view.get_model() for row in model: model.row_changed(row.path, model.get_iter(row.path)) def __handle_incoming_uri(self, obj, uri): if not PROCESS_QL_URLS: print_w("Processing of quodlibet:// URLs is disabled. (%s)" % uri) return uri = urlparse(uri) if (uri.scheme == 'quodlibet' and uri.netloc == 'callbacks' and uri.path == '/soundcloud'): try: code = parse_qs(uri.query)["code"][0] except IndexError: print_w("Malformed response in callback URI: %s" % uri) return print_d("Processing Soundcloud callback (%s)" % (uri,)) self.api_client.get_token(code) else: print_w("Unknown URL format (%s)" % (uri,)) def __on_authenticated(self, obj, data): name = data.username self.login_state = State.LOGGED_IN self.update_connect_button() self._refresh_online_filters() msg = Message(Gtk.MessageType.INFO, app.window, _("Connected"), _("Quod Libet is now connected, <b>%s</b>!") % name) msg.run() @cached_property def _logo_image(self): return WebImage( "https://developers.soundcloud.com/assets/logo_black.png", 104, 16) @cached_property def _login_state_data(self): """Login-state-based data for configuring actions (e.g. the button)""" return { State.LOGGED_IN: (_("Log out of %s") % SOUNDCLOUD_NAME, sc_btn_image('disconnect-l', 140, 29)), State.LOGGING_IN: (_("Enter code…"), None), State.LOGGED_OUT: (_("Log in to %s") % SOUNDCLOUD_NAME, sc_btn_image('connect-l', 124, 29)), }