From b554a971859a24668a8fccabbc8fa87426401f1d Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Fri, 8 Mar 2024 16:10:55 +0100 Subject: [PATCH] load channel & playlist browse screen content using separate threads NOTE: renders widgets on main thread, to not cause any segfaults --- melon/browse/channel.py | 184 ++++++++++++++++++++++++--------------- melon/browse/playlist.py | 117 ++++++++++++++++--------- 2 files changed, 191 insertions(+), 110 deletions(-) diff --git a/melon/browse/channel.py b/melon/browse/channel.py index 6f78a25..dd0beec 100644 --- a/melon/browse/channel.py +++ b/melon/browse/channel.py @@ -5,6 +5,7 @@ gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, Gdk, GLib from unidecode import unidecode from gettext import gettext as _ +import threading from melon.servers.utils import get_server_instance, get_servers_list from melon.servers.utils import pixbuf_from_url @@ -16,14 +17,25 @@ from melon.models import get_app_settings from melon.models import is_subscribed_to_channel, ensure_subscribed_to_channel, ensure_unsubscribed_from_channel class BrowseChannelScreen(Adw.NavigationPage): - def fetch_page(self, page=1): - feed_id = self.current_feed - cont = self.instance.get_channel_feed_content(self.channel_id, feed_id) + def display_page(self, cont): + """ + Display the channel feed page + """ app_conf = get_app_settings() + self.results.set_header_suffix(None) for res in cont: self.results.add(AdaptiveFeedItem(res, show_preview=app_conf["show_images_in_browse"])) - + def fetch_page(self, page=1): + """ + Fetch feed information + """ + feed_id = self.current_feed + cont = self.instance.get_channel_feed_content(self.channel.id, feed_id) + GLib.idle_add(self.display_page, cont) def change_feed(self, feed_id): + """ + change the feed and prepare layout + """ self.current_feed = feed_id if not self.results is None: self.box.remove(self.results) @@ -34,9 +46,17 @@ class BrowseChannelScreen(Adw.NavigationPage): feed_name = feed.name break self.results.set_title(feed_name) + spinner = Gtk.Spinner() + spinner.set_size_request(20, 20) + spinner.start() + self.results.set_header_suffix(spinner) self.results.set_vexpand(True) self.box.add(self.results) - self.fetch_page() + # fetch page on new thread + self.thread = threading.Thread(target=self.fetch_page) + self.thread.daemon = True + self.thread.start() + def change_feed_wrapper(self, feed_name): for feed in self.feeds: if feed.name == feed_name: @@ -44,53 +64,27 @@ class BrowseChannelScreen(Adw.NavigationPage): break def on_open_in_browser(self, arg): Gtk.UriLauncher.new(uri=self.channel.url).launch() - def __init__(self, server_id, channel_id, *args, **kwargs): - super().__init__(*args, **kwargs) - self.channel_id = channel_id - # get instance handle - server = get_servers_list()[server_id] - self.instance = get_server_instance(server) - # obtain channel information - channel = self.instance.get_channel_info(channel_id) - self.channel = channel - self.set_title(unidecode(channel.name)) - - self.header_bar = Adw.HeaderBar() - self.external_btn = IconButton("","modem-symbolic") - self.external_btn.connect("clicked", self.on_open_in_browser) - self.header_bar.pack_end(self.external_btn) - self.toolbar_view = Adw.ToolbarView() - self.toolbar_view.add_top_bar(self.header_bar) - - self.wrapper = Adw.Clamp() - self.scrollview = Gtk.ScrolledWindow() + def display_info(self, texture): + # base layout self.box = Adw.PreferencesPage() self.about = Adw.PreferencesGroup() - self.about.set_title(unidecode(channel.name).replace("&", "&")) - self.about.set_description(unidecode(channel.bio).replace("&", "&")) - self.results = None - - # we have to append all the neccessary elements here, - # because if there are no channel feeds this function will exit early - self.scrollview.set_child(self.box) - self.wrapper.set_child(self.scrollview) - self.toolbar_view.set_content(self.wrapper) - self.set_child(self.toolbar_view) self.box.add(self.about) + self.scrollview.set_child(self.box) + + # update meta + self.set_title(unidecode(self.channel.name)) + self.about.set_title(unidecode(self.channel.name).replace("&", "&")) + self.about.set_description(unidecode(self.channel.bio).replace("&", "&")) # display channel info - app_conf = get_app_settings() av = Adw.Avatar() av.set_size(64) - if app_conf["show_images_in_browse"]: - pixbuf = pixbuf_from_url(channel.avatar) - if not pixbuf is None: - texture = Gdk.Texture.new_for_pixbuf(pixbuf) - av.set_custom_image(texture) - else: - av.set_text(channel.name) + if not texture is None: + av.set_custom_image(texture) + else: + av.set_text(self.channel.name) self.about.set_header_suffix(av) # add (un)subscribe button @@ -100,42 +94,96 @@ class BrowseChannelScreen(Adw.NavigationPage): _("Add latest uploads to home feed"), PreferenceType.TOGGLE, False, - is_subscribed_to_channel(self.channel.server, self.channel_id) + is_subscribed_to_channel(self.channel.server, self.channel.id) ) sub_row = PreferenceRow(sub_pref) sub_row.set_callback(self.update_sub) self.about.add(sub_row.get_widget()) + def display_feedselect(self): + pref = Preference( + "channel-feed", + _("Channel feed"), + _("This channel provides multiple feeds, choose which one to view"), + PreferenceType.DROPDOWN, + [ feed.name for feed in self.feeds ], + self.default_feed_name + ) + # display preference + row = PreferenceRow(pref) + row.set_callback(self.change_feed_wrapper) + self.about.add(row.get_widget()) + + def background(self, channel_id): + # obtain channel information + self.channel = self.instance.get_channel_info(channel_id) + app_conf = get_app_settings() + pixbuf = None + if app_conf["show_images_in_browse"]: + pixbuf = pixbuf_from_url(self.channel.avatar) + texture = None + if not pixbuf is None: + texture = Gdk.Texture.new_for_pixbuf(pixbuf) + GLib.idle_add(self.display_info, texture) + # get channel feeds - feeds = self.instance.get_channel_feeds(self.channel_id) + feeds = self.instance.get_channel_feeds(channel_id) self.feeds = feeds if len(feeds) == 0: # if the channel doesn't have any feeds, we are done here return default_feed = feeds[0].id - if len(feeds) > 1: - default_feed = self.instance.get_default_channel_feed(self.channel_id) - # ONLY DISPLAY FEED-SELECT IF FEEDCOUNT > 1 - # construct preference to select feed - default_name = "" - for feed in feeds: - if feed.id == default_feed: - default_name = feed.name - break - pref = Preference( - "channel-feed", - _("Channel feed"), - _("This channel provides multiple feeds, choose which one to view"), - PreferenceType.DROPDOWN, - [ feed.name for feed in feeds ], - default_name - ) - # display preference - row = PreferenceRow(pref) - row.set_callback(self.change_feed_wrapper) - self.about.add(row.get_widget()) - - self.change_feed(default_feed) + default_feed = self.instance.get_default_channel_feed(channel_id) + # ONLY DISPLAY FEED-SELECT IF FEEDCOUNT > 1 + # construct preference to select feed + default_name = "" + for feed in feeds: + if feed.id == default_feed: + default_name = feed.name + break + self.default_feed_name = default_name + GLib.idle_add(self.display_feedselect) + + # run feed layout prep on main thread + # a new thread will be spawned there + GLib.idle_add(self.change_feed, default_feed) + + def __init__(self, server_id, channel_id, *args, **kwargs): + super().__init__(*args, **kwargs) + self.channel_id = channel_id + # get instance handle + server = get_servers_list()[server_id] + self.instance = get_server_instance(server) + + self.header_bar = Adw.HeaderBar() + self.external_btn = IconButton("","modem-symbolic") + self.external_btn.connect("clicked", self.on_open_in_browser) + self.header_bar.pack_end(self.external_btn) + + self.toolbar_view = Adw.ToolbarView() + self.toolbar_view.add_top_bar(self.header_bar) + + self.wrapper = Adw.Clamp() + self.scrollview = Gtk.ScrolledWindow() + + # show spinner + # will be cleared by display_info + spinner = Gtk.Spinner() + spinner.set_size_request(50, 50) + spinner.start() + cb = Gtk.CenterBox() + cb.set_center_widget(spinner) + self.scrollview.set_child(cb) + + self.wrapper.set_child(self.scrollview) + self.toolbar_view.set_content(self.wrapper) + self.set_child(self.toolbar_view) + + # start background thread + self.thread = threading.Thread(target=self.background, args=[channel_id]) + self.thread.daemon = True + self.thread.start() + def update_sub(self, subscribe): if subscribe: ensure_subscribed_to_channel(self.channel) diff --git a/melon/browse/playlist.py b/melon/browse/playlist.py index 305a202..e782c6f 100644 --- a/melon/browse/playlist.py +++ b/melon/browse/playlist.py @@ -5,6 +5,7 @@ gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, Gdk, GLib from unidecode import unidecode from gettext import gettext as _ +import threading from melon.servers.utils import get_server_instance, get_servers_list from melon.servers.utils import pixbuf_from_url @@ -16,10 +17,69 @@ from melon.models import get_app_settings from melon.models import has_bookmarked_external_playlist, ensure_bookmark_external_playlist, ensure_unbookmark_external_playlist class BrowsePlaylistScreen(Adw.NavigationPage): - def fetch_page(self, page=1): + def render_page(self, items): + app_conf = get_app_settings() + for res in items: + self.results.add(AdaptiveFeedItem(res, show_preview=app_conf["show_images_in_browse"])) + def fetch_page(self, page=0): cont = self.instance.get_playlist_content(self.playlist_id) - for res in cont: - self.results.add(AdaptiveFeedItem(res)) + GLib.idle_add(self.render_page, cont) + def do_fetch_page(self, page=0): + # start background thread + self.thread = threading.Thread(target=self.fetch_page, args=[page]) + self.thread.daemon = True + self.thread.start() + + def display_info(self, texture): + # base layout + self.box = Adw.PreferencesPage() + self.about = Adw.PreferencesGroup() + self.about.set_title(unidecode(self.playlist.title)) + self.about.set_description(unidecode(self.playlist.description)) + self.set_title(self.playlist.title) + + self.results = None + self.scrollview.set_child(self.box) + self.box.add(self.about) + + bookmark_pref = Preference( + "bookmark-playlist", + _("Bookmark"), + _("Add Playlist to your local playlist collection"), + PreferenceType.TOGGLE, + False, + has_bookmarked_external_playlist(self.playlist.server, self.playlist.id)) + bookmark_row = PreferenceRow(bookmark_pref) + bookmark_row.set_callback(self.bookmark_playlist) + self.about.add(bookmark_row.get_widget()) + + # display channel info + av = Adw.Avatar() + av.set_size(64) + if not texture is None: + av.set_custom_image(texture) + else: + av.set_text(self.playlist.title) + self.about.set_header_suffix(av) + + self.results = Adw.PreferencesGroup() + self.box.add(self.results) + + + def background(self, playlist_id): + # obtain playlist information + self.playlist = self.instance.get_playlist_info(playlist_id) + app_conf = get_app_settings() + pixbuf = None + if app_conf["show_images_in_browse"]: + pixbuf = pixbuf_from_url(self.playlist.thumbnail) + texture = None + if not pixbuf is None: + texture = Gdk.Texture.new_for_pixbuf(pixbuf) + GLib.idle_add(self.display_info, texture) + + # load first page + GLib.idle_add(self.do_fetch_page) def on_open_in_browser(self, arg): Gtk.UriLauncher.new(uri=self.playlist.url).launch() @@ -37,9 +97,6 @@ class BrowsePlaylistScreen(Adw.NavigationPage): # get instance handle server = get_servers_list()[server_id] self.instance = get_server_instance(server) - # obtain channel information - self.playlist = self.instance.get_playlist_info(playlist_id) - self.set_title(self.playlist.title) self.header_bar = Adw.HeaderBar() self.external_btn = IconButton("","modem-symbolic") @@ -51,45 +108,21 @@ class BrowsePlaylistScreen(Adw.NavigationPage): self.wrapper = Adw.Clamp() self.scrollview = Gtk.ScrolledWindow() - self.box = Adw.PreferencesPage() - self.about = Adw.PreferencesGroup() - self.about.set_title(unidecode(self.playlist.title)) - self.about.set_description(unidecode(self.playlist.description)) - bookmark_pref = Preference( - "bookmark-playlist", - _("Bookmark"), - _("Add Playlist to your local playlist collection"), - PreferenceType.TOGGLE, - False, - has_bookmarked_external_playlist(server_id, playlist_id)) - bookmark_row = PreferenceRow(bookmark_pref) - bookmark_row.set_callback(self.bookmark_playlist) - self.about.add(bookmark_row.get_widget()) - - self.results = None + # show spinner + # will be cleared by display_info + spinner = Gtk.Spinner() + spinner.set_size_request(50, 50) + spinner.start() + cb = Gtk.CenterBox() + cb.set_center_widget(spinner) + self.scrollview.set_child(cb) - # we have to append all the neccessary elements here, - # because if there are no channel feeds this function will exit early - self.scrollview.set_child(self.box) self.wrapper.set_child(self.scrollview) self.toolbar_view.set_content(self.wrapper) self.set_child(self.toolbar_view) - self.box.add(self.about) - - # display channel info - app_conf = get_app_settings() - av = Adw.Avatar() - av.set_size(64) - if app_conf["show_images_in_browse"]: - pixbuf = pixbuf_from_url(self.playlist.thumbnail) - if not pixbuf is None: - texture = Gdk.Texture.new_for_pixbuf(pixbuf) - av.set_custom_image(texture) - else: - av.set_text(self.playlist.title) - self.about.set_header_suffix(av) - self.results = Adw.PreferencesGroup() - self.box.add(self.results) - self.fetch_page() + # start background thread + self.thread = threading.Thread(target=self.background, args=[playlist_id]) + self.thread.daemon = True + self.thread.start() -- 2.38.5