M melon/models/__init__.py => melon/models/__init__.py +11 -0
@@ 725,6 725,17 @@ def get_last_feed_refresh() -> int:
return app_conf["news-feed-refresh"]
return None
+def get_video_duration(server_id:str, video_id:str) -> (float|None):
+ conn = connect_to_db()
+ results = conn.execute("""
+ SELECT duration
+ FROM playback
+ WHERE video_id = ? AND video_server = ?
+ """, (video_id, server_id)).fetchone()
+ if results is None:
+ return None
+ return results[0]
+
def get_video_playback_position(server_id:str, video_id:str) -> (float|None):
conn = connect_to_db()
results = conn.execute("""
M melon/player/__init__.py => melon/player/__init__.py +17 -6
@@ 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, set_video_playback_position_force
+from melon.models import get_video_playback_position, set_video_playback_position, set_video_playback_position_force, get_video_duration
from melon.utils import pass_me
class PlayerScreen(Adw.NavigationPage):
@@ 151,7 151,7 @@ class PlayerScreen(Adw.NavigationPage):
# 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)
+ dur = get_video_duration(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
@@ 219,9 219,15 @@ class PlayerScreen(Adw.NavigationPage):
self.position = pos
else:
self.position = None
- # get video duration & update db
- # handled by self.on_duration
- self.instance.get_video_duration(self.view, self.on_duration)
+ self.prepare_player_state()
+
+ def prepare_player_state(self):
+ self.instance.do_video_pause(self.view, self.prepared_player_state())
+
+ def prepared_player_state(self):
+ # get video duration & update db
+ # handled by self.on_duration
+ self.instance.get_video_duration(self.view, self.on_duration)
def on_duration(self, duration):
self.duration = duration
@@ 231,6 237,7 @@ class PlayerScreen(Adw.NavigationPage):
else:
self.on_player_setup_done()
def on_player_setup_done(self):
+ # save data
self.commit_playback()
# connect to events
self.connect("hiding", lambda _: self.on_hide())
@@ 267,7 274,11 @@ class PlayerScreen(Adw.NavigationPage):
def store_position(self, after=None, commit=True):
self.instance.get_video_playback_position(self.view, pass_me(self.on_position, after, commit))
def on_position(self, position, after=None, commit=True):
- self.position = position
+ if not position is None:
+ # only save valid datasets
+ self.position = position
+ # because this often occurs when killing the webview
+ # or on_hide, we still want to commit the changes
if commit:
self.commit_playback()
if not after is None:
M melon/player/playlist.py => melon/player/playlist.py +7 -0
@@ 289,3 289,10 @@ class PlaylistPlayerScreen(PlayerScreen):
self.ctr_group.add(expander)
self.about.add(self.ctr_group)
+
+ def prepare_player_state(self):
+ # by default the player pauses the video
+ # so that the user has to click "play"
+ # however in playlist mode, we kind of want videos to autoplay
+ # especially after the first one
+ self.instance.do_video_play(self.view, self.prepared_player_state())
M melon/servers/__init__.py => melon/servers/__init__.py +16 -0
@@ 294,3 294,19 @@ class Server(ABC):
should pass False to onDone if there was an error
"""
on_done(False)
+
+
+ def do_video_play(self, webview: WebKit.WebView, on_done: Callable[[bool], None]):
+ """
+ Should attempt to start playing the video
+ i.e by calling the .play() method via javascript
+ if that worked out True should be passed to on_done
+ """
+ on_done(False)
+ def do_video_pause(self, webview: WebKit.WebView, on_done: Callable[[bool], None]):
+ """
+ Should attempt to stop playing the video
+ i.e by calling the .pause() method via javascript
+ if that worked out True should be passed to on_done
+ """
+ on_done(False)
M melon/servers/invidious/__init__.py => melon/servers/invidious/__init__.py +49 -0
@@ 332,6 332,20 @@ class Invidious(Server):
});
});
}
+ let readyState = 2;
+ if (player.readyState < readyState) {
+ await new Promise(res=>{
+ if (player.readyState >= readyState) { res(); return; }
+ let list = player.addEventListener(
+ 'loadeddata',
+ ()=>{
+ if (player.readyState >= readyState) {
+ player.removeEventListener('loadeddata', list);
+ res();
+ }
+ });
+ });
+ }
"""
def get_video_playback_position(self, webview, on_data):
@@ 417,6 431,41 @@ class Invidious(Server):
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
M melon/servers/peertube/__init__.py => melon/servers/peertube/__init__.py +66 -11
@@ 295,16 295,22 @@ class Peertube(Server):
results = []
if r.ok:
for item in r.json()["data"]:
- item = item["video"]
- video_host = instance
- video_slug = item["uuid"]
- video_id=f"{video_host}::{video_slug}"
- thumb_path = item["thumbnailPath"]
- thumb = f"{instance}{thumb_path}"
- channel_name = item["channel"]["displayName"]
- channel_slug = item["channel"]["name"]
- channel_host = item["channel"]["host"]
- channel_id = f"https://{channel_host}::{channel_slug}"
+ try:
+ item = item["video"]
+ video_host = instance
+ video_slug = item["uuid"]
+ video_id=f"{video_host}::{video_slug}"
+ thumb_path = item["thumbnailPath"]
+ thumb = f"{instance}{thumb_path}"
+ channel_name = item["channel"]["displayName"]
+ channel_slug = item["channel"]["name"]
+ channel_host = item["channel"]["host"]
+ channel_id = f"https://{channel_host}::{channel_slug}"
+ except Exception as e:
+ # skip invaldi entries
+ # most likely a deleted video
+ # which we can't display
+ continue
v = Video(
self.id,
item["url"],
@@ 389,7 395,7 @@ class Peertube(Server):
js_player_ready = """
if (isNaN(player.duration)) {
await new Promise((res)=>{
- if (!isNaN(player.duration)) { res(); return undefined; }
+ if (!isNaN(player.duration)) { res(); return; }
let list = player.addEventListener(
'loadedmetadata',
()=>{
@@ 398,6 404,20 @@ class Peertube(Server):
});
});
}
+ let readyState = 2;
+ if (player.readyState < readyState) {
+ await new Promise(res=>{
+ if (player.readyState >= readyState) { res(); return; }
+ let list = player.addEventListener(
+ 'loadeddata',
+ ()=>{
+ if (player.readyState >= readyState) {
+ player.removeEventListener('loadeddata', list);
+ res();
+ }
+ });
+ });
+ }
"""
js_has_player = """
@@ 516,6 536,41 @@ class Peertube(Server):
pass_me(self.on_js_bool, webview, on_done)
)
+ def do_video_play(self, webview, on_done):
+ js = f"""
+ {self.js_has_player}
+ let players = document.getElementsByTagName('video');
+ if (players.length != 1) return false;
+ let player = players[0];
+ 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"""
+ {self.js_has_player}
+ let players = document.getElementsByTagName('video');
+ if (players.length != 1) return false;
+ let player = players[0];
+ 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