@@ 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)