~comcloudway/melon

0220dd40f6f5ab160b531297cdaea96059dbd758 — Jakob Meier 6 months ago c618292
do not render home feeds on separate threads + increase thumbnail worker
  count
M melon/background.py => melon/background.py +1 -1
@@ 22,4 22,4 @@ class Queue():
    def add(self, func, *args):
        self.tasks.append((func, args))

queue = Queue(1)
queue = Queue(5)

M melon/home/history.py => melon/home/history.py +12 -21
@@ 2,7 2,7 @@ import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw
from gi.repository import Gtk, Adw, GLib
import threading
from gettext import gettext as _



@@ 16,13 16,9 @@ class History(ViewStackPage):
    # datecount displayed
    page_size = 7

    def update(self):
        app_settings = get_app_settings()
        # make sure only videos from available servers are shown
        hist = filter_resources(get_history(), app_settings, access=lambda x:x[0])
        hist.reverse()
        self.data = list(group_by_date(hist).items())
        if len(hist) == 0:
    def update(self, ls):
        self.data = ls
        if not ls:
            status = Adw.StatusPage()
            status.set_title(_("*crickets chirping*"))
            status.set_description(_("You haven't watched any videos yet"))


@@ 45,27 41,22 @@ class History(ViewStackPage):
    def load_page(self, page=0):
        app_settings = get_app_settings()
        for date, content in self.data[page*self.page_size:(page+1)*self.page_size]:
            # faster thread breakout
            if self.stop_update:
                break
            group = Adw.PreferencesGroup()
            group.set_title(date)
            self.results.add(group)
            for resource in content:
                # faster thread breakout
                if self.stop_update:
                    break
                group.add(AdaptiveFeedItem(resource, app_settings["show_images_in_feed"]))
        # TODO somehow add "load more" button

    stop_update = False
    def schedule(self):
        app_settings = get_app_settings()
        # make sure only videos from available servers are shown
        hist = filter_resources(get_history(), app_settings, access=lambda x:x[0])
        hist.reverse()
        GLib.idle_add(self.update, list(group_by_date(hist).items()))

    thread = None
    def do_update(self):
        if not self.thread is None:
            self.stop_update = True
            # wait for old thread to finish
            self.thread.join()
            self.stop_update = False
        # show spinner
        spinner = Gtk.Spinner()
        spinner.set_size_request(50, 50)


@@ 74,7 65,7 @@ class History(ViewStackPage):
        cb.set_center_widget(spinner)
        self.inner.set_child(cb)
        # start thread
        self.thread = threading.Thread(target=self.update)
        self.thread = threading.Thread(target=self.schedule)
        self.thread.daemon = True
        self.thread.start()


M melon/home/new.py => melon/home/new.py +37 -32
@@ 16,7 16,8 @@ from melon.models import get_subscribed_channels, get_app_settings
from melon.models import get_cached_feed, clear_cached_feed, update_cached_feed, get_last_feed_refresh

class NewFeed(ViewStackPage):
    def update(self, news):
    def update(self):
        news = get_cached_feed(100)
        # both subscreens need the refresh button
        refresh_btn = IconButton(_("Refresh"), "view-refresh-symbolic")
        refresh_btn.connect("clicked", lambda a: self.reload())


@@ 57,35 58,23 @@ class NewFeed(ViewStackPage):
            title.set_header_suffix(refresh_btn)
            results.add(title)
            self.inner.set_child(results)
            for date, content in news:
                group = Adw.PreferencesGroup()
                group.set_title(date)
                results.add(group)
                for resource in content:
                    group.add(AdaptiveFeedItem(resource, app_settings["show_images_in_feed"]))
            tracker = {}
            for entry in news:
                vid = entry[0]
                uts = entry[1]
                date = datetime.fromtimestamp(uts).date().strftime("%c").replace("00:00:00","").replace("  ", " ")
                group = None
                if date in tracker:
                    group = tracker[date]
                else:
                    group = Adw.PreferencesGroup()
                    results.add(group)
                    group.set_title(date)
                    tracker[date] = group
                group.add(AdaptiveFeedItem(vid, app_settings["show_images_in_feed"]))
        # return false, because otherwise ide_add would run multiple times
        return False

    def schedule(self):
        news = get_cached_feed()
        if not news:
            # clear the feed again
            # in case this wasn't calles from reload
            # because we do NOT want duplicate entries
            clear_cached_feed()
            # long task:
            # NOTE: fetch_home_feed automatically dismisses channels
            # if the server isn't available (i.e because it has been disabled)
            news = fetch_home_feed()
            update_cached_feed(news)
        # only show last 100 videos
        news = news[-100:]
        # reverse the feed, because the latest (highest uts) should be on top
        news.reverse()
        # group the data by date
        group = group_by_date(news)
        GLib.idle_add(self.update, group.items())

    thread = None
    def do_update(self):
        # show spinner


