Пример #1
0
 def __on_search_changed_thread(self):
     """
         Populate widget
     """
     self.__timeout = None
     self.__in_thread = True
     self.__search = NetworkSearch()
     self.__search.connect('item-found', self.__on_item_found)
     self.__stack.set_visible_child(self.__spinner)
     self.__spinner.start()
     t = Thread(target=self.__populate)
     t.daemon = True
     t.start()
Пример #2
0
 def __on_search_changed_thread(self):
     """
         Populate widget
     """
     self.__reset_search()
     from lollypop.search_local import LocalSearch
     from lollypop.search_network import NetworkSearch
     self.__timeout = None
     self.__lsearch = LocalSearch()
     self.__lsearch.connect('item-found', self.__on_local_item_found)
     if self.__need_network_search():
         self.__nsearch = NetworkSearch()
         self.__nsearch.connect('item-found', self.__on_network_item_found)
     self.__populate()
Пример #3
0
 def __on_search_changed_thread(self):
     """
         Populate widget
     """
     self.__reset_search()
     from lollypop.search_local import LocalSearch
     from lollypop.search_network import NetworkSearch
     self.__timeout = None
     self.__lsearch = LocalSearch()
     self.__lsearch.connect('item-found', self.__on_local_item_found)
     if self.__need_network_search():
         self.__nsearch = NetworkSearch()
         self.__nsearch.connect('item-found', self.__on_network_item_found)
     self.__populate()
Пример #4
0
 def __update_for_url(self, url):
     """
         Update charts for url
         @param url as str
     """
     if not get_network_available():
             return
     debug("LastfmCharts::__update_for_url(): %s" % (url))
     ids = self.__get_ids(url)
     position = len(ids)
     while ids:
         sleep(10)
         (track_name, artist_name) = ids.pop(0)
         search = NetworkSearch()
         search.connect('item-found', self.__on_item_found, position)
         search.do_tracks(track_name + " " + artist_name)
         if self.__stop:
             return
         position -= 1
Пример #5
0
class SearchPopover(Gtk.Popover):
    """
        Popover allowing user to search for tracks/albums
    """

    def __init__(self):
        """
            Init Popover
        """
        Gtk.Popover.__init__(self)
        self.set_position(Gtk.PositionType.BOTTOM)
        self.connect('map', self.__on_map)
        self.connect('unmap', self.__on_unmap)
        self.__timeout = None
        self.__current_search = ''
        self.__nsearch = None
        self.__lsearch = None
        self.__history = []

        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/SearchPopover.ui')

        self.__new_btn = builder.get_object('new_btn')
        self.__entry = builder.get_object('entry')

        self.__view = Gtk.ListBox()
        self.__view.set_sort_func(self.__sort_func)
        self.__view.connect("button-press-event", self.__on_button_press)
        self.__view.connect("row-activated", self.__on_row_activated)
        self.__view.set_selection_mode(Gtk.SelectionMode.SINGLE)
        self.__view.set_activate_on_single_click(True)
        self.__view.show()

        self.__spinner = builder.get_object('spinner')
        self.__stack = builder.get_object('stack')

        switch = builder.get_object('search-switch')
        if which("youtube-dl") is None:
            switch.set_sensitive(False)
            switch.set_tooltip_text(_("You need to install youtube-dl"))
        else:
            switch.set_state(Lp().settings.get_value('network-search'))
        builder.get_object('scrolled').add(self.__view)
        self.add(builder.get_object('widget'))
        # Connect here because we don't want previous switch.set_state()
        # to emit a signal on init
        builder.connect_signals(self)

    def set_text(self, text):
        """
            Set search text
        """
        self.__entry.set_text(text)

#######################
# PROTECTED           #
#######################
    def _on_new_btn_clicked(self, button):
        """
            Create a new playlist based on search
            @param button as Gtk.Button
        """
        t = Thread(target=self.__new_playlist)
        t.daemon = True
        t.start()

    def _on_search_changed(self, widget):
        """
            Timeout filtering
            @param widget as Gtk.TextEntry
        """
        self.__reset_search()
        if self.__timeout:
            GLib.source_remove(self.__timeout)
            self.__timeout = None

        self.__current_search = widget.get_text().strip()
        if self.__current_search != "":
            self.__new_btn.set_sensitive(True)
            self.__timeout = GLib.timeout_add(500,
                                              self.__on_search_changed_thread)
        else:
            self.__new_btn.set_sensitive(False)
            for child in self.__view.get_children():
                GLib.idle_add(child.destroy)

    def _on_state_set(self, switch, state):
        """
            Save state
            @param switch as Gtk.switch
            @param state as bool
        """
        Lp().settings.set_boolean('network-search', state)
        Lp().window.reload_view()
        if state:
            if Lp().charts is None:
                from lollypop.charts import Charts
                Lp().charts = Charts()
            Lp().charts.update()
        else:
            Lp().charts.stop()

