From 128cc3b2e3636e229b5ef2afac9586e67bcda2c9 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 13 Mar 2024 14:47:02 +0100 Subject: [PATCH] initial webview control bridge support for peertube --- melon/servers/invidious/__init__.py | 2 - melon/servers/peertube/__init__.py | 164 ++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/melon/servers/invidious/__init__.py b/melon/servers/invidious/__init__.py index 79748f6..0237ef9 100644 --- a/melon/servers/invidious/__init__.py +++ b/melon/servers/invidious/__init__.py @@ -7,7 +7,6 @@ import gi gi.require_version("WebKit", "6.0") from gi.repository import GLib, WebKit - from melon.import_providers.newpipe import NewpipeImporter from melon.servers import Server, Preference, PreferenceType from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode @@ -322,7 +321,6 @@ class Invidious(Server): WEBVIEW_READY = WebKit.LoadEvent.COMMITTED js_player_ready = """ - let min_ready_state = 3; if (isNaN(player.duration)) { await new Promise((res)=>{ if (!isNaN(player.duration)) { res(); return undefined; } diff --git a/melon/servers/peertube/__init__.py b/melon/servers/peertube/__init__.py index c02e82e..8293c88 100644 --- a/melon/servers/peertube/__init__.py +++ b/melon/servers/peertube/__init__.py @@ -2,11 +2,15 @@ import requests from urllib.parse import urlparse,parse_qs from datetime import datetime 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.import_providers.newpipe import NewpipeImporter +from melon.utils import pass_me class Peertube(Server): @@ -376,3 +380,163 @@ class Peertube(Server): # TODO: port newpipe invidious importer to peertube # REQUIRES: figuring out what service_id peertube uses ] + + # + # 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(); + }); + }); + } + """ + + js_has_player = """ + let selector = '#vjs_video_3_html5_api'; + if (!document.querySelector(selector)) { + await new Promise(resolve => { + if (document.querySelector(selector)) { + resolve(document.querySelector(selector)); + return; + } + + const observer = new MutationObserver(() => { + if (document.querySelector(selector)) { + observer.disconnect(); + resolve(document.querySelector(selector)); + } + }); + + if (document.querySelector(selector)) { + resolve(document.querySelector(selector)); + return; + } + + observer.observe(document.body, { + subtree: true, + childList: true, + }); + }); + } + """ + + def get_video_playback_position(self, webview, on_data): + js = f""" + {self.js_has_player} + 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""" + {self.js_has_player} + 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""" + {self.js_has_player} + 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 = f""" + {self.js_has_player} + 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 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