@@ 95,15 84,31 @@ class NewFeed(ViewStackPage):
        cb = Gtk.CenterBox()
        cb.set_center_widget(spinner)
        self.inner.set_child(cb)
        GLib.idle_add(self.update)

    def do_reload(self):
        # clear the feed
        clear_cached_feed()
        # long task:
        # NOTE: fetch_home_feed automatically dismisses channels
        # if the server isn't available (i.e because it has been disabled)
        news = fetch_home_feed()
        update_cached_feed(news)
        GLib.idle_add(self.update)

    def reload(self):
        # show spinner
        spinner = Gtk.Spinner()
        spinner.set_size_request(50, 50)
        spinner.start()
        cb = Gtk.CenterBox()
        cb.set_center_widget(spinner)
        self.inner.set_child(cb)
        # start thread
        self.thread = threading.Thread(target=self.schedule)
        self.thread = threading.Thread(target=self.do_reload)
        self.thread.daemon = True
        self.thread.start()

    def reload(self):
        clear_cached_feed()
        self.do_update()

    def __init__(self):
        super().__init__("feed-new", _("What's new"), "user-home-symbolic")
        self.widget = Adw.Clamp()

M melon/home/playlists.py => melon/home/playlists.py +12 -17
@@ 2,7 2,7 @@ import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw
from gi.repository import Gtk, Adw, GLib
import threading
from gettext import gettext as _



@@ 14,13 14,9 @@ from melon.models import PlaylistWrapperType
from melon.servers.utils import server_is_allowed

class Playlists(ViewStackPage):
    def update(self):
    def update(self, playlists):
        app_conf = get_app_settings()
        # filter external playlists if server is disabled
        playlists = [ playlist for playlist in get_playlists() if playlist.type == PlaylistWrapperType.LOCAL or server_is_allowed(playlist.inner.server, app_conf) ]
        # local and external playlists have the inner.title attribute
        playlists.sort(key=lambda x:x.inner.title)
        if len(playlists) == 0:
        if not playlists:
             status = Adw.StatusPage()
             status.set_title(_("*crickets chirping*"))
             status.set_description(_("You don't have any playlists yet"))


@@ 40,19 36,18 @@ class Playlists(ViewStackPage):
            results.set_header_suffix(icon_button)
            self.inner.set_child(results)
            for playlist in playlists:
                # faster thread breakout
                if self.stop_update:
                    break
                results.add(AdaptivePlaylistFeedItem(playlist, app_conf["show_images_in_feed"]))

    stop_update = False
    def schedule(self):
        app_conf = get_app_settings()
        # filter external playlists if server is disabled
        playlists = [ playlist for playlist in get_playlists() if playlist.type == PlaylistWrapperType.LOCAL or server_is_allowed(playlist.inner.server, app_conf) ]
        # local and external playlists have the inner.title attribute
        playlists.sort(key=lambda x:x.inner.title)
        GLib.idle_add(self.update, playlists)

    thread = None
    def do_update(self):
        if not self.thread is None:
            self.stop_update = True
            # wait for old thread to finish
            self.thread.join()
            self.stop_update = False
        # show spinner
        spinner = Gtk.Spinner()
        spinner.set_size_request(50, 50)


@@ 61,7 56,7 @@ class Playlists(ViewStackPage):
        cb.set_center_widget(spinner)
        self.inner.set_child(cb)
        # start thread
        self.thread = threading.Thread(target=self.update)
        self.thread = threading.Thread(target=self.schedule)
        self.thread.daemon = True
        self.thread.start()


M melon/home/subs.py => melon/home/subs.py +11 -16
@@ 2,7 2,7 @@ import sys
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw
from gi.repository import Gtk, Adw, GLib
import threading
from gettext import gettext as _



@@ 13,11 13,9 @@ from melon.models import get_subscribed_channels, register_callback, get_app_set
from melon.servers.utils import filter_resources

class Subscriptions(ViewStackPage):
    def update(self):
    def update(self, subs):
        app_settings = get_app_settings()
        # make sure only channles from available servers are included
        subs = filter_resources(get_subscribed_channels(), app_settings)
        if len(subs) == 0:
        if not subs:
            status = Adw.StatusPage()
            status.set_title(_("*crickets chirping*"))
            status.set_description(_("You aren't yet subscribed to channels"))