#######################
# PRIVATE             #
#######################
    def __calculate_score(self, row):
        """
            Calculate score for row
            @param row as SearchRow
        """
        if row.score is not None:
            return
        # Network search score less
        if row.id is None:
            score = 0
            artists = row.artists
        else:
            score = 1
            artists = []
            for artist_id in row.artist_ids:
                artists.append(Lp().artists.get_name(artist_id))

        for item in self.__current_search.split():
            for artist in artists:
                if noaccents(artist.lower()).find(
                                                noaccents(item).lower()) != -1:
                    score += 2
                    if not row.is_track:
                        score += 1
            if noaccents(row.name).lower().find(
                                                noaccents(item).lower()) != -1:
                score += 1
                if row.is_track:
                    score += 1
        row.set_score(score)

    def __sort_func(self, row1, row2):
        """
            Sort rows
            @param row as SearchRow
            @param row as SearchRow
        """
        self.__calculate_score(row1)
        self.__calculate_score(row2)
        return row1.score < row2.score

    def __clear(self, rows):
        """
            Clear search view
            @param items as [SearchRow]
            @warning not thread safe
        """
        if rows:
            row = rows.pop(0)
            row.destroy()
            GLib.idle_add(self.__clear, rows)

    def __populate(self):
        """
            Populate searching items
            in db based on text entry current text
        """
        self.__stack.set_visible_child(self.__spinner)
        self.__spinner.start()
        self.__history = []
        # Network Search
        if self.__need_network_search():
            t = Thread(target=self.__nsearch.do, args=(self.__current_search,))
            t.daemon = True
            t.start()

        # Local Search
        search_items = [self.__current_search]
        for item in self.__current_search.split():
            if len(item) >= 3:
                search_items.append(item)
        GLib.idle_add(self.__clear, self.__view.get_children())
        t = Thread(target=self.__lsearch.do, args=(search_items,))
        t.daemon = True
        t.start()

    def __download_cover(self, uri, row):
        """
            Download row covers
            @param uri as str
            @param row as SearchRow
        """
        try:
            f = Gio.File.new_for_uri(uri)
            (status, data, tag) = f.load_contents(None)
            if status:
                stream = Gio.MemoryInputStream.new_from_data(data,
                                                             None)
                pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(
                                                   stream,
                                                   ArtSize.MEDIUM,
                                                   -1,
                                                   True,
                                                   None)
                GLib.idle_add(row.set_cover, pixbuf)
        except:
            pass

    def __populate_user_playlist_by_tracks(self, track_ids, track_id):
        """
            Set user playlist
            @param track_ids as [int]
            @param track id as int
            @thread safe
        """
        Lp().player.load(Track(track_id))
        Lp().player.populate_user_playlist_by_tracks(track_ids, [Type.SEARCH])

    def __play_search(self, object_id=None, is_track=True):
        """
            Play tracks based on search
            @param started object id as int
            @param is track as bool
        """
        track_ids = []
        track_id = None
        for child in self.__view.get_children():
            if child.is_track:
                track_ids.append(child.id)
            else:
                album_tracks = Lp().albums.get_track_ids(child.id)
                if not is_track and child.id == object_id and\
                        album_tracks:
                    track_id = album_tracks[0]
                for tid in album_tracks:
                    track_ids.append(tid)
        if track_ids:
            if object_id is not None and is_track:
                track_id = object_id
            elif track_id is None:
                track_id = track_ids[0]
            GLib.idle_add(self.__populate_user_playlist_by_tracks,
                          track_ids, track_id)

    def __new_playlist(self):
        """
            Create a new playlist based on search
        """
        tracks = []
        for child in self.__view.get_children():
            if child.is_track:
                tracks.append(Track(child.id))
            else:
                for track_id in Lp().albums.get_track_ids(
                                                       child.id, [],
                                                       child.artist_ids):
                    tracks.append(Track(track_id))
        if tracks:
            playlist_id = Lp().playlists.get_id(self.__current_search)
            if playlist_id == Type.NONE:
                Lp().playlists.add(self.__current_search)
                playlist_id = Lp().playlists.get_id(self.__current_search)
            Lp().playlists.add_tracks(playlist_id, tracks)

    def __reset_search(self):
        """
            Reset search object
        """
        self.__stack.set_visible_child(self.__new_btn)
        self.__spinner.stop()
        if self.__nsearch is not None:
            self.__nsearch.disconnect_by_func(self.__on_network_item_found)
            self.__nsearch.stop()
            self.__nsearch = None
        if self.__lsearch is not None:
            self.__lsearch.disconnect_by_func(self.__on_local_item_found)
            self.__lsearch.stop()
            self.__lsearch = None

    def __need_network_search(self):
        """
            Return True if network search needed
            @return True
        """
        return Lp().settings.get_value('network-search') and\
            which("youtube-dl") is not None

    def __on_local_item_found(self, search):
        """
            Add rows for internal results
            @param search as LocalSearch
        """
        if self.__lsearch != search:
            return
        if not search.items:
            if self.__lsearch.finished and\
                    (self.__nsearch is None or self.__nsearch.finished):
                self.__stack.set_visible_child(self.__new_btn)
                self.__spinner.stop()
            return
        item = search.items.pop(0)
        search_row = SearchRow(item)
        search_row.show()
        self.__view.add(search_row)

    def __on_network_item_found(self, search):
        """
            Add rows for internal results
            @param search as NetworkSearch
        """
        if self.__nsearch != search:
            return
        if not search.items:
            if self.__nsearch.finished and self.__lsearch.finished:
                self.__stack.set_visible_child(self.__new_btn)
                self.__spinner.stop()
            return
        item = search.items.pop(0)
        if item.exists_in_db():
            return
        if item.is_track:
            history = "♫" + item.name + item.artists[0]
        else:
            history = item.name + item.artists[0]
        if history not in self.__history:
            self.__history.append(history)
            search_row = SearchRow(item, False)
            search_row.show()
            self.__view.add(search_row)
            t = Thread(target=self.__download_cover,
                       args=(item.smallcover, search_row))
            t.daemon = True
            t.start()

    def __on_map(self, widget):
        """
            Disable global shortcuts and resize
            @param widget as Gtk.Widget
        """
        # FIXME Not needed with GTK >= 3.18
        Lp().window.enable_global_shorcuts(False)
        height = Lp().window.get_size()[1]
        self.set_size_request(400, height*0.7)

    def __on_unmap(self, widget):
        """
            Enable global shortcuts
            @param widget as Gtk.Widget
        """
        # FIXME Not needed with GTK >= 3.18
        Lp().window.enable_global_shorcuts(True)
        self.__reset_search()
        self.__stack.set_visible_child(self.__new_btn)
        self.__spinner.stop()

    def __on_search_changed_thread(self):
        """
            Populate widget
        """
        self.__reset_search()
        from lollypop.search_local import LocalSearch
        from lollypop.search_network import NetworkSearch
        self.__timeout = None
        self.__lsearch = LocalSearch()
        self.__lsearch.connect('item-found', self.__on_local_item_found)
        if self.__need_network_search():
            self.__nsearch = NetworkSearch()
            self.__nsearch.connect('item-found', self.__on_network_item_found)
        self.__populate()

    def __on_row_activated(self, widget, row):
        """
            Play searched item when selected
            @param widget as Gtk.ListBox
            @param row as SearchRow
        """
        if Lp().player.is_party or Lp().player.locked:
            # External track/album
            if row.id is None:
                pass
            elif row.is_track:
                if Lp().player.locked:
                    if row.id in Lp().player.get_queue():
                        Lp().player.del_from_queue(row.id)
                    else:
                        Lp().player.append_to_queue(row.id)
                    row.destroy()
                else:
                    Lp().player.load(Track(row.id))
            elif Gtk.get_minor_version() > 16:
                popover = AlbumPopover(row.id, [], [])
                popover.set_relative_to(row)
                popover.show()
            else:
                t = Thread(target=self.__play_search, args=(row.id,
                                                            row.is_track))
                t.daemon = True
                t.start()
        else:
            if row.id is None:
                row.play()
            else:
                t = Thread(target=self.__play_search, args=(row.id,
                                                            row.is_track))
                t.daemon = True
                t.start()

    def __on_button_press(self, widget, event):
        """
            Store pressed button
            @param widget as Gtk.ListBox
            @param event as Gdk.EventButton
        """
        rect = widget.get_allocation()
        rect.x = event.x
        rect.y = event.y
        rect.width = rect.height = 1
        row = widget.get_row_at_y(event.y)
        # Internal track/album
        if event.button != 1 and row.id is not None:
            if row.is_track:
                track = Track(row.id)
                popover = TrackMenuPopover(track,
                                           TrackMenu(track))
                popover.set_relative_to(widget)
                popover.set_pointing_to(rect)
                popover.show()
            else:
                popover = AlbumPopover(row.id, [],
                                       row.artist_ids)
                popover.set_relative_to(widget)
                popover.set_pointing_to(rect)
                popover.show()
