~comcloudway/melon

b554a971859a24668a8fccabbc8fa87426401f1d — Jakob Meier 6 months ago 7236274
load channel & playlist browse screen content using separate threads

NOTE: renders widgets on main thread, to not cause any segfaults
2 files changed, 191 insertions(+), 110 deletions(-)

M melon/browse/channel.py
M melon/browse/playlist.py
M melon/browse/channel.py => melon/browse/channel.py +116 -68
@@ 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)

M melon/browse/playlist.py => melon/browse/playlist.py +75 -42
@@ 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()