Example #1
0
    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)
Example #2
0
    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)
Example #3
0
 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
Example #4
0
 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
Example #5
0
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)),
        }
Example #6
0
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')
Example #7
0
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)
Example #8
0
 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()
Example #9
0
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)),
        }
Example #10
0
 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()