Пример #6
0
class SearchPopover(Gtk.Popover):
    """
        Popover allowing user to search for tracks/albums
    """
    def __init__(self):
        """
            Init Popover
        """
        Gtk.Popover.__init__(self)
        self.set_position(Gtk.PositionType.BOTTOM)
        self.connect("map", self.__on_map)
        self.connect("unmap", self.__on_unmap)
        self.__timeout = None
        self.__current_search = ""
        self.__nsearch = None
        self.__lsearch = None
        self.__history = []

        builder = Gtk.Builder()
        builder.add_from_resource("/org/gnome/Lollypop/SearchPopover.ui")

        self.__new_btn = builder.get_object("new_btn")
        self.__entry = builder.get_object("entry")

        self.__view = Gtk.ListBox()
        self.__view.set_sort_func(self.__sort_func)
        self.__view.connect("button-press-event", self.__on_button_press)
        self.__view.connect("row-activated", self.__on_row_activated)
        self.__view.set_selection_mode(Gtk.SelectionMode.NONE)
        self.__view.set_activate_on_single_click(True)
        self.__view.show()

        self.__spinner = builder.get_object("spinner")
        self.__header_stack = builder.get_object("stack")

        self.__switch = builder.get_object("search-switch")
        if GLib.find_program_in_path("youtube-dl") is None:
            self.__switch.set_tooltip_text(_("You need to install youtube-dl"))
        else:
            self.__switch.set_state(Lp().settings.get_value("network-search"))

        self.__scrolled = builder.get_object("scrolled")
        self.__scrolled.add(self.__view)
        # Connect here because we don"t want previous switch.set_state()
        # to emit a signal on init
        builder.connect_signals(self)

        self.__stack = Gtk.Stack()
        self.__stack.set_transition_duration(250)
        self.__stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
        self.__stack.show()
        self.__stack.add_named(builder.get_object("widget"), "search")
        self.add(self.__stack)

    def set_text(self, text):
        """
            Set search text
        """
        self.__entry.set_text(text)

#######################
# PROTECTED           #
#######################

    def _on_new_btn_clicked(self, button):
        """
            Create a new playlist based on search
            @param button as Gtk.Button
        """
        t = Thread(target=self.__new_playlist)
        t.daemon = True
        t.start()

    def _on_search_changed(self, widget):
        """
            Timeout filtering
            @param widget as Gtk.TextEntry
        """
        self.__reset_search()
        if self.__timeout:
            GLib.source_remove(self.__timeout)
            self.__timeout = None

        self.__current_search = widget.get_text().strip()
        if self.__current_search != "":
            self.__new_btn.set_sensitive(True)
            self.__timeout = GLib.timeout_add(500,
                                              self.__on_search_changed_thread)
        else:
            self.__new_btn.set_sensitive(False)

    def _on_state_set(self, switch, state):
        """
            Save state
            @param switch as Gtk.switch
            @param state as bool
        """
        Lp().settings.set_boolean("network-search", state)
        GLib.idle_add(self._on_search_changed, self.__entry)

