From 0a4bd2d804af9d973ebebcef67f0344227e4cef1 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 13 Mar 2024 18:38:30 +0100 Subject: [PATCH] initial webview control bridge support for nebula BUG: setting playback position appears to break playback --- melon/servers/nebula/__init__.py | 171 ++++++++++++++++++++++++++++++- 1 file changed, 169 insertions(+), 2 deletions(-) diff --git a/melon/servers/nebula/__init__.py b/melon/servers/nebula/__init__.py index 1219a88..2ece3f0 100644 --- a/melon/servers/nebula/__init__.py +++ b/melon/servers/nebula/__init__.py @@ -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) -- 2.38.5