@@ 32,26 30,23 @@ class Subscriptions(ViewStackPage):
            results = Adw.PreferencesGroup()
            results.set_title(_("Subscriptions"))
            results.set_description(_("You are subscribed to the following channels"))
            subs.sort(key=lambda c:c.name)
            # append first, so subscriptions are added as we go
            # not all at once
            # reduces time spent on loading animation
            # NOTE: might seem confusing to unknowing users
            self.inner.set_child(results)
            for channel in subs:
                # faster thread breakout
                if self.stop_update:
                    break
                results.add(AdaptiveFeedItem(channel, app_settings["show_images_in_feed"]))

    stop_update = False
    def schedule(self):
        app_settings = get_app_settings()
        # make sure only channles from available servers are included
        subs = filter_resources(get_subscribed_channels(), app_settings)
        subs.sort(key=lambda c:c.name)
        GLib.idle_add(self.update, subs)

    thread = None
    def do_update(self):
        if not self.thread is None:
            self.stop_update = True
            # wait for old thread to finish
            self.thread.join()
            self.stop_update = False
        # show spinner
        spinner = Gtk.Spinner()
        spinner.set_size_request(50, 50)


@@ 60,7 55,7 @@ class Subscriptions(ViewStackPage):
        cb.set_center_widget(spinner)
        self.inner.set_child(cb)
        # start thread
        self.thread = threading.Thread(target=self.update)
        self.thread = threading.Thread(target=self.schedule)
        self.thread.daemon = True
        self.thread.start()


M melon/models/__init__.py => melon/models/__init__.py +4 -2
@@ 638,7 638,7 @@ def get_history():
    """).fetchall()
    return [ (Video(d[1], d[3], d[2], d[4], (d[8], d[7]), d[5], d[6]), d[0]) for d in results ]

def get_cached_feed():
def get_cached_feed(limit=100):
    """
    Returns a list of (Video, uts) tuples
    """


@@ 646,7 646,9 @@ def get_cached_feed():
    results = conn.execute("""
    SELECT DISTINCT timestamp, server, id, url, title, description, thumbnail, channel_id, channel_name
    FROM news join videos on news.video_id = videos.id AND news.video_server = videos.server
    """).fetchall()
    ORDER BY timestamp DESC
    LIMIT ?
    """, (limit,)).fetchall()
    return [ (Video(d[1], d[3], d[2], d[4], (d[8], d[7]), d[5], d[6]), d[0]) for d in results ]
def clear_cached_feed():
    conn = connect_to_db()

M melon/widgets/feeditem.py => melon/widgets/feeditem.py +14 -9
@@ 83,9 83,6 @@ class AdaptiveFeedItem(Adw.ActionRow):
class AdaptivePlaylistFeedItem(Adw.ActionRow):
    def __init__(self, playlist:PlaylistWrapper, show_preview=True, *args, **kwargs):
        super().__init__(*args, **kwargs)
        pixbuf = None
        if show_preview:
            pixbuf = pixbuf_from_url(playlist.inner.thumbnail)
        self.set_title(unidecode(playlist.inner.title).replace("&","&"))
        self.set_subtitle(unidecode(playlist.inner.description).replace("&","&"))
        if playlist.type == PlaylistWrapperType.EXTERNAL:


@@ 97,11 94,19 @@ class AdaptivePlaylistFeedItem(Adw.ActionRow):
            self.set_action_target_value(GLib.Variant("u", playlist.inner.id))
        self.preview = Adw.Avatar()
        self.preview.set_size(48)
        if not pixbuf is None:
            texture = Gdk.Texture.new_for_pixbuf(pixbuf)
            self.preview.set_custom_image(texture)
        else:
            self.preview.set_show_initials(True)
            self.preview.set_text(self.get_title())
        self.preview.set_show_initials(True)
        self.preview.set_text(self.get_title())
        self.add_prefix(self.preview)
        self.set_activatable(True)
        if show_preview:
            queue.add(self.update_avatar, playlist.inner.thumbnail)

    def update_avatar(self, url):
        pixbuf = pixbuf_from_url(url)
        if not pixbuf is None:
            texture = Gdk.Texture.new_for_pixbuf(pixbuf)
            GLib.idle_add(self.complete_avatar, texture)
    def complete_avatar(self, texture):
        self.preview.set_custom_image(texture)
        # return false to cancel the idle job
        return False