#######################
# PRIVATE             #
#######################

    def __enable_network_search(self):
        """
            True if shoud enable network search
            @return bool
        """
        return GLib.find_program_in_path("youtube-dl") is not None and\
            get_network_available()

    def __calculate_score(self, row):
        """
            Calculate score for row
            @param row as SearchRow
        """
        if row.score is not None:
            return
        # Network search score less
        if row.id is None:
            score = 0
            artists = row.artists
        else:
            score = 1
            artists = []
            for artist_id in row.artist_ids:
                artists.append(Lp().artists.get_name(artist_id))

        for item in self.__current_search.split():
            try:
                year = int(item)
                if year == int(row.year):
                    score += 2
            except:
                pass
            for artist in artists:
                if noaccents(artist.lower()).find(
                        noaccents(item).lower()) != -1:
                    score += 2
                    if not row.is_track:
                        score += 1
            if noaccents(row.name).lower().find(noaccents(item).lower()) != -1:
                score += 1
                if row.is_track:
                    score += 1
        row.set_score(score)

    def __sort_func(self, row1, row2):
        """
            Sort rows
            @param row as SearchRow
            @param row as SearchRow
        """
        self.__calculate_score(row1)
        self.__calculate_score(row2)
        return row1.score < row2.score

    def __clear(self, rows):
        """
            Clear search view
            @param items as [SearchRow]
            @warning not thread safe
        """
        if rows:
            row = rows.pop(0)
            self.__view.remove(row)
            row.destroy()
            GLib.idle_add(self.__clear, rows)

    def __populate(self):
        """
            Populate searching items
            in db based on text entry current text
        """
        self.__header_stack.set_visible_child(self.__spinner)
        self.__spinner.start()
        self.__history = []
        # Network Search
        if self.__need_network_search():
            t = Thread(target=self.__nsearch.do,
                       args=(self.__current_search, ))
            t.daemon = True
            t.start()

        # Local Search
        search_items = [self.__current_search]
        for item in self.__current_search.split():
            if len(item) >= 3:
                search_items.append(item)
        GLib.idle_add(self.__clear, self.__view.get_children())
        t = Thread(target=self.__lsearch.do, args=(search_items, ))
        t.daemon = True
        t.start()

    def __download_cover(self, uri, row):
        """
            Download row covers
            @param uri as str
            @param row as SearchRow
        """
        try:
            f = Lio.File.new_for_uri(uri)
            (status, data, tag) = f.load_contents(None)
            if status:
                bytes = GLib.Bytes(data)
                stream = Gio.MemoryInputStream.new_from_bytes(bytes)
                bytes.unref()
                pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(
                    stream, ArtSize.MEDIUM, -1, True, None)
                stream.close()
                GLib.idle_add(row.set_cover, pixbuf)
        except:
            pass

    def __populate_user_playlist_by_tracks(self, track_ids, track_id):
        """
            Set user playlist
            @param track_ids as [int]
            @param track id as int
            @thread safe
        """
        Lp().player.load(Track(track_id))
        Lp().player.populate_user_playlist_by_tracks(track_ids, [Type.SEARCH])

    def __new_playlist(self):
        """
            Create a new playlist based on search
        """
        tracks = []
        for child in self.__view.get_children():
            if child.is_track:
                tracks.append(Track(child.id))
            else:
                for track_id in Lp().albums.get_track_ids(
                        child.id, [], child.artist_ids):
                    tracks.append(Track(track_id))
        if tracks:
            playlist_id = Lp().playlists.get_id(self.__current_search)
            if playlist_id == Type.NONE:
                Lp().playlists.add(self.__current_search)
                playlist_id = Lp().playlists.get_id(self.__current_search)
            Lp().playlists.add_tracks(playlist_id, tracks)

    def __reset_search(self):
        """
            Reset search object
        """
        self.__header_stack.set_visible_child(self.__new_btn)
        self.__spinner.stop()
        if self.__nsearch is not None:
            self.__nsearch.disconnect_by_func(self.__on_network_item_found)
            self.__nsearch.stop()
            self.__nsearch = None
        if self.__lsearch is not None:
            self.__lsearch.disconnect_by_func(self.__on_local_item_found)
            self.__lsearch.stop()
            self.__lsearch = None

    def __need_network_search(self):
        """
            Return True if network search needed
            @return True
        """
        return Lp().settings.get_value("network-search") and\
            GLib.find_program_in_path("youtube-dl") is not None

    def __on_local_item_found(self, search):
        """
            Add rows for internal results
            @param search as LocalSearch
        """
        if self.__lsearch != search:
            return
        if not search.items:
            if self.__lsearch.finished and\
                    (self.__nsearch is None or self.__nsearch.finished):
                self.__header_stack.set_visible_child(self.__new_btn)
                self.__spinner.stop()
            return
        item = search.items.pop(0)
        search_row = SearchRow(item)
        search_row.show()
        self.__view.add(search_row)

    def __on_network_item_found(self, search):
        """
            Add rows for internal results
            @param search as NetworkSearch
        """
        if self.__nsearch != search:
            return
        if not search.items:
            if self.__nsearch.finished and self.__lsearch.finished:
                self.__header_stack.set_visible_child(self.__new_btn)
                self.__spinner.stop()
            return
        item = search.items.pop(0)
        if item.exists_in_db()[0]:
            return
        if item.is_track:
            history = "♫" + item.name + item.artists[0]
        else:
            history = item.name + item.artists[0]
        if history.lower() not in self.__history:
            self.__history.append(history.lower())
            search_row = SearchRow(item, False)
            search_row.show()
            self.__view.add(search_row)
            t = Thread(target=self.__download_cover,
                       args=(item.smallcover, search_row))
            t.daemon = True
            t.start()

    def __on_map(self, widget):
        """
            Disable global shortcuts and resize
            @param widget as Gtk.Widget
        """
        self.__switch.set_sensitive(self.__enable_network_search())
        # FIXME Not needed with GTK >= 3.18
        Lp().window.enable_global_shortcuts(False)
        height = Lp().window.get_size()[1]
        self.set_size_request(450, height * 0.7)

    def __on_unmap(self, widget):
        """
            Enable global shortcuts
            @param widget as Gtk.Widget
        """
        # FIXME Not needed with GTK >= 3.18
        Lp().window.enable_global_shortcuts(True)
        self.__reset_search()
        self.__header_stack.set_visible_child(self.__new_btn)
        self.__spinner.stop()

    def __on_search_changed_thread(self):
        """
            Populate widget
        """
        self.__reset_search()
        from lollypop.search_local import LocalSearch
        from lollypop.search_network import NetworkSearch
        self.__timeout = None
        self.__lsearch = LocalSearch()
        self.__lsearch.connect("item-found", self.__on_local_item_found)
        if self.__need_network_search():
            self.__nsearch = NetworkSearch()
            self.__nsearch.connect("item-found", self.__on_network_item_found)
        self.__populate()

    def __on_row_activated(self, widget, row):
        """
            Play searched item when selected
            @param widget as Gtk.ListBox
            @param row as SearchRow
        """
        if row.is_loading:
            return
        if row.id is None:
            row.on_activated(DbPersistent.NONE)
        elif row.is_track:
            # Add to queue, and play (so remove from queue)
            # Allow us to not change user current playlist
            if not Lp().player.is_party:
                Lp().player.insert_in_queue(row.id, 0, False)
            Lp().player.load(Track(row.id))
        else:
            album_view = AlbumBackView(row.id, [], [])
            album_view.connect("back-clicked", self.__on_back_clicked)
            album_view.show()
            self.__stack.add(album_view)
            self.__stack.set_visible_child(album_view)

    def __on_back_clicked(self, view):
        """
            Show search
        """
        search = self.__stack.get_child_by_name("search")
        self.__stack.set_visible_child(search)
        GLib.timeout_add(5000, view.destroy)

    def __on_button_press(self, widget, event):
        """
            Store pressed button
            @param widget as Gtk.ListBox
            @param event as Gdk.EventButton
        """
        rect = widget.get_allocation()
        rect.x = event.x
        rect.y = event.y
        rect.width = rect.height = 1
        row = widget.get_row_at_y(event.y)
        # Internal track/album
        if event.button != 1 and row.id is not None:
            if row.is_track:
                track = Track(row.id)
                popover = TrackMenuPopover(track, TrackMenu(track))
                popover.set_relative_to(widget)
                popover.set_pointing_to(rect)
                popover.show()
