~comcloudway/melon

cfecbf421400ac2e1f6c6af8552d957c57332f22 — Jakob Meier 6 months ago 348bce0
basic playlist playback mode
M melon/browse/playlist.py => melon/browse/playlist.py +11 -3
@@ 31,6 31,17 @@ class BrowsePlaylistScreen(Adw.NavigationPage):
        self.thread.start()

    def display_info(self, texture):
        box = Gtk.Box()
        self.external_btn = IconButton("","modem-symbolic")
        self.external_btn.connect("clicked", self.on_open_in_browser)
        box.append(self.external_btn)
        self.startplay_btn = IconButton("","media-playback-start-symbolic", tooltip=_("Start playing"))
        self.startplay_btn.set_action_name("win.playlistplayer-external")
        self.startplay_btn.set_action_target_value(
            GLib.Variant("as", [self.playlist.server, self.playlist.id]))
        box.append(self.startplay_btn)
        self.header_bar.pack_end(box)

        # base layout
        self.box = Adw.PreferencesPage()
        self.about = Adw.PreferencesGroup()


@@ 102,9 113,6 @@ class BrowsePlaylistScreen(Adw.NavigationPage):
        self.set_title(_("Playlist"))

        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)

M melon/models/__init__.py => melon/models/__init__.py +10 -0
@@ 747,6 747,16 @@ def set_video_playback_position(vid: Video, position: float, duration: float):
    conn.close()
    notify("playback_changed")

def set_video_playback_position_force(server_id: str, video_id:str, position: float, duration: float):
    uts = datetime.now().timestamp()
    conn = connect_to_db()
    execute_sql(conn, """
    INSERT OR REPLACE INTO playback
    VALUES (?,?,?,?,?)
    """, (uts, server_id, video_id, duration, position))
    conn.close()
    notify("playback_changed")

