diff --git a/bin/mpdevil b/bin/mpdevil index c9da2ec..6d14bcd 100755 --- a/bin/mpdevil +++ b/bin/mpdevil @@ -1793,75 +1793,127 @@ class ArtistPopover(Gtk.Popover): # browser # ########### +class SearchThread(threading.Thread): + def __init__(self, client, search_entry, songs_window, hits_label, search_tag): + super().__init__(daemon=True) + self._client=client + self._search_entry=search_entry + self._songs_view=songs_window.get_treeview() + self._store=self._songs_view.get_model() + self._action_bar=songs_window.get_action_bar() + self._hits_label=hits_label + self._search_tag=search_tag + self._stop_flag=False + self._callback=None + + def set_callback(self, callback): + self._callback=callback + + def stop(self): + self._stop_flag=True + + def start(self): + self._songs_view.clear() + self._hits_label.set_text("") + self._action_bar.set_sensitive(False) + self._search_text=self._search_entry.get_text() + if self._search_text: + super().start() + else: + self._exit() + + def run(self): + hits=0 + stripe_size=1000 + songs=self._get_songs(0, stripe_size) + stripe_start=stripe_size + while songs: + hits+=len(songs) + if not self._append_songs(songs): + self._exit() + return + GLib.idle_add(self._search_entry.progress_pulse) + GLib.idle_add(self._hits_label.set_text, ngettext("{hits} hit", "{hits} hits", hits).format(hits=hits)) + stripe_end=stripe_start+stripe_size + songs=self._get_songs(stripe_start, stripe_end) + stripe_start=stripe_end + if hits > 0: + GLib.idle_add(self._action_bar.set_sensitive, True) + self._exit() + + def _exit(self): + def callback(): + self._search_entry.set_progress_fraction(0.0) + if self._callback is not None: + self._callback() + return False + GLib.idle_add(callback) + + @main_thread_function + def _get_songs(self, start, end): + if self._stop_flag: + return [] + else: + self._client.restrict_tagtypes("track", "title", "artist", "album") + songs=self._client.search(self._search_tag, self._search_text, "window", f"{start}:{end}") + self._client.tagtypes("all") + return songs + + @main_thread_function + def _append_songs(self, songs): + for song in songs: + if self._stop_flag: + return False + try: + int_track=int(song["track"][0]) + except ValueError: + int_track=0 + self._store.insert_with_valuesv(-1, range(7), [ + str(song["track"]), str(song["title"]), + str(song["artist"]), str(song["album"]), + str(song["duration"]), song["file"], + int_track + ]) + return True + class SearchWindow(Gtk.Box): def __init__(self, client): super().__init__(orientation=Gtk.Orientation.VERTICAL) self._client=client - self._stop_flag=False - self._done=True - self._pending=[] - # tag switcher + # widgets self._tag_combo_box=Gtk.ComboBoxText() - - # search entry self.search_entry=Gtk.SearchEntry() - - # label self._hits_label=Gtk.Label(xalign=1) - # store + # songs window # (track, title, artist, album, duration, file, sort track) self._store=Gtk.ListStore(str, str, str, str, str, str, int) self._store.set_default_sort_func(lambda *args: 0) - - # songs window self._songs_window=SongsWindow(self._client, self._store, 5) - - # action bar self._action_bar=self._songs_window.get_action_bar() - self._action_bar.set_sensitive(False) - - # songs view self._songs_view=self._songs_window.get_treeview() # columns renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True) renderer_text_ralign=Gtk.CellRendererText(xalign=1.0) + column_data=( + (_("No"), renderer_text_ralign, False, 0, 6), + (_("Title"), renderer_text, True, 1, 1), + (_("Artist"), renderer_text, True, 2, 2), + (_("Album"), renderer_text, True, 3, 3), + (_("Length"), renderer_text_ralign, False, 4, 4), + ) + for title, renderer, expand, text, sort in column_data: + column=Gtk.TreeViewColumn(title, renderer, text=text) + column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) + column.set_property("resizable", False) + column.set_property("expand", expand) + column.set_sort_column_id(sort) + self._songs_view.append_column(column) - column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0) - column_track.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - column_track.set_property("resizable", False) - self._songs_view.append_column(column_track) - - column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, text=1) - column_title.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - column_title.set_property("resizable", False) - column_title.set_property("expand", True) - self._songs_view.append_column(column_title) - - column_artist=Gtk.TreeViewColumn(_("Artist"), renderer_text, text=2) - column_artist.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - column_artist.set_property("resizable", False) - column_artist.set_property("expand", True) - self._songs_view.append_column(column_artist) - - column_album=Gtk.TreeViewColumn(_("Album"), renderer_text, text=3) - column_album.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - column_album.set_property("resizable", False) - column_album.set_property("expand", True) - self._songs_view.append_column(column_album) - - column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=4) - column_time.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - column_time.set_property("resizable", False) - self._songs_view.append_column(column_time) - - column_track.set_sort_column_id(6) - column_title.set_sort_column_id(1) - column_artist.set_sort_column_id(2) - column_album.set_sort_column_id(3) - column_time.set_sort_column_id(4) + # search thread + self._search_thread=SearchThread(self._client, self.search_entry, self._songs_window, self._hits_label, "any") # connect self.search_entry.connect("activate", self._search) @@ -1883,107 +1935,44 @@ class SearchWindow(Gtk.Box): self.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0) self.pack_start(self._songs_window, True, True, 0) - def _clear(self, *args): - if self._done: - self.search_entry.handler_block(self._search_entry_changed) - self._tag_combo_box.handler_block(self._tag_combo_box_changed) - self._songs_view.clear() - self.search_entry.set_text("") - self._tag_combo_box.remove_all() - self.search_entry.handler_unblock(self._search_entry_changed) - self._tag_combo_box.handler_unblock(self._tag_combo_box_changed) - elif not self._clear in self._pending: - self._stop_flag=True - self._pending.append(self._clear) - def _on_disconnected(self, *args): - self._tag_combo_box.set_sensitive(False) - self.search_entry.set_sensitive(False) - self._clear() + self._search_thread.stop() def _on_reconnected(self, *args): - if self._done: + def callback(): + self._action_bar.set_sensitive(False) + self._songs_view.clear() + self._hits_label.set_text("") + self.search_entry.handler_block(self._search_entry_changed) + self.search_entry.set_text("") + self.search_entry.handler_unblock(self._search_entry_changed) self._tag_combo_box.handler_block(self._tag_combo_box_changed) + self._tag_combo_box.remove_all() self._tag_combo_box.append_text(_("all tags")) for tag in self._client.tagtypes(): if not tag.startswith("MUSICBRAINZ"): self._tag_combo_box.append_text(tag) self._tag_combo_box.set_active(0) - self._tag_combo_box.set_sensitive(True) - self.search_entry.set_sensitive(True) self._tag_combo_box.handler_unblock(self._tag_combo_box_changed) - elif not self._on_reconnected in self._pending: - self._stop_flag=True - self._pending.append(self._on_reconnected) + if self._search_thread.is_alive(): + self._search_thread.set_callback(callback) + self._search_thread.stop() + else: + callback() def _search(self, *args): - if self._done: - self._done=False - self._songs_view.clear() - self._hits_label.set_text("") - self._action_bar.set_sensitive(False) - hits=0 - if self.search_entry.get_text(): - if self._tag_combo_box.get_active() == 0: - search_tag="any" - else: - search_tag=self._tag_combo_box.get_active_text() - search_text=self.search_entry.get_text() - stripe_size=1000 - self._client.restrict_tagtypes("track", "title", "artist", "album") - try: # client cloud meanwhile disconnect - songs=self._client.search(search_tag, search_text, "window", f"0:{stripe_size}") - except MPDBase.ConnectionError: - self._done_callback() - return - stripe_start=stripe_size - while songs: - hits+=len(songs) - for song in songs: - if self._stop_flag: - self._done_callback() - return - try: - int_track=int(song["track"][0]) - except ValueError: - int_track=0 - self._store.insert_with_valuesv(-1, range(7), [ - str(song["track"]), str(song["title"]), - str(song["artist"]), str(song["album"]), - str(song["duration"]), song["file"], - int_track - ]) - self.search_entry.progress_pulse() - self._hits_label.set_text(ngettext("{hits} hit", "{hits} hits", hits).format(hits=hits)) - while Gtk.events_pending(): - Gtk.main_iteration_do(True) - stripe_end=stripe_start+stripe_size - try: # client cloud meanwhile disconnect - songs=self._client.search(search_tag, search_text, "window", f"{stripe_start}:{stripe_end}") - except MPDBase.ConnectionError: - self._done_callback() - return - stripe_start+=stripe_size - self._client.tagtypes("all") - if hits > 0: - self._action_bar.set_sensitive(True) - self._done_callback() - elif not self._search in self._pending: - self._stop_flag=True - self._pending.append(self._search) - - def _done_callback(self, *args): - self.search_entry.set_progress_fraction(0.0) - self._stop_flag=False - self._done=True - pending=self._pending - self._pending=[] - for p in pending: - try: - p() - except: - pass - return False + def callback(): + if self._tag_combo_box.get_active() == 0: + search_tag="any" + else: + search_tag=self._tag_combo_box.get_active_text() + self._search_thread=SearchThread(self._client, self.search_entry, self._songs_window, self._hits_label, search_tag) + self._search_thread.start() + if self._search_thread.is_alive(): + self._search_thread.set_callback(callback) + self._search_thread.stop() + else: + callback() def _on_search_entry_focus_event(self, widget, event, focus): app=self.get_toplevel().get_application()