~comcloudway/melon

0a4bd2d804af9d973ebebcef67f0344227e4cef1 — Jakob Meier 6 months ago 81b5517 dev
initial webview control bridge support for nebula

BUG:  setting playback position appears to break playback
1 files changed, 169 insertions(+), 2 deletions(-)

M melon/servers/nebula/__init__.py
M melon/servers/nebula/__init__.py => melon/servers/nebula/__init__.py +169 -2
@@ 4,10 4,14 @@ from urllib.parse import urlparse, parse_qs
from datetime import datetime
import json
from gettext import gettext as _
import gi
gi.require_version("WebKit", "6.0")
from gi.repository import GLib, WebKit

from melon.servers import Server, Preference, PreferenceType
from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode
from melon.servers import USER_AGENT
from melon.utils import pass_me

# NOTE: uses beautifulsoup instead of the invidious api
# because not all invidious servers provide the api


@@ 142,6 146,7 @@ class Nebula(Server):
        return []

    def search(self, query, mask, page=0):
        page = page + 1
        # nebula doesn't provide playlist search
        # which means that this feed will always be empty
        if mask == SearchMode.PLAYLISTS:


@@ 194,7 199,7 @@ class Nebula(Server):
        video_feed = Feed("videos", _("Videos"))
        if r.ok:
            data = json.loads(r.text)
            if (not "playlist" in data) or len(data["playlist"]) == 0:
            if (not "playlists" in data) or len(data["playlists"]) == 0:
                return [ video_feed ]
            else:
                return [ video_feed, Feed("playlists", _("Playlists")) ]


@@ 242,7 247,7 @@ class Nebula(Server):
                playlist_id, data["title"],
                # nebula doesn't support getting the channel
                # when accessing the playlist
                None,
                (None, None),
                # nebula doesn't support playlist thumbnails
                None)



@@ 346,3 351,165 @@ class Nebula(Server):
        return [
            Stream(self.settings["m3u8-player"].value.replace("%s",manifest_link), "auto")
        ]

    #
    # WEBVIEW CONTROL BRIDGE
    #
    WEBVIEW_READY = WebKit.LoadEvent.FINISHED

    js_player_ready = """
        if (isNaN(player.duration)) {
            await new Promise((res)=>{
                if (!isNaN(player.duration)) { res(); return undefined; }
                let list = player.addEventListener(
                    'loadedmetadata',
                    ()=>{
                        player.removeEventListener('loadedmetadata', list);
                        res();
                });
            });
        }
        """

    def get_video_playback_position(self, webview, on_data):
        js = f"""
        let players = document.getElementsByTagName('video');
        if (players.length != 1) return undefined;
        let player = players[0];
        {self.js_player_ready}
        if (isNaN(player.duration)) return undefined;
        return player.currentTime;
        """
        webview.call_async_javascript_function(
            js, len(js),
            None,
            None,
            self.id,
            None,
            pass_me(self.on_js_double, webview, on_data)
        )
    def get_video_duration(self, webview, on_data):
        js = f"""
        let players = document.getElementsByTagName('video');
        if (players.length != 1) return undefined;
        let player = players[0];
        {self.js_player_ready}
        if (isNaN(player.duration)) return undefined;
        return player.duration;
        """
        webview.call_async_javascript_function(
            js, len(js),
            None,
            None,
            self.id,
            None,
            pass_me(self.on_js_double, webview, on_data)
        )
    def set_video_playback_position(self, webview, timestamp, on_done):
        js = f"""
        let players = document.getElementsByTagName('video');
        if (players.length != 1) return false;
        let player = players[0];
        {self.js_player_ready}
        if (timestamp > player.duration || timestamp < 0) return false;
        if (!player.seekable) return false;
        player.currentTime = timestamp;
        return true;
        """
        tab = GLib.VariantDict()
        tab.insert_value("timestamp", GLib.Variant("d", timestamp))
        args = tab.end()
        webview.call_async_javascript_function(
            js, len(js),
            args,
            None,
            self.id,
            None,
            pass_me(self.on_js_bool, webview, on_done)
        )
    def connect_video_ended(self, webview, on_done):
        js = """
        let players = document.getElementsByTagName('video');
        if (players.length != 1) { return undefined; }
        let player = players[0];
        if (!player.ended) {
            await new Promise((res)=>{
                let list = player.addEventListener(
                    'ended',
                    ()=>{
                        player.removeEventListener('ended', list);
                        res(true);
                    }
                );
            });
        }
        return player.ended;
        """
        webview.call_async_javascript_function(
            js, len(js),
            None,
            None,
            self.id,
            None,
            pass_me(self.on_js_bool, webview, on_done)
        )

    def do_video_play(self, webview, on_done):
        js = f"""
        let players = document.getElementsByTagName('video');
        if (players.length != 1) return false;
        let player = players[0];
        {self.js_player_ready}
        await player.play();
        return true;
        """
        webview.call_async_javascript_function(
            js, len(js),
            None,
            None,
            self.id,
            None,
            pass_me(self.on_js_bool, webview, on_done)
        )
    def do_video_pause(self, webview, on_done):
        js = f"""
        let players = document.getElementsByTagName('video');
        if (players.length != 1) return false;
        let player = players[0];
        {self.js_player_ready}
        player.pause();
        return true;
        """
        webview.call_async_javascript_function(
            js, len(js),
            None,
            None,
            self.id,
            None,
            pass_me(self.on_js_bool, webview, on_done)
        )

    def on_js_bool(self, obj, async_res, webview, on_bool):
        if on_bool is None:
            return
        try:
            val = webview.call_async_javascript_function_finish(async_res)
        except Exception as e:
            on_bool(False)
            return
        if val.is_boolean():
            on_bool(val.to_boolean())
        else:
            on_bool(False)
    def on_js_double(self, obj, async_res, webview, on_double):
        if on_double is None:
            return
        try:
            val = webview.call_async_javascript_function_finish(async_res)
        except Exception as e:
            on_double(None)
            return
        if val.is_number():
            on_double(val.to_double())
        else:
            on_double(None)