M melon/application.py => melon/application.py +2 -1
@@ 4,7 4,7 @@ gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio, GLib
from melon.window import MainWindow
-from melon.models import init_db
+from melon.models import init_db, notify
from melon.servers.utils import get_server_instance, load_server, get_servers_list
class Application(Adw.Application):
@@ 19,6 19,7 @@ class Application(Adw.Application):
instance = get_server_instance(server_data)
load_server(instance)
self.connect('activate', self.on_activate)
+ self.connect("shutdown", lambda _: notify("quit"))
def on_activate(self, app):
self.win = MainWindow(application=app)
M melon/home/__init__.py => melon/home/__init__.py +6 -5
@@ 3,6 3,7 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
from gi.repository import Gtk, Adw, Gio
+from gettext import gettext as _
from melon.home.new import NewFeed
from melon.home.subs import Subscriptions
@@ 14,7 15,7 @@ from melon.widgets.iconbutton import IconButton
class HomeScreen(Adw.NavigationPage):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.set_title("Home")
+ self.set_title(_("Home"))
# main stack view
self.view_stack = Adw.ViewStack()
@@ 35,16 36,16 @@ class HomeScreen(Adw.NavigationPage):
self.header_bar.set_title_widget(self.switcher)
# add browse servers button
self.add_server_button = IconButton("", "list-add-symbolic")
- self.add_server_button.set_tooltip_text("Browse Servers")
+ self.add_server_button.set_tooltip_text(_("Browse Servers"))
self.add_server_button.set_action_name("win.browse")
self.header_bar.pack_start(self.add_server_button)
# add menu to top bar
self.menu_button = Gtk.MenuButton()
self.menu_button.set_icon_name("open-menu-symbolic")
model = Gio.Menu()
- model.append("Preferences", "win.prefs")
- model.append("Import Data", "win.import")
- model.append("About Melon", "win.about")
+ model.append(_("Preferences"), "win.prefs")
+ model.append(_("Import Data"), "win.import")
+ model.append(_("About Melon"), "win.about")
self.menu_popover = Gtk.PopoverMenu()
self.menu_popover.set_menu_model(model)
self.menu_button.set_popover(self.menu_popover)
M melon/home/new.py => melon/home/new.py +37 -1
@@ 14,6 14,7 @@ from melon.servers.utils import fetch_home_feed, group_by_date
from melon.models import register_callback
from melon.models import get_subscribed_channels, get_app_settings
from melon.models import get_cached_feed, clear_cached_feed, update_cached_feed, get_last_feed_refresh
+from melon.models import get_last_playback
class NewFeed(ViewStackPage):
def update(self):
@@ 109,12 110,47 @@ class NewFeed(ViewStackPage):
self.thread.daemon = True
self.thread.start()
+ ov_toast = None
+ def load_overlay(self):
+ last = get_last_playback()
+ if not last is None:
+ pos = last[1]
+ dur = last[2]
+ if dur == 0:
+ # video has no length, why would anybody continue watching it
+ last = None
+ elif pos > dur*0.95:
+ # video was already played >0.95 percent
+ # so we can consider it watched
+ # TODO: move into settings menu
+ last = None
+ if last is None:
+ if not self.ov_toast is None:
+ self.ov_toast.dismiss()
+ return
+ if not self.ov_toast is None:
+ self.ov_toast.dismiss()
+ self.ov_toast = None
+ resource = last[0]
+ self.ov_toast = Adw.Toast.new(_("Pick up where you left off"))
+ self.ov_toast.set_button_label(_("Watch"))
+ self.ov_toast.set_action_name("win.player")
+ self.ov_toast.set_action_target_value(
+ GLib.Variant("as", [resource.server, resource.id]))
+ self.overlay.add_toast(self.ov_toast)
+ self.ov_toast.set_timeout(20)
+
def __init__(self):
super().__init__("feed-new", _("What's new"), "user-home-symbolic")
self.widget = Adw.Clamp()
+ self.overlay = Adw.ToastOverlay()
self.inner = Gtk.ScrolledWindow()
- self.widget.set_child(self.inner)
+ self.overlay.set_child(self.inner)
+ self.widget.set_child(self.overlay)
# register update listener
register_callback("channels_changed", "home-new", self.do_update)
+ register_callback("playback_changed", "home-new", self.load_overlay)
+ # load overlay
+ self.load_overlay()
# manually run render
self.do_update()
M melon/models/__init__.py => melon/models/__init__.py +80 -4
@@ 5,6 5,7 @@ from functools import cache
import sqlite3
import os
import json
+import sys
from melon.servers import Video, Channel, Playlist, Resource
from melon.utils import get_data_dir
@@ 74,7 75,7 @@ def execute_sql(conn:sqlite3.Connection, sql, *sqlargs, many=False) -> bool:
else:
return True
-VERSION = 1
+VERSION = 2
app_conf_template = {
# NOTE: they are set to False to save bandwidth
@@ 111,6 112,8 @@ def init_db():
# "Houston, we have a problem"
return
version = conn.execute("PRAGMA user_version").fetchone()[0]
+ if version > VERSION:
+ sys.exit(1)
if version == 0:
# initial version (defaults to 0, so this means the DB isn't initialized)
# perform initial setup
@@ 243,12 246,36 @@ def init_db():
FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server)
)
""")
+ execute_sql(conn, """
+ CREATE TABLE playback(
+ timestamp,
+ video_server,
+ video_id,
+ duration,
+ position,
+ PRIMARY KEY(video_id, video_server),
+ FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server)
+ )
+ """)
# the initial setup should always initialize the newest version
# and not depend on migrations
version = VERSION
# MIGRATIONS go here
- # if minv < = version <= maxv:
+ # if minv <= version <= maxv:
+ if version < 2:
+ # newly added in db v2
+ execute_sql(conn, """
+ CREATE TABLE playback(
+ timestamp,
+ video_server,
+ video_id,
+ duration,
+ position,
+ PRIMARY KEY(video_id, video_server),
+ FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server)
+ )
+ """)
# MIGRATIONS FINISHED
execute_sql(conn, f"PRAGMA user_version = {VERSION}") or die()
@@ 284,6 311,7 @@ def load_server(server):
if server.requires_login:
ensure_server_disabled(sid)
servers[sid]["enabled"] = False
+ conn.close()
notify("settings_changed")
def set_server_setting(sid:str, pref:str, value):
init_server_settings(sid)
@@ 291,13 319,14 @@ def set_server_setting(sid:str, pref:str, value):
execute_sql(conn,
"INSERT OR REPLACE INTO server_settings VALUES (?,?,?)",
(sid, pref, value))
+ conn.close()
notify("settings_changed")
def get_server_settings(sid:str):
init_server_settings(sid)
base = servers[sid]
- conn = connect_to_db()
value = is_server_enabled(sid)
base["enabled"] = value
+ conn = connect_to_db()
results = conn.execute(
"SELECT key, value FROM server_settings WHERE server = ?", (sid,)).fetchall()
for setting in results:
@@ 329,6 358,7 @@ def ensure_server(server_id: str, mode: bool):
VALUES (?, ?)
""", (server_id, mode))
# notify channels and playlists, because available channels might be different
+ conn.close()
notify("channels_changed")
notify("playlists_changed")
notify("settings_changed")
@@ 353,6 383,7 @@ def set_app_setting(pref:str, value):
WHERE key = ?
""", (value, pref))
# notify channels and playlists, because available channels might be different
+ conn.close()
notify("channels_changed")
notify("playlists_changed")
notify("settings_changed")
@@ 430,6 461,7 @@ def ensure_video(vid):
VALUES (?,?,?,?,?,?,?,?)
""",
(vid.server, vid.id, vid.url, vid.title, vid.description, vid.thumbnail, vid.channel[1], vid.channel[0]))
+ conn.close()
def add_videos(vids:list[Video]):
conn = connect_to_db()
execute_sql(
@@ 440,6 472,7 @@ def add_videos(vids:list[Video]):
""",
[ (vid.server, vid.id, vid.url, vid.title, vid.description, vid.thumbnail, vid.channel[1], vid.channel[0]) for vid in vids],
many=True)
+ conn.close()
notify("history_changed")
def has_bookmarked_external_playlist(server_id: str, playlist_id:str) -> bool:
@@ 484,6 517,7 @@ def ensure_playlist(playlist: PlaylistWrapper):
VALUES (?,?,?,?,?,?,?,?)
""",
(playlist.inner.server, playlist.inner.id, playlist.inner.url, playlist.inner.title, playlist.inner.description,playlist.inner.channel[1], playlist.inner.channel[0], playlist.inner.thumbnail))
+ conn.close()
notify("playlists_changed")
def ensure_bookmark_external_playlist(playlist: Playlist):
wrapped = PlaylistWrapper.from_external(playlist)
@@ 496,6 530,7 @@ def ensure_bookmark_external_playlist(playlist: Playlist):
""",
(playlist.server, playlist.id))
# we need to rerun the notification because now the playlist was linked
+ conn.close()
notify("playlists_changed")
def ensure_unbookmark_external_playlist(server_id: str, playlist_id: str):
# only removes the playlist from the linking table
@@ 505,6 540,7 @@ def ensure_unbookmark_external_playlist(server_id: str, playlist_id: str):
WHERE server = ?, playlist_id = ?
""",
(server_id, playlist_id))
+ conn.close()
notify("playlists_changed")
def new_local_playlist(name: str, description: str, videos=[]) -> int:
"""Create a new local playlist and return id"""
@@ 519,6 555,7 @@ def new_local_playlist(name: str, description: str, videos=[]) -> int:
for vid in videos:
add_to_local_playlist(id, vid, index)
index += 1
+ conn.close()
notify("playlists_changed")
return id
def ensure_delete_local_playlist(playlist_id:int):
@@ 568,6 605,7 @@ def add_to_local_playlist(playlist_id:int, vid, pos=None):
INSERT INTO local_playlist_content
VALUES (?, ?, ?, ?)
""", (playlist_id, vid.id, vid.server, pos))
+ conn.close()
notify("playlists_changed")
def get_local_playlist(playlist_id:int) -> LocalPlaylist:
conn = connect_to_db()
@@ 593,6 631,7 @@ def set_local_playlist_thumbnail(playlist_id: int, thumb: str):
SET thumbnail = ?
WHERE id = ?
""", (thumb, playlist_id))
+ conn.close()
notify("playlists_changed")
def add_to_history(vid, uts=None):
@@ 609,7 648,9 @@ def add_to_history(vid, uts=None):
VALUES (?, ?, ?)
""",
(uts, vid.id, vid.server))
+ conn.close()
notify("history_changed")
+ return False
def add_history_items(items:list[(Video, int)]):
# NOTE: you have to ensure the videos exist yourself
conn = connect_to_db()
@@ 621,6 662,7 @@ def add_history_items(items:list[(Video, int)]):
""",
[(d[1], d[0].id, d[0].server) for d in items],
many=True)
+ conn.close()
notify("history_changed")
def get_history():
@@ 678,8 720,42 @@ def update_cached_feed(ls: list[(Video, int)]):
""",
(uts, vid.id, vid.server))
def get_last_feed_refresh() -> int:
- conn = connect_to_db()
app_conf = get_app_settings()
if "news-feed-refresh" in app_conf:
return app_conf["news-feed-refresh"]
return None
+
+def get_video_playback_position(server_id:str, video_id:str) -> (float|None):
+ conn = connect_to_db()
+ results = conn.execute("""
+ SELECT position
+ FROM playback
+ WHERE video_id = ? AND video_server = ?
+ """, (video_id, server_id)).fetchone()
+ if results is None:
+ return None
+ return results[0]
+
+def set_video_playback_position(vid: Video, position: float, duration: float):
+ uts = datetime.now().timestamp()
+ ensure_video(vid)
+ conn = connect_to_db()
+ execute_sql(conn, """
+ INSERT OR REPLACE INTO playback
+ VALUES (?,?,?,?,?)
+ """, (uts, vid.server, vid.id, duration, position))
+ conn.close()
+ notify("playback_changed")
+
+def get_last_playback() -> (tuple[Video, float, float] | None):
+ conn = connect_to_db()
+ results = conn.execute("""
+ SELECT server, id, url, title, description, thumbnail, channel_id, channel_name, position, duration
+ FROM playback join videos ON playback.video_id = videos.id AND playback.video_server = videos.server
+ ORDER BY playback.timestamp DESC
+ LIMIT 1
+ """).fetchone()
+ if results is None:
+ return None
+ d = results
+ return (Video(d[0], d[2], d[1], d[3], (d[7], d[6]), d[4], d[5]), d[8], d[9])
M melon/models/callbacks.py => melon/models/callbacks.py +3 -1
@@ 10,7 10,9 @@ callbacks = {
"channels_changed": {},
"playlists_changed": {},
"history_changed": {},
- "settings_changed": {}
+ "settings_changed": {},
+ "playback_changed": {},
+ "quit": {}
}
frozen = False
def freeze():
M melon/player/__init__.py => melon/player/__init__.py +97 -4
@@ 12,9 12,13 @@ from melon.servers import SearchMode
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, notify, add_to_history
+from melon.models import get_app_settings, add_to_history, register_callback
+from melon.models import get_video_playback_position, set_video_playback_position
+from melon.utils import pass_me
class PlayerScreen(Adw.NavigationPage):
+ duration = None
+ position = None
def on_open_in_browser(self, arg):
Gtk.UriLauncher.new(uri=self.video.url).launch()
@@ 53,7 57,9 @@ class PlayerScreen(Adw.NavigationPage):
self.view = WebKit.WebView()
self.view.set_hexpand(True)
self.view.set_vexpand(True)
+ self.view.connect("load-changed", self.on_load)
default_stream = self.streams[0].quality
+ # load default stream
self.select_stream(default_stream)
self.box.append(self.view)
# stream selector
@@ 65,13 71,17 @@ class PlayerScreen(Adw.NavigationPage):
[ unidecode(stream.quality) for stream in self.streams ],
default_stream)
row = PreferenceRow(pref)
- row.set_callback(self.select_stream)
+ # pass true to select_stream to store the current playback position
+ # and restore it after changing it
+ row.set_callback(pass_me(self.select_stream, True))
self.quality_select = row.get_widget()
+ return False
channel = None
def display_channel(self):
if not self.channel is None:
self.about.add(AdaptiveFeedItem(self.channel))
+ return False
def display_error(self):
text = Gtk.Label()
@@ 79,6 89,7 @@ class PlayerScreen(Adw.NavigationPage):
cb = Gtk.CenterBox()
cb.set_center_widget(text)
self.scrollview.set_child(cb)
+ return False
def background(self, video_id):
# obtain video_information
@@ 96,11 107,16 @@ class PlayerScreen(Adw.NavigationPage):
GLib.idle_add(self.display_info)
# get channel details
- channel = self.instance.get_channel_info(self.video.channel[1])
+ self.channel = self.instance.get_channel_info(self.video.channel[1])
GLib.idle_add(self.display_channel)
def __init__(self, server_id, video_id, *args, **kwargs):
super().__init__(*args, **kwargs)
+
+ # reset these for now
+ self.position = None
+ self.duration = None
+
self.video_id = video_id
# get instance handle
server = get_servers_list()[server_id]
@@ 136,12 152,89 @@ class PlayerScreen(Adw.NavigationPage):
self.box = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)
# start background thread
+ self.visible = True
self.thread = threading.Thread(target=self.background, args=[video_id])
self.thread.daemon = True
self.thread.start()
- def select_stream(self, quality):
+ def on_hide(self):
+ self.visible = False
+ # save playback position & duration when closing the video
+ # NOTE: this does not include closing the app
+ self.store_position()
+ def on_quit(self):
+ self.commit_playback()
+ def loop(self):
+ if not self.visible:
+ return False
+ # store playback position locally every couple of seconds
+ self.store_position(None, False)
+ # add this function to the queue (in 2sec)
+ # better than running it every 2seconds
+ # in case it takes longer than 2 seconds to run
+ # as it doesn't push as many tasks into the queue
+ GLib.timeout_add_seconds(2, self.loop)
+ return False
+
+ def on_load(self, view, event):
+ if event == self.instance.WEBVIEW_READY:
+ # video meta is ready now
+ # update playback position from db
+ pos = get_video_playback_position(self.video.server, self.video.id)
+ if not pos is None:
+ 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)
+
+ def on_duration(self, duration):
+ self.duration = duration
+ pos = self.position
+ if not pos is None:
+ self.instance.set_video_playback_position(self.view, pos, lambda s: self.on_player_setup_done())
+ else:
+ self.on_player_setup_done()
+ def on_player_setup_done(self):
+ self.commit_playback()
+ # connect to events
+ self.connect("hiding", lambda _: self.on_hide())
+ register_callback("quit", "player", self.commit_playback)
+ # runs loop every two seconds
+ # should probably happen every second,
+ # but I fear that this would consume to many resources
+ #self.loop()
+ self.instance.connect_video_ended(self.view, self.on_end)
+
+ def on_end(self, state):
+ print("BYE")
+
+ def select_stream(self, quality, store_position=False):
+ # get current playback position & save to db
+ # redirects to select_stream2 where the stream will be replaced
+ if store_position:
+ self.store_position(pass_me(self.select_stream2, quality))
+ else:
+ self.select_stream2(quality)
+ def select_stream2(self, quality):
+ # set stream details
for stream in self.streams:
if stream.quality == quality:
self.view.load_uri(stream.url)
break
+
+ 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 commit:
+ self.commit_playback()
+ if not after is None:
+ after()
+
+ def commit_playback(self):
+ # only save to db if playhead position and video duration are known
+ if self.position is None or self.duration is None:
+ return
+ set_video_playback_position(self.video, self.position, self.duration)
M melon/servers/__init__.py => melon/servers/__init__.py +45 -1
@@ 3,7 3,9 @@ from abc import ABC,abstractmethod
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
-from gi.repository import GObject
+gi.require_version('WebKit', '6.0')
+from gi.repository import GObject, WebKit
+from typing import Callable
from melon.servers.loader import server_finder
from melon.import_providers import ImportProvider
@@ 249,4 251,46 @@ class Server(ABC):
"""
return []
def get_import_providers(self) -> list[ImportProvider]:
+ """
+ Returns a list of import providers,
+ that import data used by this server
+ """
return []
+
+ WEBVIEW_READY = None
+
+ def get_video_duration(self, webview: WebKit.WebView, on_data: Callable[[(None|float)], None]):
+ """
+ Should attempt to get the video duration
+ using the player in the webview
+ Should call onData once completed
+ with the video duration in seconds
+ or None if it isn't possible to obtain i.e. for a livestream
+ """
+ on_data(None)
+
+ def get_video_playback_position(self, webview: WebKit.WebView, on_data: Callable[[None|float], None]):
+ """
+ Should attempt to get the video playback position
+ from the webview i.e. by executing javascript to read html elements
+ Should call onData once complete
+ with the video playback position in seconds
+ or None if it couldn't detect the playback position
+ """
+ on_data(None)
+ def set_video_playback_position(self, webview: WebKit.WebView, timestamp: float, on_done: Callable[[bool], None]):
+ """
+ Takes the video playback position in seconds
+ Should attempt to set the video playback position in the webview
+ i.e. by calling a javascript function to set the position
+ Should call onDone with true after successfully setting the position
+ pass false if there was a problem
+ """
+ on_done(False)
+ def connect_video_ended(self, webview: WebKit.WebView, on_done: Callable[[bool], None]):
+ """
+ Should call onDone once the video ended
+ only has to work the first time
+ should pass False to onDone if there was an error
+ """
+ on_done(False)
M melon/servers/invidious/__init__.py => melon/servers/invidious/__init__.py +138 -0
@@ 3,11 3,16 @@ 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.import_providers.newpipe import NewpipeImporter
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
class NewpipeInvidiousImporter(NewpipeImporter):
server_id = "invidious"
@@ 310,3 315,136 @@ class Invidious(Server):
return [
NewpipeInvidiousImporter(self.get_external_url())
]
+
+ #
+ # WEBVIEW CONTROL BRIDGE
+ #
+ 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; }
+ let list = player.addEventListener(
+ 'loadedmetadata',
+ ()=>{
+ player.removeEventListener('loadedmetadata', list);
+ res();
+ });
+ });
+ }
+ if (player.readyState < min_ready_state) {
+ await new Promise((res) => {
+ if (player.readyState >= min_ready_state) { res(); return undefined; }
+ let list = player.addEventListener(
+ 'loadeddata',
+ ()=>{
+ if (player.readyState >= min_ready_state) {
+ player.removeEventListener('loadeddata', 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 on_js_bool(self, obj, async_res, webview, on_bool):
+ if on_bool is None:
+ return
+ val = webview.call_async_javascript_function_finish(async_res)
+ 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
+ val = webview.call_async_javascript_function_finish(async_res)
+ if val.is_number():
+ on_double(val.to_double())
+ else:
+ on_double(None)
M melon/utils.py => melon/utils.py +11 -0
@@ 19,3 19,14 @@ def get_data_dir():
os.mkdir(data_dir_path)
return data_dir_path
+
+def pass_me(func, *args):
+ """
+ Pass additional arguments to lambda functions
+ """
+ return lambda *a: func(*a, *args)
+def many(*funcs):
+ """
+ Make multiple function steps easier to read
+ """
+ return [funcs]
M melon/widgets/preferencerow.py => melon/widgets/preferencerow.py +1 -5
@@ 11,6 11,7 @@ from melon.servers import Resource,Video,Playlist,Channel,Preference,PreferenceT
from melon.servers.utils import pixbuf_from_url
from melon.widgets.iconbutton import IconButton
from melon.widgets.simpledialog import SimpleDialog
+from melon.utils import pass_me, many
class PreferenceRow():
def __init__(self, pref: Preference, *args, **kwargs):
@@ 252,8 253,3 @@ class MultiRow(Adw.PreferencesRow):
val = self.values.pop(counter)
self.values.insert(counter+1, val)
self.notify()
-
-def pass_me(func, *args):
- return lambda *a: func(*a, *args)
-def many(*funcs):
- return [funcs]