From c1bed7a3364c72d4223a0cf0a22905bd7b300e31 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Mon, 11 Mar 2024 21:01:36 +0100 Subject: [PATCH] basic playlist playback mode --- melon/browse/playlist.py | 12 ++- melon/models/__init__.py | 10 +++ melon/player/__init__.py | 117 +++++++++++++++++++------ melon/player/playlist.py | 144 +++++++++++++++++++++++++++++++ melon/playlist/__init__.py | 8 +- melon/widgets/player.py | 7 ++ melon/window.py | 11 +++ po/POTFILES.in | 2 + po/de.po | 172 +++++++++++++++++++++++-------------- po/fa.po | 170 ++++++++++++++++++++++-------------- po/melon.pot | 170 ++++++++++++++++++++++-------------- 11 files changed, 596 insertions(+), 227 deletions(-) create mode 100644 melon/player/playlist.py diff --git a/melon/browse/playlist.py b/melon/browse/playlist.py index ee240c4..dee4ba0 100644 --- a/melon/browse/playlist.py +++ b/melon/browse/playlist.py @@ -31,6 +31,15 @@ class BrowsePlaylistScreen(Adw.NavigationPage): self.thread.start() def display_info(self, texture): + self.external_btn = IconButton("","modem-symbolic") + self.external_btn.connect("clicked", self.on_open_in_browser) + self.header_bar.pack_end(self.external_btn) + self.startplay_btn = IconButton("","media-playback-start-symbolic", tooltip=_("Start playing")) + self.startplay_btn.set_action_name("win.playlistplayer-external") + self.startplay_btn.set_action_target_value( + GLib.Variant("as", [self.playlist.server, self.playlist.id])) + self.header_bar.pack_end(self.startplay_btn) + # base layout self.box = Adw.PreferencesPage() self.about = Adw.PreferencesGroup() @@ -102,9 +111,6 @@ class BrowsePlaylistScreen(Adw.NavigationPage): self.set_title(_("Playlist")) self.header_bar = Adw.HeaderBar() - self.external_btn = IconButton("","modem-symbolic") - self.external_btn.connect("clicked", self.on_open_in_browser) - self.header_bar.pack_end(self.external_btn) self.toolbar_view = Adw.ToolbarView() self.toolbar_view.add_top_bar(self.header_bar) diff --git a/melon/models/__init__.py b/melon/models/__init__.py index 6df48a5..1cac642 100644 --- a/melon/models/__init__.py +++ b/melon/models/__init__.py @@ -747,6 +747,16 @@ def set_video_playback_position(vid: Video, position: float, duration: float): conn.close() notify("playback_changed") +def set_video_playback_position_force(server_id: str, video_id:str, position: float, duration: float): + uts = datetime.now().timestamp() + conn = connect_to_db() + execute_sql(conn, """ + INSERT OR REPLACE INTO playback + VALUES (?,?,?,?,?) + """, (uts, server_id, video_id, duration, position)) + conn.close() + notify("playback_changed") + def get_last_playback() -> (tuple[Video, float, float] | None): conn = connect_to_db() results = conn.execute(""" diff --git a/melon/player/__init__.py b/melon/player/__init__.py index 9901c1e..5173638 100644 --- a/melon/player/__init__.py +++ b/melon/player/__init__.py @@ -15,22 +15,19 @@ 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 +from melon.models import get_video_playback_position, set_video_playback_position, set_video_playback_position_force from melon.utils import pass_me class PlayerScreen(Adw.NavigationPage): def on_open_in_browser(self, arg): Gtk.UriLauncher.new(uri=self.video.url).launch() - quality_select = None def display_info(self): self.set_title(self.video.title) self.scrollview.set_child(self.box) # video details self.about = Adw.PreferencesGroup() self.about.set_title(unidecode(self.video.title).replace("&","&")) - if not self.quality_select is None: - self.about.add(self.quality_select) # expandable description field desc_field = Adw.ExpanderRow() @@ -54,34 +51,60 @@ class PlayerScreen(Adw.NavigationPage): self.about.add(btn_bookmark) def display_player(self): - player = VideoPlayer(self.streams) + self.position = 0 + self.duration = 0 + self.player = VideoPlayer(self.streams) pos = get_video_playback_position(self.video.server, self.video.id) if not pos is None: - player.goto(pos) - player.connect_update(self.on_player_data) + self.position = pos + self.player.goto(pos) + self.player.connect_update(self.on_player_data) + self.player.connect_ended(self.on_player_end) # do not autoplay - player.pause() - self.box.append(player) + self.player.pause() + self.box.append(self.player) # TODO: stop + kill player on hide + # TODO: connect this to navpage close + def on_close(self): + self.disconnect_player() + + def disconnect_player(self): + if not self.player is None: + self.player.connect_ended(None) + self.player.connect_update(None) + + def on_player_end(self): + if self.position is None or self.duration is None: + # we only want to save valid datasets + return + set_video_playback_position(self.video, self.position, self.duration) + self.disconnect_player() def on_player_data(self, position, duration): if position is None or duration is None: # we only want to save valid datasets return - set_video_playback_position(self.video, position, duration) + if self.position is None or self.duration is None: + # comparisons wouldn't be possible + return + if int(self.position) != int(position) or int(self.duration) != int(duration): + # only commit data if the second changed + set_video_playback_position(self.video, position, duration) + self.position = position + self.duration = duration - 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() - text.set_label(_("Video could not be loaded")) - cb = Gtk.CenterBox() - cb.set_center_widget(text) - self.scrollview.set_child(cb) + status = Adw.StatusPage() + status.set_title(_("*crickets chirping*")) + subs = get_subscribed_channels() + status.set_description(_("Video could not be loaded")) + status.set_icon_name("weather-few-clouds-night-symbolic") + self.scrollview.set_child(status) return False def background(self, video_id): @@ -106,19 +129,18 @@ class PlayerScreen(Adw.NavigationPage): def __init__(self, server_id, video_id, *args, **kwargs): super().__init__(*args, **kwargs) - self.video_id = video_id - # get instance handle - server = get_servers_list()[server_id] - self.instance = get_server_instance(server) + self.external_btn = None + self.channel = None + self.player = None + + # skip init + if server_id is None or video_id is None: + return # show fallback title - self.set_title(_("Player")) + self.set_title(_("Loading...")) self.header_bar = Adw.HeaderBar() - self.external_btn = IconButton("","modem-symbolic") - self.external_btn.connect("clicked", self.on_open_in_browser) - self.header_bar.pack_end(self.external_btn) - self.toolbar_view = Adw.ToolbarView() self.toolbar_view.add_top_bar(self.header_bar) self.set_child(self.toolbar_view) @@ -127,6 +149,49 @@ class PlayerScreen(Adw.NavigationPage): self.toolbar_view.set_content(self.wrapper) self.scrollview = Gtk.ScrolledWindow() + self.wrapper.set_child(self.scrollview) + + self.init_video(server_id, video_id) + + def init_video(self, server_id, video_id): + # reset these for now + self.position = None + self.duration = None + + self.video_id = video_id + # get instance handle + server = get_servers_list()[server_id] + self.instance = get_server_instance(server) + + # show fallback title + # in case init_video is called twice, + # this resets the title + self.set_title(_("Loading...")) + + # 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) + if not pos is None and not dur is None: + # TODO consider moving this to the settings panel + consider_done = 0.99*dur + # video was probably watched till end + # reset playback position + if pos >= consider_done: + # we can use force here, because if pos and dur aren't None + # set_video_playback_position was called before so the video exists + set_video_playback_position_force(server_id, video_id, 0, dur) + + self.video_id = video_id + # get instance handle + server = get_servers_list()[server_id] + self.instance = get_server_instance(server) + + if not self.external_btn is None: + self.header_bar.remove(self.external_btn) + self.external_btn = IconButton("","modem-symbolic") + self.external_btn.connect("clicked", self.on_open_in_browser) + self.header_bar.pack_end(self.external_btn) + # show spinner # will be cleared by display_info spinner = Gtk.Spinner() @@ -136,8 +201,6 @@ class PlayerScreen(Adw.NavigationPage): cb.set_center_widget(spinner) self.scrollview.set_child(cb) - self.wrapper.set_child(self.scrollview) - self.box = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) # add a padding to the box # so the preference groups do not touch the edge on mobile diff --git a/melon/player/playlist.py b/melon/player/playlist.py new file mode 100644 index 0000000..229a5d9 --- /dev/null +++ b/melon/player/playlist.py @@ -0,0 +1,144 @@ +import gi +gi.require_version("WebKit", "6.0") +gi.require_version('Gtk', '4.0') +gi.require_version('Adw', '1') +from gi.repository import Gtk, Adw, WebKit, GLib +from unidecode import unidecode +from gettext import gettext as _ +import threading + +from melon.servers.utils import get_server_instance, get_servers_list +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, add_to_history, register_callback +from melon.models import get_video_playback_position, set_video_playback_position, set_video_playback_position_force +from melon.utils import pass_me + +from melon.models import get_local_playlist, PlaylistWrapper, PlaylistWrapperType + +from melon.player import PlayerScreen + +class PlaylistPlayerScreen(PlayerScreen): + playlist = None + playlist_index = 0 + playlist_instance = None + playlist_content = [] + + def __init__(self, ref: (tuple[str, str] | int), index=0, *args, **kwargs): + # make sure to initalize widget + # PlayerScreen.__init__ will break after initializing parents + # because neither server or video are supplied + super().__init__(None, None) + + self.playlist_index = index + self.playlist = None + self.video_duration = None + self.video_position = None + self.playlist_instance = None + + # prepare base layout + self.header_bar = Adw.HeaderBar() + self.set_title(_("Loading...")) + # show spinner + self.toolbar_view = Adw.ToolbarView() + self.toolbar_view.add_top_bar(self.header_bar) + self.set_child(self.toolbar_view) + + self.wrapper = Adw.Clamp() + self.toolbar_view.set_content(self.wrapper) + + self.scrollview = Gtk.ScrolledWindow() + # show spinner + # will be overwritten by init_load_video + spinner = Gtk.Spinner() + spinner.set_size_request(50, 50) + spinner.start() + cb = Gtk.CenterBox() + cb.set_center_widget(spinner) + self.scrollview.set_child(cb) + self.wrapper.set_child(self.scrollview) + + # start prepare_playlist thread + self.visible = True + self.thread = threading.Thread(target=self.prepare_playlist_task, args=[ref]) + self.thread.daemon = True + self.thread.start() + + # fetch playlist data & store it interally + def prepare_playlist_task(self, ref: (tuple[str, str] | int)): + if isinstance(ref, int): + playlist_id = ref + # local playlist + pl = get_local_playlist(playlist_id) + self.playlist = PlaylistWrapper.from_local(pl) + self.playlist_content = pl.content + elif isinstance(ref, tuple): + server_id = ref[0] + playlist_id = ref[1] + # external playlist + server = get_servers_list()[server_id] + self.playlist_instance = get_server_instance(server) + pl = self.playlist_instance.get_playlist_info(playlist_id) + self.playlist = PlaylistWrapper.from_external(pl) + cont = self.playlist_instance.get_playlist_content(playlist_id) + self.playlist_content = cont + else: + # shouldn't be reachable + # if properly used + # (see argument type bounds) + GLib.idle_add(self.playlist_error) + return + + # show empty playlist screen if there are no videos + if not self.playlist_content: + GLib.idle_add(self.empty_playlist) + return + + # show playlist error screen if something didn't quite work out + if self.playlist_index > len(self.playlist_content): + GLib.idle_add(self.playlist_error) + return + self.prepare(0) + + def prepare(self, index): + dt = self.playlist_content[index] + server_id = dt.server + video_id = dt.id + + GLib.idle_add(self.init_video, server_id, video_id) + + def on_player_end(self): + super().on_player_end() + # if there are more videos in the playlist + # load the next one + # do nothing if there are no more videos + if self.playlist_index+1 < len(self.playlist_content): + self.playlist_index += 1 + self.prepare(self.playlist_index) + + def empty_playlist(self): + # display playlist is empty banner + # acts as a drop in replacement for the player screen + status = Adw.StatusPage() + status.set_title(_("*crickets chirping*")) + subs = get_subscribed_channels() + status.set_description(_("This playlist is empty")) + status.set_icon_name("weather-few-clouds-night-symbolic") + self.scrollview.set_child(status) + + def playlist_error(self): + status = Adw.StatusPage() + status.set_title(_("*crickets chirping*")) + subs = get_subscribed_channels() + status.set_description(_("There was an error loading the playlist")) + status.set_icon_name("weather-few-clouds-night-symbolic") + self.scrollview.set_child(status) + + def display_player(self): + super().display_player() + # the super method pauses video by default + # however in playlist mode it makes sense to always autoplay + # TODO: consider not autoplaying the first title? + self.player.play() diff --git a/melon/playlist/__init__.py b/melon/playlist/__init__.py index 1c43eae..aa398c1 100644 --- a/melon/playlist/__init__.py +++ b/melon/playlist/__init__.py @@ -2,7 +2,7 @@ import sys import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') -from gi.repository import Gtk, Adw +from gi.repository import Gtk, Adw, GLib from unidecode import unidecode from gettext import gettext as _ @@ -39,6 +39,12 @@ class LocalPlaylistScreen(Adw.NavigationPage): edit_button = IconButton(_("Edit"), "document-edit-symbolic") edit_button.connect("clicked", lambda _: self.open_edit()) + self.startplay_btn = IconButton("","media-playback-start-symbolic", tooltip=_("Start playing")) + self.startplay_btn.set_action_name("win.playlistplayer-local") + self.startplay_btn.set_action_target_value( + GLib.Variant("u", self.playlist.id)) + self.header_bar.pack_end(self.startplay_btn) + if len(playlist.content) == 0: status = Adw.StatusPage() status.set_title(_("*crickets chirping*")) diff --git a/melon/widgets/player.py b/melon/widgets/player.py index c7fe255..13f2fb7 100644 --- a/melon/widgets/player.py +++ b/melon/widgets/player.py @@ -24,6 +24,7 @@ class VideoPlayer(Gtk.Overlay): self.duration = None self.paused = True self.update_callback=None + self.ended_callback=None overlay = Gtk.Overlay() self.set_child(overlay) @@ -129,6 +130,8 @@ class VideoPlayer(Gtk.Overlay): def connect_update(self, callback=None): self.update_callback = callback + def connect_ended(self, callback=None): + self.ended_callback = callback def _show_controls(self): self.controls.set_opacity(1) @@ -184,6 +187,10 @@ class VideoPlayer(Gtk.Overlay): def _loop(self): dur = self.source.query_duration(Gst.Format.TIME) pos = self.source.query_position(Gst.Format.TIME) + # compare the nanoseconds values before converting them to seconds + # so we don't have to deal with float arithmetics + if pos[0] and dur[0] and pos[1] == dur[1] and not self.ended_callback is None: + self.ended_callback() if dur[0]: # convert nanoseconds to senconds dur = dur[1]/(10**9) diff --git a/melon/window.py b/melon/window.py index a421b74..2c52d33 100644 --- a/melon/window.py +++ b/melon/window.py @@ -15,6 +15,7 @@ from melon.servers.utils import get_servers_list, get_server_instance from melon.settings import SettingsScreen from melon.importer import ImporterScreen from melon.player import PlayerScreen +from melon.player.playlist import PlaylistPlayerScreen from melon.widgets.simpledialog import SimpleDialog from melon.playlist import LocalPlaylistScreen from melon.playlist.pick import PlaylistPickerDialog @@ -66,6 +67,13 @@ class MainWindow(Adw.ApplicationWindow): def open_local_playlist_viewer(self, action, prefs): self.view.push(LocalPlaylistScreen(prefs.unpack())) + def open_playlistplayer_local(self, action, prefs): + self.view.push(PlaylistPlayerScreen(prefs.unpack())) + def open_playlistplayer_external(self, action, prefs): + server_id = prefs[0] + playlist_id = prefs[1] + self.view.push(PlaylistPlayerScreen((server_id, playlist_id))) + # Opens the about app screen def open_about(self,action,prefs): dialog = Adw.AboutWindow() @@ -128,6 +136,9 @@ class MainWindow(Adw.ApplicationWindow): self.reg_action("add_to_playlist", self.open_playlist_picker, "as") # same as new_playlist but with [server_id, video_id] to identify a video self.reg_action("add_to_new_playlist", self.open_playlist_creator, "as") + # playlist player + self.reg_action("playlistplayer-local", self.open_playlistplayer_local, "u") + self.reg_action("playlistplayer-external", self.open_playlistplayer_external, "as") def reg_action(self, name, func, variant=None, target=None): vtype = None diff --git a/po/POTFILES.in b/po/POTFILES.in index fa04de0..a643f57 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -5,6 +5,7 @@ melon/browse/channel.py melon/browse/playlist.py melon/browse/search.py melon/browse/server.py +melon/home/__init__.py melon/home/history.py melon/home/new.py melon/home/playlists.py @@ -12,6 +13,7 @@ melon/home/subs.py melon/import_providers/newpipe.py melon/importer.py melon/player/__init__.py +melon/player/playlist.py melon/playlist/__init__.py melon/playlist/create.py melon/playlist/pick.py diff --git a/po/de.po b/po/de.po index 2d65d21..b1dbf8a 100644 --- a/po/de.po +++ b/po/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Melon 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-14 20:08+0100\n" +"POT-Creation-Date: 2024-03-15 08:56+0100\n" "PO-Revision-Date: 2024-03-01 11:23+0000\n" "Last-Translator: Anonymous \n" "Language-Team: German \n" "Language-Team: Persian \n" "Language-Team: LANGUAGE \n" @@ -74,9 +74,11 @@ msgstr "" #: ../melon/browse/__init__.py:18 ../melon/browse/search.py:58 #: ../melon/browse/server.py:101 ../melon/browse/server.py:127 -#: ../melon/home/history.py:23 ../melon/home/new.py:28 +#: ../melon/home/history.py:23 ../melon/home/new.py:29 #: ../melon/home/playlists.py:21 ../melon/home/subs.py:20 -#: ../melon/importer.py:61 ../melon/playlist/__init__.py:44 +#: ../melon/importer.py:61 ../melon/player/__init__.py:101 +#: ../melon/player/playlist.py:125 ../melon/player/playlist.py:133 +#: ../melon/playlist/__init__.py:50 msgid "*crickets chirping*" msgstr "" @@ -125,15 +127,19 @@ msgstr "" msgid "Channel" msgstr "" -#: ../melon/browse/playlist.py:47 ../melon/player/__init__.py:45 +#: ../melon/browse/playlist.py:37 ../melon/playlist/__init__.py:42 +msgid "Start playing" +msgstr "" + +#: ../melon/browse/playlist.py:56 ../melon/player/__init__.py:44 msgid "Bookmark" msgstr "" -#: ../melon/browse/playlist.py:48 +#: ../melon/browse/playlist.py:57 msgid "Add Playlist to your local playlist collection" msgstr "" -#: ../melon/browse/playlist.py:102 +#: ../melon/browse/playlist.py:111 msgid "Playlist" msgstr "" @@ -190,14 +196,31 @@ msgstr "" msgid "This feed is empty" msgstr "" -#: ../melon/home/history.py:24 -msgid "You haven't watched any videos yet" +#: ../melon/home/__init__.py:18 +msgid "Home" msgstr "" -#: ../melon/home/history.py:26 ../melon/home/new.py:35 ../melon/home/subs.py:23 +#: ../melon/home/__init__.py:39 ../melon/home/history.py:26 +#: ../melon/home/new.py:36 ../melon/home/subs.py:23 msgid "Browse Servers" msgstr "" +#: ../melon/home/__init__.py:46 +msgid "Preferences" +msgstr "" + +#: ../melon/home/__init__.py:47 +msgid "Import Data" +msgstr "" + +#: ../melon/home/__init__.py:48 +msgid "About Melon" +msgstr "" + +#: ../melon/home/history.py:24 +msgid "You haven't watched any videos yet" +msgstr "" + #: ../melon/home/history.py:36 ../melon/home/history.py:117 msgid "History" msgstr "" @@ -214,31 +237,39 @@ msgstr "" msgid "Load older videos" msgstr "" -#: ../melon/home/new.py:22 +#: ../melon/home/new.py:23 msgid "Refresh" msgstr "" -#: ../melon/home/new.py:31 +#: ../melon/home/new.py:32 msgid "Subscribe to a channel first, to view new uploads" msgstr "" -#: ../melon/home/new.py:33 +#: ../melon/home/new.py:34 msgid "The channels you are subscribed to haven't uploaded anything yet" msgstr "" -#: ../melon/home/new.py:54 +#: ../melon/home/new.py:55 #, python-brace-format msgid "(Last refresh: {last_refresh})" msgstr "" -#: ../melon/home/new.py:56 ../melon/home/new.py:113 +#: ../melon/home/new.py:57 ../melon/home/new.py:146 msgid "What's new" msgstr "" -#: ../melon/home/new.py:57 +#: ../melon/home/new.py:58 msgid "These are the latest videos of channels you follow" msgstr "" +#: ../melon/home/new.py:137 +msgid "Pick up where you left off" +msgstr "" + +#: ../melon/home/new.py:138 +msgid "Watch" +msgstr "" + #: ../melon/home/playlists.py:22 msgid "You don't have any playlists yet" msgstr "" @@ -297,102 +328,111 @@ msgstr "" msgid "There are no available importer methods" msgstr "" -#: ../melon/player/__init__.py:35 +#: ../melon/player/__init__.py:34 msgid "Description" msgstr "" -#: ../melon/player/__init__.py:46 +#: ../melon/player/__init__.py:45 msgid "Add this video to a playlist" msgstr "" -#: ../melon/player/__init__.py:65 +#: ../melon/player/__init__.py:103 msgid "Video could not be loaded" msgstr "" -#: ../melon/player/__init__.py:97 -msgid "Player" +#: ../melon/player/__init__.py:139 ../melon/player/__init__.py:173 +#: ../melon/player/playlist.py:43 +msgid "Loading..." +msgstr "" + +#: ../melon/player/playlist.py:127 +msgid "This playlist is empty" +msgstr "" + +#: ../melon/player/playlist.py:135 +msgid "There was an error loading the playlist" msgstr "" #: ../melon/playlist/__init__.py:39 msgid "Edit" msgstr "" -#: ../melon/playlist/__init__.py:45 +#: ../melon/playlist/__init__.py:51 msgid "You haven't added any videos to this playlist yet" msgstr "" -#: ../melon/playlist/__init__.py:47 +#: ../melon/playlist/__init__.py:53 msgid "Start watching" msgstr "" -#: ../melon/playlist/__init__.py:77 +#: ../melon/playlist/__init__.py:83 msgid "Edit Playlist" msgstr "" -#: ../melon/playlist/__init__.py:83 +#: ../melon/playlist/__init__.py:89 msgid "Playlist details" msgstr "" -#: ../melon/playlist/__init__.py:84 +#: ../melon/playlist/__init__.py:90 msgid "Change playlist information" msgstr "" -#: ../melon/playlist/__init__.py:86 ../melon/playlist/create.py:37 +#: ../melon/playlist/__init__.py:92 ../melon/playlist/create.py:37 msgid "Playlist name" msgstr "" -#: ../melon/playlist/__init__.py:89 ../melon/playlist/create.py:40 +#: ../melon/playlist/__init__.py:95 ../melon/playlist/create.py:40 msgid "Playlist description" msgstr "" -#: ../melon/playlist/__init__.py:94 +#: ../melon/playlist/__init__.py:100 msgid "Save details" msgstr "" -#: ../melon/playlist/__init__.py:95 +#: ../melon/playlist/__init__.py:101 msgid "Change playlist title and description. (Closes the dialog)" msgstr "" -#: ../melon/playlist/__init__.py:105 ../melon/playlist/__init__.py:147 +#: ../melon/playlist/__init__.py:111 ../melon/playlist/__init__.py:153 msgid "Delete Playlist" msgstr "" -#: ../melon/playlist/__init__.py:106 +#: ../melon/playlist/__init__.py:112 msgid "Delete this playlist and it's content. This can NOT be undone." msgstr "" -#: ../melon/playlist/__init__.py:117 +#: ../melon/playlist/__init__.py:123 msgid "Close" msgstr "" -#: ../melon/playlist/__init__.py:117 +#: ../melon/playlist/__init__.py:123 msgid "Close without changing anything" msgstr "" -#: ../melon/playlist/__init__.py:150 +#: ../melon/playlist/__init__.py:156 msgid "Do you really want to delete this playlist?" msgstr "" -#: ../melon/playlist/__init__.py:151 +#: ../melon/playlist/__init__.py:157 msgid "This cannot be undone" msgstr "" -#: ../melon/playlist/__init__.py:155 ../melon/playlist/create.py:47 -#: ../melon/playlist/pick.py:70 ../melon/widgets/preferencerow.py:174 -#: ../melon/widgets/preferencerow.py:217 +#: ../melon/playlist/__init__.py:161 ../melon/playlist/create.py:47 +#: ../melon/playlist/pick.py:70 ../melon/widgets/preferencerow.py:175 +#: ../melon/widgets/preferencerow.py:218 msgid "Cancel" msgstr "" -#: ../melon/playlist/__init__.py:155 +#: ../melon/playlist/__init__.py:161 msgid "Do not delete the playlist" msgstr "" -#: ../melon/playlist/__init__.py:156 ../melon/widgets/preferencerow.py:206 -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/playlist/__init__.py:162 ../melon/widgets/preferencerow.py:207 +#: ../melon/widgets/preferencerow.py:219 msgid "Delete" msgstr "" -#: ../melon/playlist/__init__.py:156 +#: ../melon/playlist/__init__.py:162 msgid "Delete this playlist" msgstr "" @@ -420,7 +460,7 @@ msgstr "" msgid "Do not create playlist" msgstr "" -#: ../melon/playlist/create.py:48 ../melon/widgets/preferencerow.py:175 +#: ../melon/playlist/create.py:48 ../melon/widgets/preferencerow.py:176 msgid "Create" msgstr "" @@ -454,24 +494,24 @@ msgstr "" msgid "Add to Playlist" msgstr "" -#: ../melon/servers/invidious/__init__.py:27 +#: ../melon/servers/invidious/__init__.py:32 msgid "Open source alternative front-end to YouTube" msgstr "" -#: ../melon/servers/invidious/__init__.py:32 +#: ../melon/servers/invidious/__init__.py:37 msgid "Instance" msgstr "" -#: ../melon/servers/invidious/__init__.py:33 +#: ../melon/servers/invidious/__init__.py:38 msgid "" "See https://docs.invidious.io/instances/ for a list of available instances" msgstr "" -#: ../melon/servers/invidious/__init__.py:44 +#: ../melon/servers/invidious/__init__.py:49 msgid "Trending" msgstr "" -#: ../melon/servers/invidious/__init__.py:45 +#: ../melon/servers/invidious/__init__.py:50 msgid "Popular" msgstr "" @@ -611,70 +651,70 @@ msgid "" "Disabled servers won't show up in the browser or on the local/home screen" msgstr "" -#: ../melon/widgets/player.py:32 +#: ../melon/widgets/player.py:34 msgid "No streams available" msgstr "" -#: ../melon/widgets/player.py:78 ../melon/widgets/player.py:244 +#: ../melon/widgets/player.py:80 ../melon/widgets/player.py:279 msgid "Play" msgstr "" -#: ../melon/widgets/player.py:99 +#: ../melon/widgets/player.py:101 msgid "Video quality" msgstr "" -#: ../melon/widgets/player.py:238 +#: ../melon/widgets/player.py:273 msgid "Pause" msgstr "" -#: ../melon/widgets/preferencerow.py:115 +#: ../melon/widgets/preferencerow.py:116 msgid "Add" msgstr "" -#: ../melon/widgets/preferencerow.py:129 +#: ../melon/widgets/preferencerow.py:130 msgid "Move up" msgstr "" -#: ../melon/widgets/preferencerow.py:140 +#: ../melon/widgets/preferencerow.py:141 msgid "Move down" msgstr "" -#: ../melon/widgets/preferencerow.py:149 +#: ../melon/widgets/preferencerow.py:150 msgid "Remove from list" msgstr "" -#: ../melon/widgets/preferencerow.py:162 +#: ../melon/widgets/preferencerow.py:163 msgid "Add Item" msgstr "" -#: ../melon/widgets/preferencerow.py:165 +#: ../melon/widgets/preferencerow.py:166 msgid "Create a new list entry" msgstr "" -#: ../melon/widgets/preferencerow.py:166 +#: ../melon/widgets/preferencerow.py:167 msgid "Enter the new value here" msgstr "" -#: ../melon/widgets/preferencerow.py:169 +#: ../melon/widgets/preferencerow.py:170 msgid "Value" msgstr "" -#: ../melon/widgets/preferencerow.py:209 +#: ../melon/widgets/preferencerow.py:210 msgid "Do you really want to delete this item?" msgstr "" -#: ../melon/widgets/preferencerow.py:210 +#: ../melon/widgets/preferencerow.py:211 msgid "You won't be able to restore it afterwards" msgstr "" -#: ../melon/widgets/preferencerow.py:217 +#: ../melon/widgets/preferencerow.py:218 msgid "Do not remove item" msgstr "" -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/widgets/preferencerow.py:219 msgid "Remove item from list" msgstr "" -#: ../melon/window.py:76 +#: ../melon/window.py:84 msgid "Stream videos on the go" msgstr "" -- 2.38.5