Пример #7
0
class SearchPopover(Gtk.Popover):
    """
        Popover allowing user to search for tracks/albums
    """

    def __init__(self):
        """
            Init Popover
        """
        Gtk.Popover.__init__(self)
        self.set_position(Gtk.PositionType.BOTTOM)
        self.connect('map', self.__on_map)
        self.connect('unmap', self.__on_unmap)
        self.__in_thread = False
        self.__stop_thread = False
        self.__timeout = None
        self.__current_search = ''
        self.__search = None

        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/SearchPopover.ui')
        builder.connect_signals(self)

        self.__new_btn = builder.get_object('new_btn')

        self.__view = Gtk.ListBox()
        self.__view.connect("button-press-event", self.__on_button_press)
        self.__view.connect("row-activated", self.__on_row_activated)
        self.__view.set_selection_mode(Gtk.SelectionMode.SINGLE)
        self.__view.set_activate_on_single_click(True)
        self.__view.show()

        self.__spinner = builder.get_object('spinner')
        self.__stack = builder.get_object('stack')

        switch = builder.get_object('search-switch')
        if which("youtube-dl") is None:
            switch.set_sensitive(False)
            switch.set_tooltip_text(_("You need to install youtube-dl"))
        else:
            switch.set_state(Lp().settings.get_value('network-search'))
        builder.get_object('scrolled').add(self.__view)
        self.add(builder.get_object('widget'))

#######################
# PROTECTED           #
#######################
    def _on_new_btn_clicked(self, button):
        """
            Create a new playlist based on search
            @param button as Gtk.Button
        """
        t = Thread(target=self.__new_playlist)
        t.daemon = True
        t.start()

    def _on_search_changed(self, widget):
        """
            Timeout filtering
            @param widget as Gtk.TextEntry
        """
        if self.__in_thread:
            self.__stop_thread = True
            self.__reset_search()
            self.__stack.set_visible_child(self.__new_btn)
            self.__spinner.stop()
            GLib.timeout_add(100, self._on_search_changed, widget)

        if self.__timeout:
            GLib.source_remove(self.__timeout)
            self.__timeout = None

        self.__current_search = widget.get_text().strip()
        if self.__current_search != "":
            self.__new_btn.set_sensitive(True)
            self.__timeout = GLib.timeout_add(100,
                                              self.__on_search_changed_thread)
        else:
            self.__reset_search()
            self.__stack.set_visible_child(self.__new_btn)
            self.__spinner.stop()
            self.__new_btn.set_sensitive(False)
            for child in self.__view.get_children():
                GLib.idle_add(child.destroy)

    def _on_state_set(self, switch, state):
        """
            Save state
            @param switch as Gtk.switch
            @param state as bool
        """
        Lp().settings.set_boolean('network-search', state)