def get_last_playback() -> (tuple[Video, float, float] | None):
    conn = connect_to_db()
    results = conn.execute("""

M melon/player/__init__.py => melon/player/__init__.py +60 -22
@@ 13,7 13,7 @@ from melon.widgets.feeditem import AdaptiveFeedItem
from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference
from melon.widgets.iconbutton import IconButton
from melon.models import get_app_settings, add_to_history, register_callback
from melon.models import get_video_playback_position, set_video_playback_position
from melon.models import get_video_playback_position, set_video_playback_position, set_video_playback_position_force
from melon.utils import pass_me

class PlayerScreen(Adw.NavigationPage):


@@ 84,11 84,22 @@ class PlayerScreen(Adw.NavigationPage):
        return False

    def display_error(self):
        text = Gtk.Label()
        text.set_label(_("Video could not be loaded"))
        cb = Gtk.CenterBox()
        cb.set_center_widget(text)
        self.scrollview.set_child(cb)
        status = Adw.StatusPage()
        status.set_title(_("*crickets chirping*"))
        subs = get_subscribed_channels()
        status.set_description(_("Video could not be loaded"))
        status.set_icon_name("weather-few-clouds-night-symbolic")
        icon_button = IconButton(_("Exit Playlist mode"), "window-close-symbolic")
        # TODO fix action
        icon_button.set_action_name("win.browse")
        icon_button.set_margin_bottom(6)
        box = Gtk.CenterBox()
        ls = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)
        ls.append(icon_button)
        ls.append(refresh_btn)
        box.set_center_widget(ls)
        status.set_child(box)
        self.scrollview.set_child(status)
        return False

    def background(self, video_id):


@@ 112,32 123,57 @@ class PlayerScreen(Adw.NavigationPage):

    def __init__(self, server_id, video_id, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if server_id is None or video_id is None:
            return

        # show fallback title
        self.set_title(_("Loading..."))

        self.header_bar = Adw.HeaderBar()
        self.toolbar_view = Adw.ToolbarView()
        self.toolbar_view.add_top_bar(self.header_bar)
        self.set_child(self.toolbar_view)

        self.wrapper = Adw.Clamp()
        self.toolbar_view.set_content(self.wrapper)

        self.scrollview = Gtk.ScrolledWindow()
        self.wrapper.set_child(self.scrollview)

        self.init_video(server_id, video_id)

    def init_video(self, server_id, video_id):
        # reset these for now
        self.position = None
        self.duration = None

        # show fallback title
        # in case init_video is called twice,
        # this resets the title
        self.set_title(_("Loading..."))

        # reset playback position if video was nearly completely watched
        pos = get_video_playback_position(server_id, video_id)
        dur = get_video_playback_position(server_id, video_id)
        if not pos is None and not dur is None:
            # TODO consider moving this to the settings panel
            consider_done = 0.99*dur
            # video was probably watched till end
            # reset playback position
            if pos >= consider_done:
                # we can use force here, because if pos and dur aren't None
                # set_video_playback_position was called before so the video exists
                set_video_playback_position_force(server_id, video_id, 0, dur)

        self.video_id = video_id
        # get instance handle
        server = get_servers_list()[server_id]
        self.instance = get_server_instance(server)

        # show fallback title
        self.set_title(_("Player"))

        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.set_child(self.toolbar_view)

        self.wrapper = Adw.Clamp()
        self.toolbar_view.set_content(self.wrapper)

        self.scrollview = Gtk.ScrolledWindow()
        # show spinner
        # will be cleared by display_info
        spinner = Gtk.Spinner()


@@ 147,8 183,6 @@ class PlayerScreen(Adw.NavigationPage):
        cb.set_center_widget(spinner)
        self.scrollview.set_child(cb)

        self.wrapper.set_child(self.scrollview)

        self.box = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)

        # start background thread


@@ 204,11 238,15 @@ class PlayerScreen(Adw.NavigationPage):
        # runs loop every two seconds
        # should probably happen every second,
        # but I fear that this would consume to many resources
        #self.loop()
        self.loop()
        self.instance.connect_video_ended(self.view, self.on_end)

    def on_end(self, state):
        print("BYE")
        # we've reached the end of the video
        # might as well manually save the position
        if not self.duration is None:
            self.position = self.duration
            self.commit_playback()

    def select_stream(self, quality, store_position=False):
        # get current playback position & save to db

A melon/player/playlist.py => melon/player/playlist.py +157 -0
@@ 0,0 1,157 @@
import gi
gi.require_version("WebKit", "6.0")
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, WebKit, 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 import SearchMode
from melon.widgets.feeditem import AdaptiveFeedItem
from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference
from melon.widgets.iconbutton import IconButton
from melon.models import get_app_settings, add_to_history, register_callback
from melon.models import get_video_playback_position, set_video_playback_position, set_video_playback_position_force
from melon.utils import pass_me

from melon.models import get_local_playlist, PlaylistWrapper, PlaylistWrapperType

from melon.player import PlayerScreen

class PlaylistPlayerScreen(PlayerScreen):
    playlist = None
    playlist_index = 0
    playlist_instance = None
    playlist_content = []

    def __init__(self, ref: (tuple[str, str] | int), index=0, *args, **kwargs):
        # make sure to initalize widget
        # PlayerScreen.__init__ will break after initializing parents
        # because neither server or video are supplied
        super().__init__(None, None)

        self.playlist_index = index
        self.playlist = None
        self.video_duration = None
        self.video_position = None
        self.playlist_instance = None

        # prepare base layout
        self.header_bar = Adw.HeaderBar()
        self.set_title(_("Loading..."))
        # show spinner
        self.toolbar_view = Adw.ToolbarView()
        self.toolbar_view.add_top_bar(self.header_bar)
        self.set_child(self.toolbar_view)

        self.wrapper = Adw.Clamp()
        self.toolbar_view.set_content(self.wrapper)

        self.scrollview = Gtk.ScrolledWindow()
        # show spinner
        # will be overwritten by init_load_video
        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)

        # start prepare_playlist thread
        self.visible = True
        self.thread = threading.Thread(target=self.prepare_playlist_task, args=[ref])
        self.thread.daemon = True
        self.thread.start()

    # fetch playlist data & store it interally
    def prepare_playlist_task(self, ref: (tuple[str, str] | int)):
        if isinstance(ref, int):
            playlist_id = ref
            # local playlist
            pl = get_local_playlist(playlist_id)
            self.playlist = PlaylistWrapper.from_local(pl)
            self.playlist_content = pl.content
        elif isinstance(ref, tuple):
            server_id = ref[0]
            playlist_id = ref[1]
            # external playlist
            server = get_servers_list()[server_id]
            self.playlist_instance = get_server_instance(server)
            pl = self.playlist_instance.get_playlist_info(playlist_id)
            self.playlist = PlaylistWrapper.from_external(pl)
            cont = self.playlist_instance.get_playlist_content(playlist_id)
            self.playlist_content = cont
        else:
            # shouldn't be reachable
            # if properly used
            # (see argument type bounds)
            GLib.idle_add(self.playlist_error)
            return

        # show empty playlist screen if there are no videos
        if not self.playlist_content:
            GLib.idle_add(self.empty_playlist)
            return

        # show playlist error screen if something didn't quite work out
        if self.playlist_index > len(self.playlist_content):
            GLib.idle_add(self.playlist_error)
            return
        self.prepare(0)

    def prepare(self, index):
        dt = self.playlist_content[index]
        server_id = dt.server
        video_id = dt.id

        GLib.idle_add(self.init_video, server_id, video_id)

    def on_end(self, state):
        super().on_end(state)
        # if there are more videos in the playlist
        # load the next one
        # do nothing if there are no more videos
        if self.playlist_index+1 < len(self.playlist_content):
            self.playlist_index += 1
            self.prepare(self.playlist_index)

    def empty_playlist(self):
        # display playlist is empty banner
        # acts as a drop in replacement for the player screen
        status = Adw.StatusPage()
        status.set_title(_("*crickets chirping*"))
        subs = get_subscribed_channels()
        status.set_description(_("This playlist is empty"))
        status.set_icon_name("weather-few-clouds-night-symbolic")
        icon_button = IconButton(_("Exit Playlist mode"), "window-close-symbolic")
        # TODO fix action
        icon_button.set_action_name("win.browse")
        icon_button.set_margin_bottom(6)
        box = Gtk.CenterBox()
        ls = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)
        ls.append(icon_button)
        ls.append(refresh_btn)
        box.set_center_widget(ls)
        status.set_child(box)
        self.scrollview.set_child(status)

    def playlist_error(self):
        status = Adw.StatusPage()
        status.set_title(_("*crickets chirping*"))
        subs = get_subscribed_channels()
        status.set_description(_("There was an error loading the playlist"))
        status.set_icon_name("weather-few-clouds-night-symbolic")
        icon_button = IconButton(_("Close"), "window-close-symbolic")
        # TODO fix action
        icon_button.set_action_name("win.browse")
        icon_button.set_margin_bottom(6)
        box = Gtk.CenterBox()
        ls = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)
        ls.append(icon_button)
        ls.append(refresh_btn)
        box.set_center_widget(ls)
        status.set_child(box)
        self.scrollview.set_child(status)

M melon/playlist/__init__.py => melon/playlist/__init__.py +9 -1
@@ 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
from unidecode import unidecode
from gettext import gettext as _



@@ 39,6 39,14 @@ class LocalPlaylistScreen(Adw.NavigationPage):
        edit_button = IconButton(_("Edit"), "document-edit-symbolic")
        edit_button.connect("clicked", lambda _: self.open_edit())

        box = Gtk.Box()
        self.startplay_btn = IconButton("","media-playback-start-symbolic", tooltip=_("Start playing"))
        self.startplay_btn.set_action_name("win.playlistplayer-local")
        self.startplay_btn.set_action_target_value(
            GLib.Variant("u", self.playlist.id))
        box.append(self.startplay_btn)
        self.header_bar.pack_end(box)

        if len(playlist.content) == 0:
            status = Adw.StatusPage()
            status.set_title(_("*crickets chirping*"))

M melon/servers/invidious/__init__.py => melon/servers/invidious/__init__.py +0 -13
@@ 334,19 334,6 @@ class Invidious(Server):
                });
            });
        }
        if (player.readyState < min_ready_state) {
            await new Promise((res) => {
                if (player.readyState >= min_ready_state) { res(); return undefined; }
                let list = player.addEventListener(
                    'loadeddata',
                    ()=>{
                        if (player.readyState >= min_ready_state) {
                            player.removeEventListener('loadeddata', list);
                            res();
                        }
                    });
            });
        }
    """

    def get_video_playback_position(self, webview, on_data):

M melon/window.py => melon/window.py +11 -0
@@ 15,6 15,7 @@ from melon.servers.utils import get_servers_list, get_server_instance
from melon.settings import SettingsScreen
from melon.importer import ImporterScreen
from melon.player import PlayerScreen
from melon.player.playlist import PlaylistPlayerScreen
from melon.widgets.simpledialog import SimpleDialog
from melon.playlist import LocalPlaylistScreen
from melon.playlist.pick import PlaylistPickerDialog


@@ 66,6 67,13 @@ class MainWindow(Adw.ApplicationWindow):
    def open_local_playlist_viewer(self, action, prefs):
        self.view.push(LocalPlaylistScreen(prefs.unpack()))

    def open_playlistplayer_local(self, action, prefs):
        self.view.push(PlaylistPlayerScreen(prefs.unpack()))
    def open_playlistplayer_external(self, action, prefs):
        server_id = prefs[0]
        playlist_id = prefs[1]
        self.view.push(PlaylistPlayerScreen((server_id, playlist_id)))

    # Opens the about app screen
    def open_about(self,action,prefs):
        dialog = Adw.AboutWindow()


@@ 128,6 136,9 @@ class MainWindow(Adw.ApplicationWindow):
        self.reg_action("add_to_playlist", self.open_playlist_picker, "as")
        # same as new_playlist but with [server_id, video_id] to identify a video
        self.reg_action("add_to_new_playlist", self.open_playlist_creator, "as")
        # playlist player
        self.reg_action("playlistplayer-local", self.open_playlistplayer_local, "u")
        self.reg_action("playlistplayer-external", self.open_playlistplayer_external, "as")

    def reg_action(self, name, func, variant=None, target=None):
        vtype = None