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_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 __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
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()
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()
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()
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()