#######################
# PRIVATE             #
#######################
    def __clear(self):
        """
            Clear search view
            @warning not thread safe
        """
        for child in self.__view.get_children():
            child.destroy()

    def __populate(self):
        """
            Populate searching items
            in db based on text entry current text
        """
        GLib.idle_add(self.__clear)

        # Network Search
        t = Thread(target=self.__network_search)
        t.daemon = True
        t.start()

        # Local search
        results = []
        added_album_ids = []
        added_track_ids = []
        search_items = [self.__current_search]
        # search_items += self.__current_search.split()
        for item in search_items:
            albums = []
            tracks_non_album_artist = []
            # Get all albums for all artists and non album_artist tracks
            for artist_id in Lp().artists.search(item):
                for album_id in Lp().albums.get_ids([artist_id], []):
                    if (album_id, artist_id) not in albums:
                        albums.append((album_id, artist_id))
                for track_id, track_name in Lp(
                                   ).tracks.get_as_non_album_artist(artist_id):
                    tracks_non_album_artist.append((track_id, track_name))

            for album_id, artist_id in albums:
                if album_id in added_album_ids:
                    continue
                search_item = SearchItem()
                search_item.id = album_id
                added_album_ids.append(album_id)
                search_item.is_track = False
                search_item.artist_ids = [artist_id]
                results.append(search_item)

            albums = Lp().albums.search(item)
            for album_id in albums:
                if album_id in added_album_ids:
                    continue
                search_item = SearchItem()
                search_item.id = album_id
                added_album_ids.append(album_id)
                search_item.is_track = False
                search_item.artist_ids = Lp().albums.get_artist_ids(album_id)
                results.append(search_item)

            for track_id, track_name in Lp().tracks.search(
                                               item) + tracks_non_album_artist:
                if track_id in added_track_ids:
                    continue
                search_item = SearchItem()
                search_item.id = track_id
                added_track_ids.append(track_id)
                search_item.is_track = True
                search_item.artist_ids = Lp().tracks.get_artist_ids(track_id)
                results.append(search_item)
        if not self.__stop_thread:
            GLib.idle_add(self.__add_rows_internal, results)
        else:
            self.__in_thread = False
            self.__stop_thread = False
            if not self.__need_network_search():
                self.__stack.set_visible_child(self.__new_btn)
                self.__spinner.stop()

    def __network_search(self):
        """
            Search on network
        """
        if self.__need_network_search():
            self.__search.do(self.__current_search)

    def __add_rows_internal(self, results):
        """
            Add rows for internal results
            @param results as array of SearchItem
        """
        if results:
            result = results.pop(0)
            search_row = SearchRow(result)
            search_row.show()
            self.__view.add(search_row)
            if self.__stop_thread:
                self.__in_thread = False
                self.__stop_thread = False
            else:
                GLib.idle_add(self.__add_rows_internal, results)
        else:
            self.__in_thread = False
            self.__stop_thread = False
            if not self.__need_network_search():
                self.__stack.set_visible_child(self.__new_btn)
                self.__spinner.stop()

    def __download_cover(self, uri, row):
        """
            Download row covers
            @param uri as str
            @param row as SearchRow
        """
        try:
            f = Gio.File.new_for_uri(uri)
            (status, data, tag) = f.load_contents(None)
            if status:
                stream = Gio.MemoryInputStream.new_from_data(data,
                                                             None)
                pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(
                                                   stream,
                                                   ArtSize.MEDIUM,
                                                   -1,
                                                   True,
                                                   None)
                GLib.idle_add(row.set_cover, pixbuf)
        except:
            pass

    def __populate_user_playlist_by_tracks(self, track_ids, track_id):
        """
            Set user playlist
            @param track_ids as [int]
            @param track id as int
            @thread safe
        """
        Lp().player.load(Track(track_id))
        Lp().player.populate_user_playlist_by_tracks(track_ids, [Type.SEARCH])

    def __play_search(self, object_id=None, is_track=True):
        """
            Play tracks based on search
            @param started object id as int
            @param is track as bool
        """
        track_ids = []
        track_id = None
        for child in self.__view.get_children():
            if child.is_track:
                track_ids.append(child.id)
            else:
                album_tracks = Lp().albums.get_track_ids(child.id)
                if not is_track and child.id == object_id and\
                        album_tracks:
                    track_id = album_tracks[0]
                for tid in album_tracks:
                    track_ids.append(tid)
        if track_ids:
            if object_id is not None and is_track:
                track_id = object_id
            elif track_id is None:
                track_id = track_ids[0]
            GLib.idle_add(self.__populate_user_playlist_by_tracks,
                          track_ids, track_id)

    def __new_playlist(self):
        """
            Create a new playlist based on search
        """
        tracks = []
        for child in self.__view.get_children():
            if child.is_track:
                tracks.append(Track(child.id))
            else:
                for track_id in Lp().albums.get_track_ids(
                                                       child.id, [],
                                                       child.artist_ids):
                    tracks.append(Track(track_id))
        if tracks:
            playlist_id = Lp().playlists.get_id(self.__current_search)
            if playlist_id == Type.NONE:
                Lp().playlists.add(self.__current_search)
                playlist_id = Lp().playlists.get_id(self.__current_search)
            Lp().playlists.add_tracks(playlist_id, tracks)

    def __reset_search(self):
        """
            Reset search object
        """
        if self.__search is not None:
            self.__search.disconnect_by_func(self.__on_item_found)
            self.__search.stop()
            self.__search = None

    def __need_network_search(self):
        """
            Return True if network search needed
            @return True
        """
        return Lp().settings.get_value('network-search') and\
            which("youtube-dl") is not None

    def __item_exists_in_db(self, item):
        """
            Search if item exists in db
            @return bool
        """
        artist_ids = []
        for artist in item.artists:
            artist_id = Lp().artists.get_id(artist)
            artist_ids.append(artist_id)
        if item.is_track:
            for track_id in Lp().tracks.get_ids_for_name(item.name):
                db_artist_ids = Lp().tracks.get_artist_ids(track_id)
                union = list(set(artist_ids) & set(db_artist_ids))
                if union == db_artist_ids:
                    return True
        else:
            album_ids = Lp().albums.get_ids(artist_ids, [])
            for album_id in album_ids:
                album_name = Lp().albums.get_name(album_id)
                if album_name == item.album_name:
                    return True
        return False

    def __on_item_found(self, search):
        """
            Add rows for internal results
            @param search as NetworkSearch
        """
        if self.__search != search:
            return
        if search.finished:
            self.__stack.set_visible_child(self.__new_btn)
            self.__spinner.stop()
        if not search.items:
            return
        item = search.items.pop(0)
        if self.__item_exists_in_db(item):
            return
        search_row = SearchRow(item, False)
        search_row.show()
        self.__view.add(search_row)
        t = Thread(target=self.__download_cover,
                   args=(item.smallcover, search_row))
        t.daemon = True
        t.start()

    def __on_map(self, widget):
        """
            Disable global shortcuts and resize
            @param widget as Gtk.Widget
        """
        # FIXME Not needed with GTK >= 3.18
        Lp().window.enable_global_shorcuts(False)
        height = Lp().window.get_size()[1]
        self.set_size_request(400, height*0.7)

    def __on_unmap(self, widget):
        """
            Enable global shortcuts
            @param widget as Gtk.Widget
        """
        # FIXME Not needed with GTK >= 3.18
        Lp().window.enable_global_shorcuts(True)
        self.__reset_search()
        self.__stack.set_visible_child(self.__new_btn)
        self.__spinner.stop()

    def __on_search_changed_thread(self):
        """
            Populate widget
        """
        self.__timeout = None
        self.__in_thread = True
        self.__search = NetworkSearch()
        self.__search.connect('item-found', self.__on_item_found)
        self.__stack.set_visible_child(self.__spinner)
        self.__spinner.start()
        t = Thread(target=self.__populate)
        t.daemon = True
        t.start()

    def __on_row_activated(self, widget, row):
        """
            Play searched item when selected
            @param widget as Gtk.ListBox
            @param row as SearchRow
        """
        if Lp().player.is_party or Lp().player.locked:
            # External track/album
            if row.id is None:
                pass
            elif row.is_track:
                if Lp().player.locked:
                    if row.id in Lp().player.get_queue():
                        Lp().player.del_from_queue(row.id)
                    else:
                        Lp().player.append_to_queue(row.id)
                    row.destroy()
                else:
                    Lp().player.load(Track(row.id))
            elif Gtk.get_minor_version() > 16:
                popover = AlbumPopover(row.id, [], [])
                popover.set_relative_to(row)
                popover.show()
            else:
                t = Thread(target=self.__play_search, args=(row.id,
                                                            row.is_track))
                t.daemon = True
                t.start()
        else:
            if row.id is None:
                row.play()
            else:
                t = Thread(target=self.__play_search, args=(row.id,
                                                            row.is_track))
                t.daemon = True
                t.start()

    def __on_button_press(self, widget, event):
        """
            Store pressed button
            @param widget as Gtk.ListBox
            @param event as Gdk.EventButton
        """
        rect = widget.get_allocation()
        rect.x = event.x
        rect.y = event.y
        rect.width = rect.height = 1
        row = widget.get_row_at_y(event.y)
        # Internal track/album
        if event.button != 1 and row.id is not None:
            if row.is_track:
                popover = TrackMenuPopover(row.id,
                                           TrackMenu(row.id))
                popover.set_relative_to(widget)
                popover.set_pointing_to(rect)
                popover.show()
            else:
                popover = AlbumPopover(row.id, [],
                                       row.artist_ids)
                popover.set_relative_to(widget)
                popover.set_pointing_to(rect)
                popover.show()
Пример #8
0
class SearchPopover(Gtk.Popover):
    """
        Popover allowing user to search for tracks/albums
    """

    def __init__(self):
        """
            Init Popover
        """
        Gtk.Popover.__init__(self)
        self.set_position(Gtk.PositionType.BOTTOM)
        self.connect('map', self.__on_map)
        self.connect('unmap', self.__on_unmap)
        self.__timeout = None
        self.__current_search = ''
        self.__nsearch = None
        self.__lsearch = None
        self.__history = []

        builder = Gtk.Builder()
        builder.add_from_resource('/org/gnome/Lollypop/SearchPopover.ui')

        self.__new_btn = builder.get_object('new_btn')
        self.__entry = builder.get_object('entry')

        self.__view = Gtk.ListBox()
        self.__view.connect("button-press-event", self.__on_button_press)
        self.__view.connect("row-activated", self.__on_row_activated)
        self.__view.set_selection_mode(Gtk.SelectionMode.NONE)
        self.__view.set_activate_on_single_click(True)
        self.__view.show()

        self.__spinner = builder.get_object('spinner')
        self.__header_stack = builder.get_object('stack')

        self.__switch = builder.get_object('search-switch')
        if GLib.find_program_in_path("youtube-dl") is None:
            self.__switch.set_tooltip_text(_("You need to install youtube-dl"))
        else:
            self.__switch.set_state(Lp().settings.get_value('network-search'))

        self.__scrolled = builder.get_object('scrolled')
        self.__scrolled.add(self.__view)
        # Connect here because we don't want previous switch.set_state()
        # to emit a signal on init
        builder.connect_signals(self)

        self.__stack = Gtk.Stack()
        self.__stack.set_transition_duration(250)
        self.__stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE)
        self.__stack.show()
        self.__stack.add_named(builder.get_object('widget'), "search")
        self.add(self.__stack)

    def set_text(self, text):
        """
            Set search text
        """
        self.__entry.set_text(text)

#######################
# PROTECTED           #
#######################
    def _on_new_btn_clicked(self, button):
        """
            Create a new playlist based on search
            @param button as Gtk.Button
        """
        t = Thread(target=self.__new_playlist)
        t.daemon = True
        t.start()

    def _on_search_changed(self, widget):
        """
            Timeout filtering
            @param widget as Gtk.TextEntry
        """
        self.__reset_search()
        if self.__timeout:
            GLib.source_remove(self.__timeout)
            self.__timeout = None

        self.__current_search = widget.get_text().strip()
        if self.__current_search != "":
            self.__new_btn.set_sensitive(True)
            self.__timeout = GLib.timeout_add(500,
                                              self.__on_search_changed_thread)
        else:
            self.__new_btn.set_sensitive(False)
            for child in self.__view.get_children():
                GLib.idle_add(child.destroy)

    def _on_state_set(self, switch, state):
        """
            Save state
            @param switch as Gtk.switch
            @param state as bool
        """
        Lp().settings.set_boolean('network-search', state)
        GLib.idle_add(self._on_search_changed, self.__entry)

#######################
# PRIVATE             #
#######################
    def __enable_network_search(self):
        """
            True if shoud enable network search
            @return bool
        """
        return GLib.find_program_in_path("youtube-dl") is not None and\
            get_network_available()

    def __calculate_score(self, row):
        """
            Calculate score for row
            @param row as SearchRow
        """
        if row.score is not None:
            return
        # Network search score less
        if row.id is None:
            score = 0
            artists = row.artists
        else:
            score = 1
            artists = []
            for artist_id in row.artist_ids:
                artists.append(Lp().artists.get_name(artist_id))

        for item in self.__current_search.split():
            try:
                year = int(item)
                if year == int(row.year):
                    score += 2
            except:
                pass
            for artist in artists:
                if noaccents(artist.lower()).find(
                                                noaccents(item).lower()) != -1:
                    score += 2
                    if not row.is_track:
                        score += 1
            if noaccents(row.name).lower().find(
                                                noaccents(item).lower()) != -1:
                score += 1
                if row.is_track:
                    score += 1
        row.set_score(score)

    def __sort_func(self, row1, row2):
        """
            Sort rows
            @param row as SearchRow
            @param row as SearchRow
        """
        self.__calculate_score(row1)
        self.__calculate_score(row2)
        return row1.score < row2.score

    def __clear(self, rows):
        """
            Clear search view
            @param items as [SearchRow]
            @warning not thread safe
        """
        if rows:
            row = rows.pop(0)
            row.destroy()
            GLib.idle_add(self.__clear, rows)

    def __populate(self):
        """
            Populate searching items
            in db based on text entry current text
        """
        self.__view.set_sort_func(None)
        self.__header_stack.set_visible_child(self.__spinner)
        self.__spinner.start()
        self.__history = []
        # Network Search
        if self.__need_network_search():
            t = Thread(target=self.__nsearch.do, args=(self.__current_search,))
            t.daemon = True
            t.start()

        # Local Search
        search_items = [self.__current_search]
        for item in self.__current_search.split():
            if len(item) >= 3:
                search_items.append(item)
        GLib.idle_add(self.__clear, self.__view.get_children())
        t = Thread(target=self.__lsearch.do, args=(search_items,))
        t.daemon = True
        t.start()

    def __download_cover(self, uri, row):
        """
            Download row covers
            @param uri as str
            @param row as SearchRow
        """
        try:
            f = Lio.File.new_for_uri(uri)
            (status, data, tag) = f.load_contents(None)
            if status:
                stream = Gio.MemoryInputStream.new_from_data(data,
                                                             None)
                pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(
                                                   stream,
                                                   ArtSize.MEDIUM,
                                                   -1,
                                                   True,
                                                   None)
                stream.close()
                GLib.idle_add(row.set_cover, pixbuf)
        except:
            pass

    def __populate_user_playlist_by_tracks(self, track_ids, track_id):
        """
            Set user playlist
            @param track_ids as [int]
            @param track id as int
            @thread safe
        """
        Lp().player.load(Track(track_id))
        Lp().player.populate_user_playlist_by_tracks(track_ids, [Type.SEARCH])

    def __new_playlist(self):
        """
            Create a new playlist based on search
        """
        tracks = []
        for child in self.__view.get_children():
            if child.is_track:
                tracks.append(Track(child.id))
            else:
                for track_id in Lp().albums.get_track_ids(
                                                       child.id, [],
                                                       child.artist_ids):
                    tracks.append(Track(track_id))
        if tracks:
            playlist_id = Lp().playlists.get_id(self.__current_search)
            if playlist_id == Type.NONE:
                Lp().playlists.add(self.__current_search)
                playlist_id = Lp().playlists.get_id(self.__current_search)
            Lp().playlists.add_tracks(playlist_id, tracks)

    def __reset_search(self):
        """
            Reset search object
        """
        self.__header_stack.set_visible_child(self.__new_btn)
        self.__spinner.stop()
        if self.__nsearch is not None:
            self.__nsearch.disconnect_by_func(self.__on_network_item_found)
            self.__nsearch.stop()
            self.__nsearch = None
        if self.__lsearch is not None:
            self.__lsearch.disconnect_by_func(self.__on_local_item_found)
            self.__lsearch.stop()
            self.__lsearch = None

    def __need_network_search(self):
        """
            Return True if network search needed
            @return True
        """
        return Lp().settings.get_value('network-search') and\
            GLib.find_program_in_path("youtube-dl") is not None

    def __on_local_item_found(self, search):
        """
            Add rows for internal results
            @param search as LocalSearch
        """
        if self.__lsearch != search:
            return
        if not search.items:
            if self.__lsearch.finished and\
                    (self.__nsearch is None or self.__nsearch.finished):
                self.__header_stack.set_visible_child(self.__new_btn)
                self.__spinner.stop()
                # Prevent jumping UI
                if self.__scrolled.get_vadjustment().get_value() == 0:
                    self.__view.set_sort_func(self.__sort_func)
            return
        item = search.items.pop(0)
        search_row = SearchRow(item)
        search_row.show()
        self.__view.add(search_row)

    def __on_network_item_found(self, search):
        """
            Add rows for internal results
            @param search as NetworkSearch
        """
        if self.__nsearch != search:
            return
        if not search.items:
            if self.__nsearch.finished and self.__lsearch.finished:
                self.__header_stack.set_visible_child(self.__new_btn)
                self.__spinner.stop()
                # Prevent jumping UI
                if self.__scrolled.get_vadjustment().get_value() == 0:
                    self.__view.set_sort_func(self.__sort_func)
            return
        item = search.items.pop(0)
        if item.exists_in_db()[0]:
            return
        if item.is_track:
            history = "♫" + item.name + item.artists[0]
        else:
            history = item.name + item.artists[0]
        if history.lower() not in self.__history:
            self.__history.append(history.lower())
            search_row = SearchRow(item, False)
            search_row.show()
            self.__view.add(search_row)
            t = Thread(target=self.__download_cover,
                       args=(item.smallcover, search_row))
            t.daemon = True
            t.start()

    def __on_map(self, widget):
        """
            Disable global shortcuts and resize
            @param widget as Gtk.Widget
        """
        self.__switch.set_sensitive(self.__enable_network_search())
        # FIXME Not needed with GTK >= 3.18
        Lp().window.enable_global_shortcuts(False)
        height = Lp().window.get_size()[1]
        self.set_size_request(450, height*0.7)

    def __on_unmap(self, widget):
        """
            Enable global shortcuts
            @param widget as Gtk.Widget
        """
        # FIXME Not needed with GTK >= 3.18
        Lp().window.enable_global_shortcuts(True)
        self.__reset_search()
        self.__header_stack.set_visible_child(self.__new_btn)
        self.__spinner.stop()

    def __on_search_changed_thread(self):
        """
            Populate widget
        """
        self.__reset_search()
        from lollypop.search_local import LocalSearch
        from lollypop.search_network import NetworkSearch
        self.__timeout = None
        self.__lsearch = LocalSearch()
        self.__lsearch.connect('item-found', self.__on_local_item_found)
        if self.__need_network_search():
            self.__nsearch = NetworkSearch()
            self.__nsearch.connect('item-found', self.__on_network_item_found)
        self.__populate()

    def __on_row_activated(self, widget, row):
        """
            Play searched item when selected
            @param widget as Gtk.ListBox
            @param row as SearchRow
        """
        if row.is_loading:
            return
        if row.id is None:
            row.on_activated(DbPersistent.NONE)
        elif row.is_track:
            # Add to queue, and play (so remove from queue)
            # Allow us to not change user current playlist
            if not Lp().player.is_party:
                Lp().player.insert_in_queue(row.id, 0, False)
            Lp().player.load(Track(row.id))
        else:
            album_view = AlbumBackView(row.id, [], [])
            album_view.connect('back-clicked', self.__on_back_clicked)
            album_view.show()
            self.__stack.add(album_view)
            self.__stack.set_visible_child(album_view)

    def __on_back_clicked(self, view):
        """
            Show search
        """
        search = self.__stack.get_child_by_name("search")
        self.__stack.set_visible_child(search)
        GLib.timeout_add(5000, view.destroy)

    def __on_button_press(self, widget, event):
        """
            Store pressed button
            @param widget as Gtk.ListBox
            @param event as Gdk.EventButton
        """
        rect = widget.get_allocation()
        rect.x = event.x
        rect.y = event.y
        rect.width = rect.height = 1
        row = widget.get_row_at_y(event.y)
        # Internal track/album
        if event.button != 1 and row.id is not None:
            if row.is_track:
                track = Track(row.id)
                popover = TrackMenuPopover(track,
                                           TrackMenu(track))
                popover.set_relative_to(widget)
                popover.set_pointing_to(rect)
                popover.show()