From 30671537ff1072ca07bc7c622001cadac93272cd Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Thu, 13 Jun 2024 08:22:03 +0200 Subject: [PATCH] Format files using black --- melon/application.py | 10 +- melon/background.py | 7 +- melon/browse/__init__.py | 18 +- melon/browse/channel.py | 27 +- melon/browse/playlist.py | 31 +- melon/browse/search.py | 38 +- melon/browse/server.py | 48 +- melon/home/__init__.py | 6 +- melon/home/history.py | 24 +- melon/home/new.py | 48 +- melon/home/playlists.py | 48 +- melon/home/subs.py | 13 +- melon/import_providers/__init__.py | 8 +- melon/import_providers/newpipe.py | 98 +++-- melon/import_providers/utils.py | 14 +- melon/importer.py | 13 +- melon/models/__init__.py | 656 +++++++++++++++++++++------- melon/models/callbacks.py | 16 +- melon/player/__init__.py | 33 +- melon/player/playlist.py | 69 ++- melon/playlist/__init__.py | 93 ++-- melon/playlist/create.py | 50 ++- melon/playlist/pick.py | 58 ++- melon/servers/__init__.py | 75 +++- melon/servers/invidious/__init__.py | 149 ++++--- melon/servers/loader.py | 10 +- melon/servers/nebula/__init__.py | 117 ++--- melon/servers/peertube/__init__.py | 96 ++-- melon/servers/utils.py | 97 ++-- melon/settings/__init__.py | 129 +++--- melon/utils.py | 14 +- melon/widgets/feeditem.py | 105 +++-- melon/widgets/filterbutton.py | 4 +- melon/widgets/iconbutton.py | 10 +- melon/widgets/player.py | 168 ++++--- melon/widgets/preferencerow.py | 106 ++--- melon/widgets/simpledialog.py | 14 +- melon/widgets/viewstackpage.py | 11 +- melon/window.py | 37 +- po/de.po | 386 ++++++++-------- po/fa.po | 386 ++++++++-------- po/melon.pot | 386 ++++++++-------- po/nl.po | 392 ++++++++--------- 43 files changed, 2527 insertions(+), 1591 deletions(-) diff --git a/melon/application.py b/melon/application.py index 1976704..0cc6ed9 100644 --- a/melon/application.py +++ b/melon/application.py @@ -1,12 +1,14 @@ import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, GLib from melon.window import MainWindow from melon.models import init_db, notify from melon.servers.utils import get_server_instance, load_server, get_servers_list + class Application(Adw.Application): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -15,10 +17,10 @@ class Application(Adw.Application): # initialize db init_db() # this has to wait till the db is initialized - for _,server_data in get_servers_list().items(): + for _, server_data in get_servers_list().items(): instance = get_server_instance(server_data) load_server(instance) - self.connect('activate', self.on_activate) + self.connect("activate", self.on_activate) self.connect("shutdown", lambda _: notify("quit")) def on_activate(self, app): diff --git a/melon/background.py b/melon/background.py index f387a28..d65b22c 100644 --- a/melon/background.py +++ b/melon/background.py @@ -1,15 +1,18 @@ import threading from time import sleep -class Queue(): + +class Queue: tasks = [] thread = None starting = False + def __init__(self, workers=1): for i in range(workers): self.thread = threading.Thread(target=self.run, name=str(i)) self.thread.daemon = True self.thread.start() + def run(self): while True: while not self.tasks: @@ -19,7 +22,9 @@ class Queue(): args = task[1] # run task func(*args) + def add(self, func, *args): self.tasks.append((func, args)) + queue = Queue(5) diff --git a/melon/browse/__init__.py b/melon/browse/__init__.py index 09ba492..453ad98 100644 --- a/melon/browse/__init__.py +++ b/melon/browse/__init__.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, Gdk, GLib from gettext import gettext as _ @@ -9,6 +10,7 @@ from melon.widgets.iconbutton import IconButton from melon.servers.utils import get_allowed_servers_list from melon.models import get_app_settings, register_callback + class BrowseScreen(Adw.NavigationPage): def update(self): servers = get_allowed_servers_list(get_app_settings()) @@ -18,7 +20,9 @@ class BrowseScreen(Adw.NavigationPage): status.set_title(_("*crickets chirping*")) status.set_description(_("There are no available servers")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton(_("Enable servers in the settings menu"), "list-add-symbolic") + icon_button = IconButton( + _("Enable servers in the settings menu"), "list-add-symbolic" + ) icon_button.set_action_name("win.prefs") box = Gtk.CenterBox() box.set_center_widget(icon_button) @@ -27,13 +31,17 @@ class BrowseScreen(Adw.NavigationPage): else: results = Adw.PreferencesGroup() results.set_title(_("Available Servers")) - results.set_description(_("You can enable/disable and filter servers in the settings menu")) + results.set_description( + _("You can enable/disable and filter servers in the settings menu") + ) for server in servers: row = Adw.ActionRow() row.set_title(server["name"]) row.set_subtitle(server["description"]) icon = Adw.Avatar() - icon.set_custom_image(Gdk.Texture.new_from_filename(server["logo_path"])) + icon.set_custom_image( + Gdk.Texture.new_from_filename(server["logo_path"]) + ) icon.set_size(32) row.add_prefix(icon) row.add_suffix(Gtk.Image.new_from_icon_name("go-next-symbolic")) diff --git a/melon/browse/channel.py b/melon/browse/channel.py index 390248c..d98ac52 100644 --- a/melon/browse/channel.py +++ b/melon/browse/channel.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, Gdk, GLib from unidecode import unidecode from gettext import gettext as _ @@ -14,7 +15,12 @@ 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 -from melon.models import is_subscribed_to_channel, ensure_subscribed_to_channel, ensure_unsubscribed_from_channel +from melon.models import ( + is_subscribed_to_channel, + ensure_subscribed_to_channel, + ensure_unsubscribed_from_channel, +) + class BrowseChannelScreen(Adw.NavigationPage): def display_page(self, cont): @@ -24,7 +30,10 @@ class BrowseChannelScreen(Adw.NavigationPage): app_conf = get_app_settings() self.results.set_header_suffix(None) for res in cont: - self.results.add(AdaptiveFeedItem(res, show_preview=app_conf["show_images_in_browse"])) + self.results.add( + AdaptiveFeedItem(res, show_preview=app_conf["show_images_in_browse"]) + ) + def fetch_page(self, page=1): """ Fetch feed information @@ -32,6 +41,7 @@ class BrowseChannelScreen(Adw.NavigationPage): feed_id = self.current_feed cont = self.instance.get_channel_feed_content(self.channel.id, feed_id) GLib.idle_add(self.display_page, cont) + def change_feed(self, feed_id): """ change the feed and prepare layout @@ -62,6 +72,7 @@ class BrowseChannelScreen(Adw.NavigationPage): if feed.name == feed_name: self.change_feed(feed.id) break + def on_open_in_browser(self, arg): Gtk.UriLauncher.new(uri=self.channel.url).launch() @@ -94,7 +105,7 @@ class BrowseChannelScreen(Adw.NavigationPage): _("Add latest uploads to home feed"), PreferenceType.TOGGLE, False, - is_subscribed_to_channel(self.channel.server, self.channel.id) + is_subscribed_to_channel(self.channel.server, self.channel.id), ) sub_row = PreferenceRow(sub_pref) sub_row.set_callback(self.update_sub) @@ -106,8 +117,8 @@ class BrowseChannelScreen(Adw.NavigationPage): _("Channel feed"), _("This channel provides multiple feeds, choose which one to view"), PreferenceType.DROPDOWN, - [ feed.name for feed in self.feeds ], - self.default_feed_name + [feed.name for feed in self.feeds], + self.default_feed_name, ) # display preference row = PreferenceRow(pref) @@ -159,7 +170,7 @@ class BrowseChannelScreen(Adw.NavigationPage): self.set_title(_("Channel")) self.header_bar = Adw.HeaderBar() - self.external_btn = IconButton("","modem-symbolic") + self.external_btn = IconButton("", "modem-symbolic") self.external_btn.connect("clicked", self.on_open_in_browser) self.header_bar.pack_end(self.external_btn) diff --git a/melon/browse/playlist.py b/melon/browse/playlist.py index dee4ba0..2fc3603 100644 --- a/melon/browse/playlist.py +++ b/melon/browse/playlist.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, Gdk, GLib from unidecode import unidecode from gettext import gettext as _ @@ -14,16 +15,25 @@ from melon.widgets.feeditem import AdaptiveFeedItem from melon.widgets.iconbutton import IconButton from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference from melon.models import get_app_settings -from melon.models import has_bookmarked_external_playlist, ensure_bookmark_external_playlist, ensure_unbookmark_external_playlist +from melon.models import ( + has_bookmarked_external_playlist, + ensure_bookmark_external_playlist, + ensure_unbookmark_external_playlist, +) + class BrowsePlaylistScreen(Adw.NavigationPage): def render_page(self, items): app_conf = get_app_settings() for res in items: - self.results.add(AdaptiveFeedItem(res, show_preview=app_conf["show_images_in_browse"])) + self.results.add( + AdaptiveFeedItem(res, show_preview=app_conf["show_images_in_browse"]) + ) + def fetch_page(self, page=0): cont = self.instance.get_playlist_content(self.playlist_id) GLib.idle_add(self.render_page, cont) + def do_fetch_page(self, page=0): # start background thread self.thread = threading.Thread(target=self.fetch_page, args=[page]) @@ -31,13 +41,16 @@ class BrowsePlaylistScreen(Adw.NavigationPage): self.thread.start() def display_info(self, texture): - self.external_btn = IconButton("","modem-symbolic") + 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 = 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])) + GLib.Variant("as", [self.playlist.server, self.playlist.id]) + ) self.header_bar.pack_end(self.startplay_btn) # base layout @@ -57,7 +70,8 @@ class BrowsePlaylistScreen(Adw.NavigationPage): _("Add Playlist to your local playlist collection"), PreferenceType.TOGGLE, False, - has_bookmarked_external_playlist(self.playlist.server, self.playlist.id)) + has_bookmarked_external_playlist(self.playlist.server, self.playlist.id), + ) bookmark_row = PreferenceRow(bookmark_pref) bookmark_row.set_callback(self.bookmark_playlist) self.about.add(bookmark_row.get_widget()) @@ -74,7 +88,6 @@ class BrowsePlaylistScreen(Adw.NavigationPage): self.results = Adw.PreferencesGroup() self.box.add(self.results) - def background(self, playlist_id): # obtain playlist information self.playlist = self.instance.get_playlist_info(playlist_id) diff --git a/melon/browse/search.py b/melon/browse/search.py index e9b66ca..9353fbe 100644 --- a/melon/browse/search.py +++ b/melon/browse/search.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, Gdk, GLib from gettext import gettext as _ from gettext import ngettext @@ -13,18 +14,22 @@ from melon.servers.utils import get_allowed_servers_list, get_server_instance from melon.models import get_app_settings, register_callback from melon.servers import SearchMode, Preference, PreferenceType + class GlobalSearchScreen(Adw.NavigationPage): search_mode = SearchMode.ANY text = "" + # update the filter value def on_filter(self, mode): self.search_mode = mode self.search() + # update the text query def update_query(self, query): text = query.get_text() self.text = text self.search() + # rerun the search def search(self): # remove old results @@ -43,11 +48,14 @@ class GlobalSearchScreen(Adw.NavigationPage): box.set_title(instance.name) box.set_subtitle(_("No results")) count = len(results) - box.set_subtitle(ngettext("{count} result","{count} results", count).format(count = count)) + box.set_subtitle( + ngettext("{count} result", "{count} results", count).format(count=count) + ) self.results.add(box) for entry in results: row = AdaptiveFeedItem(entry, app_conf["show_images_in_browse"]) box.add_row(row) + # rerender widget def update(self): app_conf = get_app_settings() @@ -56,9 +64,13 @@ class GlobalSearchScreen(Adw.NavigationPage): # if no servers are allowed, show the nothing here box status = Adw.StatusPage() status.set_title(_("*crickets chirping*")) - status.set_description(_("There are no available servers, a search would yield no results")) + status.set_description( + _("There are no available servers, a search would yield no results") + ) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton(_("Enable servers in the settings menu"), "list-add-symbolic") + icon_button = IconButton( + _("Enable servers in the settings menu"), "list-add-symbolic" + ) icon_button.set_action_name("win.prefs") box = Gtk.CenterBox() box.set_center_widget(icon_button) @@ -80,15 +92,23 @@ class GlobalSearchScreen(Adw.NavigationPage): filter_popover = Gtk.Popover() filter_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # the filter options - filter_any = FilterButton(_("Any"), SearchMode.ANY, self.on_filter, self.search_mode) + filter_any = FilterButton( + _("Any"), SearchMode.ANY, self.on_filter, self.search_mode + ) filter_box.append(filter_any) - filter_channels = FilterButton(_("Channels"), SearchMode.CHANNELS, self.on_filter, self.search_mode) + filter_channels = FilterButton( + _("Channels"), SearchMode.CHANNELS, self.on_filter, self.search_mode + ) filter_channels.set_group(filter_any) filter_box.append(filter_channels) - filter_playlists = FilterButton(_("Playlists"), SearchMode.PLAYLISTS, self.on_filter, self.search_mode) + filter_playlists = FilterButton( + _("Playlists"), SearchMode.PLAYLISTS, self.on_filter, self.search_mode + ) filter_playlists.set_group(filter_any) filter_box.append(filter_playlists) - filter_videos = FilterButton(_("Videos"), SearchMode.VIDEOS, self.on_filter, self.search_mode) + filter_videos = FilterButton( + _("Videos"), SearchMode.VIDEOS, self.on_filter, self.search_mode + ) filter_videos.set_group(filter_any) filter_box.append(filter_videos) diff --git a/melon/browse/server.py b/melon/browse/server.py index 88aa947..7582fb3 100644 --- a/melon/browse/server.py +++ b/melon/browse/server.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, Gdk, GLib import threading from gettext import gettext as _ @@ -14,11 +15,13 @@ from melon.widgets.feeditem import AdaptiveFeedItem from melon.widgets.filterbutton import FilterButton from melon.models import get_app_settings + class Search(ViewStackPage): query = "" text = "" search_mode = SearchMode.ANY results = None + def __init__(self, plugin): super().__init__("search", _("Search"), "x-office-address-book-symbolic") self.instance = plugin @@ -40,15 +43,23 @@ class Search(ViewStackPage): filter_popover = Gtk.Popover() filter_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # the filter options - filter_any = FilterButton(_("Any"), SearchMode.ANY, self.on_filter, self.search_mode) + filter_any = FilterButton( + _("Any"), SearchMode.ANY, self.on_filter, self.search_mode + ) filter_box.append(filter_any) - filter_channels = FilterButton(_("Channels"), SearchMode.CHANNELS, self.on_filter, self.search_mode) + filter_channels = FilterButton( + _("Channels"), SearchMode.CHANNELS, self.on_filter, self.search_mode + ) filter_channels.set_group(filter_any) filter_box.append(filter_channels) - filter_playlists = FilterButton(_("Playlists"), SearchMode.PLAYLISTS, self.on_filter, self.search_mode) + filter_playlists = FilterButton( + _("Playlists"), SearchMode.PLAYLISTS, self.on_filter, self.search_mode + ) filter_playlists.set_group(filter_any) filter_box.append(filter_playlists) - filter_videos = FilterButton(_("Videos"), SearchMode.VIDEOS, self.on_filter, self.search_mode) + filter_videos = FilterButton( + _("Videos"), SearchMode.VIDEOS, self.on_filter, self.search_mode + ) filter_videos.set_group(filter_any) filter_box.append(filter_videos) @@ -64,20 +75,20 @@ class Search(ViewStackPage): self.scroll.set_child(self.inner) self.widget.set_child(self.outer) self.reset() + def update_query(self, query): self.text = query.get_text() self.search() + def on_filter(self, mode): self.search_mode = mode self.search() + def search(self): if self.text == "": # do not search for empty query return - results = self.instance.search( - self.text, - self.search_mode - ) + results = self.instance.search(self.text, self.search_mode) # show empty page if len(results) == 0: self.reset() @@ -92,8 +103,11 @@ class Search(ViewStackPage): app_settings = get_app_settings() # add results for item in results: - self.results.add(AdaptiveFeedItem(item, app_settings["show_images_in_browse"])) + self.results.add( + AdaptiveFeedItem(item, app_settings["show_images_in_browse"]) + ) self.inner.append(self.results) + def reset(self): if not self.results is None: self.inner.remove(self.results) @@ -107,6 +121,7 @@ class Search(ViewStackPage): self.results.set_icon_name("weather-few-clouds-night-symbolic") self.inner.append(self.results) + class Feed(ViewStackPage): def update(self): # show spinner @@ -119,6 +134,7 @@ class Feed(ViewStackPage): # long task self.results = self.instance.get_public_feed_content(self.feed.id) GLib.idle_add(self.display) + def display(self): # display results if len(self.results) == 0: @@ -134,7 +150,10 @@ class Feed(ViewStackPage): self.inner.set_child(scrollable) app_settings = get_app_settings() for item in self.results: - scrollable.add(AdaptiveFeedItem(item, app_settings["show_images_in_browse"])) + scrollable.add( + AdaptiveFeedItem(item, app_settings["show_images_in_browse"]) + ) + def do_update(self): self.thread = threading.Thread(target=self.update) self.thread.daemon = True @@ -149,6 +168,7 @@ class Feed(ViewStackPage): self.widget.set_child(self.inner) self.do_update() + class BrowseServerScreen(Adw.NavigationPage): def on_open_in_browser(self, arg): child_name = self.view_stack.get_visible_child_name() @@ -158,11 +178,13 @@ class BrowseServerScreen(Adw.NavigationPage): return url = self.instance.get_external_url() Gtk.UriLauncher.new(uri=url).launch() + def render_feeds(self): # runs on main for feed in self.feeds: widg = Feed(self.instance, feed) widg.bind_to(self.view_stack) + def load_feeds(self): # runs in thread self.feeds = self.instance.get_public_feeds() @@ -185,7 +207,7 @@ class BrowseServerScreen(Adw.NavigationPage): self.switcher.set_stack(self.view_stack) self.header_bar = Adw.HeaderBar() self.header_bar.set_title_widget(self.switcher) - self.external_btn = IconButton("","modem-symbolic") + self.external_btn = IconButton("", "modem-symbolic") self.external_btn.connect("clicked", self.on_open_in_browser) self.header_bar.pack_end(self.external_btn) diff --git a/melon/home/__init__.py b/melon/home/__init__.py index cf35dcc..907507a 100644 --- a/melon/home/__init__.py +++ b/melon/home/__init__.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio from gettext import gettext as _ @@ -12,6 +13,7 @@ from melon.home.history import History from melon.widgets.iconbutton import IconButton + class HomeScreen(Adw.NavigationPage): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/melon/home/history.py b/melon/home/history.py index 61e3dc4..0367133 100644 --- a/melon/home/history.py +++ b/melon/home/history.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib import threading from gettext import gettext as _ @@ -12,6 +13,7 @@ from melon.widgets.feeditem import AdaptiveFeedItem from melon.models import get_history, register_callback, get_app_settings from melon.servers.utils import group_by_date, filter_resources + class History(ViewStackPage): # datecount displayed page_size = 7 @@ -31,7 +33,7 @@ class History(ViewStackPage): self.inner.set_child(status) else: self.wrapper = Gtk.Viewport() - self.results = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) + self.results = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) title = Adw.PreferencesGroup() title.set_title(_("History")) title.set_description(_("These are the videos you opened in the past")) @@ -46,17 +48,22 @@ class History(ViewStackPage): # keep track of the load more button next_ctr = None focus_node = None + def load_page(self, page=0): self.focus_node = None padding = 12 # add history entries app_settings = get_app_settings() - for date, content in self.data[page*self.page_size:(page+1)*self.page_size]: + for date, content in self.data[ + page * self.page_size : (page + 1) * self.page_size + ]: group = Adw.PreferencesGroup() group.set_title(date) self.results.append(group) for resource in content: - group.add(AdaptiveFeedItem(resource, app_settings["show_images_in_feed"])) + group.add( + AdaptiveFeedItem(resource, app_settings["show_images_in_feed"]) + ) # because we aren't using a preferencepage # we have to take care of padding ourselfs group.set_margin_end(padding) @@ -70,14 +77,14 @@ class History(ViewStackPage): if not self.next_ctr is None: self.results.remove(self.next_ctr) # show Load more button if there are more entries - if len(self.data) > (page+1)*self.page_size: + if len(self.data) > (page + 1) * self.page_size: self.next_ctr = Adw.PreferencesGroup() btn = Adw.ActionRow() btn.set_title(_("Show more")) btn.set_subtitle(_("Load older videos")) btn.add_suffix(Gtk.Image.new_from_icon_name("go-down-symbolic")) btn.set_activatable(True) - btn.connect("activated", lambda _: self.load_page(page+1)) + btn.connect("activated", lambda _: self.load_page(page + 1)) self.next_ctr.add(btn) # because we aren't using a preferencepage # we have to take care of padding ourselfs @@ -96,10 +103,11 @@ class History(ViewStackPage): def schedule(self): app_settings = get_app_settings() # make sure only videos from available servers are shown - hist = filter_resources(get_history(), app_settings, access=lambda x:x[0]) + hist = filter_resources(get_history(), app_settings, access=lambda x: x[0]) GLib.idle_add(self.update, list(group_by_date(hist).items())) thread = None + def do_update(self): # show spinner spinner = Gtk.Spinner() diff --git a/melon/home/new.py b/melon/home/new.py index c12d030..5133845 100644 --- a/melon/home/new.py +++ b/melon/home/new.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib, Gio, GObject import threading from datetime import datetime @@ -13,9 +14,15 @@ from melon.widgets.feeditem import AdaptiveFeedItem 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_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): news = get_cached_feed(100) @@ -29,15 +36,21 @@ class NewFeed(ViewStackPage): status.set_title(_("*crickets chirping*")) subs = get_subscribed_channels() if len(subs) == 0: - status.set_description(_("Subscribe to a channel first, to view new uploads")) + status.set_description( + _("Subscribe to a channel first, to view new uploads") + ) else: - status.set_description(_("The channels you are subscribed to haven't uploaded anything yet")) + status.set_description( + _( + "The channels you are subscribed to haven't uploaded anything yet" + ) + ) status.set_icon_name("weather-few-clouds-night-symbolic") icon_button = IconButton(_("Browse Servers"), "list-add-symbolic") icon_button.set_action_name("win.browse") icon_button.set_margin_bottom(6) box = Gtk.CenterBox() - ls = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) + ls = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) ls.append(icon_button) ls.append(refresh_btn) box.set_center_widget(ls) @@ -52,10 +65,14 @@ class NewFeed(ViewStackPage): last_refresh = "Never" else: last_refresh = datetime.fromtimestamp(last_refresh).strftime("%c") - refresh_info = _("(Last refresh: {last_refresh})").format(last_refresh = last_refresh) + refresh_info = _("(Last refresh: {last_refresh})").format( + last_refresh=last_refresh + ) title = Adw.PreferencesGroup() title.set_title(_("What's new") + " " + refresh_info) - title.set_description(_("These are the latest videos of channels you follow")) + title.set_description( + _("These are the latest videos of channels you follow") + ) title.set_header_suffix(refresh_btn) results.add(title) self.inner.set_child(results) @@ -63,7 +80,13 @@ class NewFeed(ViewStackPage): for entry in news: vid = entry[0] uts = entry[1] - date = datetime.fromtimestamp(uts).date().strftime("%c").replace("00:00:00","").replace(" ", " ") + date = ( + datetime.fromtimestamp(uts) + .date() + .strftime("%c") + .replace("00:00:00", "") + .replace(" ", " ") + ) group = None if date in tracker: group = tracker[date] @@ -77,6 +100,7 @@ class NewFeed(ViewStackPage): return False thread = None + def do_update(self): # show spinner spinner = Gtk.Spinner() @@ -111,6 +135,7 @@ class NewFeed(ViewStackPage): self.thread.start() ov_toast = None + def load_overlay(self): last = get_last_playback() if not last is None: @@ -121,7 +146,7 @@ class NewFeed(ViewStackPage): elif dur == 0: # video has no length, why would anybody continue watching it last = None - elif pos > dur*0.95: + elif pos > dur * 0.95: # video was already played >0.95 percent # so we can consider it watched # TODO: move into settings menu @@ -138,7 +163,8 @@ class NewFeed(ViewStackPage): 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])) + GLib.Variant("as", [resource.server, resource.id]) + ) self.overlay.add_toast(self.ov_toast) self.ov_toast.set_timeout(20) diff --git a/melon/home/playlists.py b/melon/home/playlists.py index b59a184..62495bc 100644 --- a/melon/home/playlists.py +++ b/melon/home/playlists.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib import threading from gettext import gettext as _ @@ -13,40 +14,53 @@ from melon.models import register_callback, get_playlists, get_app_settings from melon.models import PlaylistWrapperType from melon.servers.utils import server_is_allowed + class Playlists(ViewStackPage): def update(self, playlists): app_conf = get_app_settings() if not playlists: - status = Adw.StatusPage() - status.set_title(_("*crickets chirping*")) - status.set_description(_("You don't have any playlists yet")) - status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton(_("Create a new playlist"), "list-add-symbolic") - icon_button.set_action_name("win.new_playlist") - box = Gtk.CenterBox() - box.set_center_widget(icon_button) - status.set_child(box) - self.inner.set_child(status) + status = Adw.StatusPage() + status.set_title(_("*crickets chirping*")) + status.set_description(_("You don't have any playlists yet")) + status.set_icon_name("weather-few-clouds-night-symbolic") + icon_button = IconButton(_("Create a new playlist"), "list-add-symbolic") + icon_button.set_action_name("win.new_playlist") + box = Gtk.CenterBox() + box.set_center_widget(icon_button) + status.set_child(box) + self.inner.set_child(status) else: results = Adw.PreferencesGroup() results.set_title(_("Playlists")) - results.set_description(_("Here are playlists you've bookmarked or created yourself")) - icon_button = IconButton(_("New"), "list-add-symbolic", tooltip=_("Create a new playlist")) + results.set_description( + _("Here are playlists you've bookmarked or created yourself") + ) + icon_button = IconButton( + _("New"), "list-add-symbolic", tooltip=_("Create a new playlist") + ) icon_button.set_action_name("win.new_playlist") results.set_header_suffix(icon_button) self.inner.set_child(results) for playlist in playlists: - results.add(AdaptivePlaylistFeedItem(playlist, app_conf["show_images_in_feed"])) + results.add( + AdaptivePlaylistFeedItem(playlist, app_conf["show_images_in_feed"]) + ) def schedule(self): app_conf = get_app_settings() # filter external playlists if server is disabled - playlists = [ playlist for playlist in get_playlists() if playlist.type == PlaylistWrapperType.LOCAL or server_is_allowed(playlist.inner.server, app_conf) ] + playlists = [ + playlist + for playlist in get_playlists() + if playlist.type == PlaylistWrapperType.LOCAL + or server_is_allowed(playlist.inner.server, app_conf) + ] # local and external playlists have the inner.title attribute - playlists.sort(key=lambda x:x.inner.title) + playlists.sort(key=lambda x: x.inner.title) GLib.idle_add(self.update, playlists) thread = None + def do_update(self): # show spinner spinner = Gtk.Spinner() diff --git a/melon/home/subs.py b/melon/home/subs.py index f54eec6..456e247 100644 --- a/melon/home/subs.py +++ b/melon/home/subs.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib import threading from gettext import gettext as _ @@ -12,6 +13,7 @@ from melon.widgets.feeditem import AdaptiveFeedItem from melon.models import get_subscribed_channels, register_callback, get_app_settings from melon.servers.utils import filter_resources + class Subscriptions(ViewStackPage): def update(self, subs): app_settings = get_app_settings() @@ -36,16 +38,19 @@ class Subscriptions(ViewStackPage): # NOTE: might seem confusing to unknowing users self.inner.set_child(results) for channel in subs: - results.add(AdaptiveFeedItem(channel, app_settings["show_images_in_feed"])) + results.add( + AdaptiveFeedItem(channel, app_settings["show_images_in_feed"]) + ) def schedule(self): app_settings = get_app_settings() # make sure only channles from available servers are included subs = filter_resources(get_subscribed_channels(), app_settings) - subs.sort(key=lambda c:c.name) + subs.sort(key=lambda c: c.name) GLib.idle_add(self.update, subs) thread = None + def do_update(self): # show spinner spinner = Gtk.Spinner() diff --git a/melon/import_providers/__init__.py b/melon/import_providers/__init__.py index 5ecdcc6..acfb13a 100644 --- a/melon/import_providers/__init__.py +++ b/melon/import_providers/__init__.py @@ -1,9 +1,12 @@ from enum import Enum, auto -from abc import ABC,abstractmethod +from abc import ABC, abstractmethod + class PickerMode(Enum): FILE = auto() FOLDER = auto() + + class ImportProvider(ABC): id: str title: str @@ -15,8 +18,9 @@ class ImportProvider(ABC): # and mime types in an array as the value # only used for PickerMode.FILE filters: dict[str, list[str]] = {} + @abstractmethod - def load(self, selection:list[str]): + def load(self, selection: list[str]): """ Called with a list of file/folder paths after the selection dialog was shown diff --git a/melon/import_providers/newpipe.py b/melon/import_providers/newpipe.py index a1e0e76..2a523d1 100644 --- a/melon/import_providers/newpipe.py +++ b/melon/import_providers/newpipe.py @@ -5,51 +5,65 @@ from gettext import gettext as _ from melon.import_providers import ImportProvider, PickerMode from melon.servers import Channel, Playlist, Video -from melon.models import ensure_subscribed_to_channel, ensure_bookmark_external_playlist, add_to_history, new_local_playlist, add_to_local_playlist, set_local_playlist_thumbnail, add_history_items, add_videos +from melon.models import ( + ensure_subscribed_to_channel, + ensure_bookmark_external_playlist, + add_to_history, + new_local_playlist, + add_to_local_playlist, + set_local_playlist_thumbnail, + add_history_items, + add_videos, +) + def connect_to_db(database_path): con = sqlite3.connect(database_path) return con + def adapt_json(data): return (json.dumps(data, sort_keys=True)).encode() + def convert_json(blob): return json.loads(blob.decode()) + sqlite3.register_adapter(dict, adapt_json) sqlite3.register_adapter(list, adapt_json) sqlite3.register_adapter(tuple, adapt_json) -sqlite3.register_converter('json', convert_json) +sqlite3.register_converter("json", convert_json) + class NewpipeImporter(ImportProvider, ABC): id = "newpipedb" title = _("Newpipe Database importer") - description = _("Import the .db file from inside the newpipe .zip export (as invidious content)") + description = _( + "Import the .db file from inside the newpipe .zip export (as invidious content)" + ) picker_title = _("Select .db file") # the server id for resources - server_id:str + server_id: str # url used to replace https://www.youtube.com - base_url:str + base_url: str # url used to replace https://i.ytimg.com - img_url:str + img_url: str # service id to match in newpipe db - service_id:int + service_id: int mode = PickerMode.FILE is_multi = False - filters = { - _("Newpipe Database"): [ "application/x-sqlite3" ] - } + filters = {_("Newpipe Database"): ["application/x-sqlite3"]} - db:sqlite3.Connection = None + db: sqlite3.Connection = None # additional information about newpipes database layout # https://github.com/TeamNewPipe/NewPipe/wiki/Database - def load(self, selection:list[str]): + def load(self, selection: list[str]): db_path = selection[0] self.db = connect_to_db(db_path) @@ -59,10 +73,12 @@ class NewpipeImporter(ImportProvider, ABC): self.__load_history() def __load_subs(self): - results = self.db.execute(""" + results = self.db.execute( + """ SELECT service_id, url, name, avatar_url, description FROM subscriptions - """).fetchall() + """ + ).fetchall() for dt in results: if dt[0] == self.service_id: server = self.server_id @@ -75,10 +91,12 @@ class NewpipeImporter(ImportProvider, ABC): ensure_subscribed_to_channel(c) def __load_history(self): - results = self.db.execute(""" + results = self.db.execute( + """ SELECT service_id, url, title, access_date, uploader_url, uploader, thumbnail_url FROM streams JOIN stream_history ON stream_history.stream_id = streams.uid - """).fetchall() + """ + ).fetchall() entries = [] for dt in results: if dt[0] == self.service_id: @@ -94,27 +112,34 @@ class NewpipeImporter(ImportProvider, ABC): # newpipe does not store description desc = "" uts = dt[3] / 1000 - v = Video(server, url, id, title, (channel_name, channel_id), desc, thumb) + v = Video( + server, url, id, title, (channel_name, channel_id), desc, thumb + ) entries.append((v, uts)) - add_videos([ d[0] for d in entries ]) + add_videos([d[0] for d in entries]) add_history_items(entries) def __load_local_playlists(self): - results = self.db.execute(""" + results = self.db.execute( + """ SELECT uid, name, thumbnail_stream_id FROM playlists - """).fetchall() + """ + ).fetchall() for dt in results: name = dt[1] # newpipe doesn't support playlist descriptions description = "" uid = dt[0] - vids = self.db.execute(""" + vids = self.db.execute( + """ SELECT service_id, url, title, uploader, uploader_url, thumbnail_url FROM playlist_stream_join, streams WHERE playlist_stream_join.playlist_id = ? AND streams.uid = playlist_stream_join.stream_id ORDER BY playlist_stream_join.join_index - """, (uid,)).fetchall() + """, + (uid,), + ).fetchall() videos = [] for vdt in vids: if vdt[0] == self.service_id: @@ -130,21 +155,38 @@ class NewpipeImporter(ImportProvider, ABC): thumb = vdt[5].replace("https://i.ytimg.com", self.img_url) # newpipe doesn't store descriptions desc = "" - videos.append(Video(server, url, id, title, (channel_name, channel_id), desc, thumb)) + videos.append( + Video( + server, + url, + id, + title, + (channel_name, channel_id), + desc, + thumb, + ) + ) pid = new_local_playlist(name, description, videos) - thumb = self.db.execute(""" + thumb = self.db.execute( + """ SELECT thumbnail_url FROM streams WHERE uid = ? - """, (dt[2],)).fetchone() + """, + (dt[2],), + ).fetchone() if not thumb is None: - set_local_playlist_thumbnail(pid, thumb[0].replace("https://i.ytimg.com", self.img_url)) + set_local_playlist_thumbnail( + pid, thumb[0].replace("https://i.ytimg.com", self.img_url) + ) def __load_remote_playlists(self): - results = self.db.execute(""" + results = self.db.execute( + """ SELECT service_id, name, url, thumbnail_url, uploader FROM remote_playlists - """).fetchall() + """ + ).fetchall() for dt in results: if dt[0] == self.service_id: server = self.server_id diff --git a/melon/import_providers/utils.py b/melon/import_providers/utils.py index 668942b..b9bdc6a 100644 --- a/melon/import_providers/utils.py +++ b/melon/import_providers/utils.py @@ -1,23 +1,29 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib, Gio from melon.import_providers import ImportProvider, PickerMode + class ImportPicker(Gtk.FileDialog): cb = None - def __init__(self, provider:ImportProvider, *args, **kwargs): + + def __init__(self, provider: ImportProvider, *args, **kwargs): super().__init__(*args, **kwargs) self.set_title(provider.picker_title) self.set_modal(True) self.provider = provider + def set_onselect(self, cb): self.cb = cb + def done(self, paths: list[str]): if self.cb is None: return self.cb(self.provider, paths) + def open_single_file(self, dialog, result): try: file = dialog.open_finish(result) @@ -25,6 +31,7 @@ class ImportPicker(Gtk.FileDialog): self.done([file.get_path()]) except GLib.Error as error: print(f"Error opening file: {error.message}") + def open_multi_files(self, dialog, result): try: data = dialog.open_multiple_finish(result) @@ -36,6 +43,7 @@ class ImportPicker(Gtk.FileDialog): self.done(files) except GLib.Error as error: print(f"Error opening file: {error.message}") + def show(self, win): if self.provider.mode == PickerMode.FILE: # generate filter diff --git a/melon/importer.py b/melon/importer.py index 7cf3117..c10fbad 100644 --- a/melon/importer.py +++ b/melon/importer.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio from gettext import gettext as _ @@ -11,6 +12,7 @@ from melon.models.callbacks import freeze, unfreeze from melon.servers.utils import get_allowed_servers_list, get_server_instance from melon.models import get_app_settings + class ImporterScreen(Adw.NavigationPage): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -61,7 +63,9 @@ class ImporterScreen(Adw.NavigationPage): status.set_title(_("*crickets chirping*")) status.set_description(_("There are no available importer methods")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton(_("Enable servers in the settings menu"), "list-add-symbolic") + icon_button = IconButton( + _("Enable servers in the settings menu"), "list-add-symbolic" + ) icon_button.set_action_name("win.prefs") box = Gtk.CenterBox() box.set_center_widget(icon_button) @@ -75,7 +79,7 @@ class ImporterScreen(Adw.NavigationPage): picker.show(None) picker.set_onselect(self.on_select) - def on_select(self, provider:ImportProvider, files): + def on_select(self, provider: ImportProvider, files): # show spinner spinner = Gtk.Spinner() spinner.set_size_request(50, 50) @@ -95,5 +99,6 @@ class ImporterScreen(Adw.NavigationPage): # automatically go back home self.activate_action("win.home", None) + def pass_me(func, *args): return lambda x: func(x, *args) diff --git a/melon/models/__init__.py b/melon/models/__init__.py index 2b79116..49aa058 100644 --- a/melon/models/__init__.py +++ b/melon/models/__init__.py @@ -1,5 +1,5 @@ from copy import deepcopy -from enum import Enum,auto +from enum import Enum, auto from datetime import datetime from functools import cache import sqlite3 @@ -11,7 +11,8 @@ from melon.servers import Video, Channel, Playlist, Resource from melon.utils import get_data_dir from melon.models.callbacks import notify, register_callback -class LocalPlaylist(): + +class LocalPlaylist: # id used for local identification id: int # video title @@ -21,46 +22,60 @@ class LocalPlaylist(): # preview image url (None if non existend) thumbnail: str = None content: list[(Resource, int)] = [] + def __init__(self, id, title, desc): self.id = id self.title = title self.description = desc + class PlaylistWrapperType(Enum): LOCAL = auto() EXTERNAL = auto() + + class PlaylistWrapper: - inner: (Playlist | LocalPlaylist) = None + inner: Playlist | LocalPlaylist = None type: PlaylistWrapperType = None + def __init__(self, type, inner): self.inner = inner self.type = type + def from_local(playlist: LocalPlaylist): return PlaylistWrapper(PlaylistWrapperType.LOCAL, playlist) + def from_external(playlist: Playlist): return PlaylistWrapper(PlaylistWrapperType.EXTERNAL, playlist) + ################################################## # DATABASE IMPLEMENTATION ################################################## + def adapt_json(data): return (json.dumps(data, sort_keys=True)).encode() + def convert_json(blob): return json.loads(blob.decode()) + sqlite3.register_adapter(dict, adapt_json) sqlite3.register_adapter(list, adapt_json) sqlite3.register_adapter(tuple, adapt_json) -sqlite3.register_converter('json', convert_json) +sqlite3.register_converter("json", convert_json) database_path = os.path.join(get_data_dir(), "melon.db") + def connect_to_db(): con = sqlite3.connect(database_path) return con -def execute_sql(conn:sqlite3.Connection, sql, *sqlargs, many=False) -> bool: + + +def execute_sql(conn: sqlite3.Connection, sql, *sqlargs, many=False) -> bool: try: c = conn.cursor() if many: @@ -70,11 +85,12 @@ def execute_sql(conn:sqlite3.Connection, sql, *sqlargs, many=False) -> bool: conn.commit() c.close() except Exception as e: - print('SQLite-error:', e) + print("SQLite-error:", e) return False else: return True + VERSION = 3 app_conf_template = { @@ -89,7 +105,7 @@ app_conf_template = { # do not include plugins with only nsfw content by default "nsfw_only_content": False, # do not include apps that require login by default - "login_required": False + "login_required": False, } server_settings_template = { @@ -100,12 +116,14 @@ server_settings_template = { # do not require being stored "nsfw_content": False, "nsfw_only_content": False, - "login_required": False + "login_required": False, } + def die(): raise "DB ERROR" + def init_db(): conn = connect_to_db() if conn is None: @@ -118,32 +136,45 @@ def init_db(): # initial version (defaults to 0, so this means the DB isn't initialized) # perform initial setup # application settings - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE appconf( key, value, PRIMARY KEY(key) ) - """) or die() - execute_sql(conn, - "INSERT INTO appconf VALUES (?,?)", - [ (key, value) for key,value in app_conf_template.items() ], - many=True) + """, + ) or die() + execute_sql( + conn, + "INSERT INTO appconf VALUES (?,?)", + [(key, value) for key, value in app_conf_template.items()], + many=True, + ) # enabled servers lookup table - execute_sql(conn, """CREATE TABLE servers( + execute_sql( + conn, + """CREATE TABLE servers( server, enabled, PRIMARY KEY(server) - )""") or die() + )""", + ) or die() # server settings - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE server_settings( server, key, value, - PRIMARY KEY(server, key))""") or die() + PRIMARY KEY(server, key))""", + ) or die() # channels - execute_sql(conn,""" + execute_sql( + conn, + """ CREATE TABLE channels( server, id, @@ -152,9 +183,12 @@ def init_db(): bio, avatar, PRIMARY KEY(server, id) - )""") or die() + )""", + ) or die() # videos - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE videos( server, id, @@ -165,9 +199,12 @@ def init_db(): channel_id, channel_name, PRIMARY KEY(server, id)) - """) or die() + """, + ) or die() # playlists - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE playlists( server, id, @@ -179,9 +216,12 @@ def init_db(): thumbnail, PRIMARY KEY(server, id) ) - """) or die() + """, + ) or die() # playlist <-> videos - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE playlist_content( server, playlist_id, @@ -190,18 +230,24 @@ def init_db(): FOREIGN KEY(video_id, server) REFERENCES videos(id, server), PRIMARY KEY(server, playlist_id, video_id) ) - """) or die() + """, + ) or die() # local playlists - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE local_playlists( id, title, description, thumbnail, PRIMARY KEY(id) - )""") or die() + )""", + ) or die() # local-playlists <-> videos - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE local_playlist_content( id, video_id, @@ -210,43 +256,58 @@ def init_db(): FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server), PRIMARY KEY(id, position) ) - """) or die() + """, + ) or die() # history - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE history( timestamp, video_id, video_server, FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server) ) - """) or die() + """, + ) or die() # subscriptions - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE subscriptions( video_id, server, PRIMARY KEY(server, video_id), FOREIGN KEY(server, video_id) REFERENCES videos(server, id) ) - """) + """, + ) # bookmarked playlists - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE bookmarked_playlists( server, playlist_id, PRIMARY KEY(server, playlist_id), FOREIGN KEY(server, playlist_id) REFERENCES playlists(server, id) ) - """) - execute_sql(conn, """ + """, + ) + execute_sql( + conn, + """ CREATE TABLE news( timestamp, video_id, video_server, FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server) ) - """) - execute_sql(conn, """ + """, + ) + execute_sql( + conn, + """ CREATE TABLE playback( timestamp, video_server, @@ -256,7 +317,8 @@ def init_db(): 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 @@ -265,7 +327,9 @@ def init_db(): # if minv <= version <= maxv: if version < 2: # newly added in db v2 - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE playback( timestamp, video_server, @@ -275,10 +339,13 @@ def init_db(): PRIMARY KEY(video_id, video_server), FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server) ) - """) + """, + ) if version < 3: # video_id and video_server are no longer part of the primary key - execute_sql(conn, """ + execute_sql( + conn, + """ CREATE TABLE lpc2( id, video_id, @@ -286,32 +353,46 @@ def init_db(): position, FOREIGN KEY(video_id, video_server) REFERENCES videos(id, server), PRIMARY KEY(id, position) - )""") - execute_sql(conn, """ + )""", + ) + execute_sql( + conn, + """ INSERT INTO lpc2(id, video_id, video_server, position) SELECT id, video_id, video_server, position FROM local_playlist_content - """) - execute_sql(conn, """ + """, + ) + execute_sql( + conn, + """ DROP TABLE local_playlist_content - """) - execute_sql(conn, """ + """, + ) + execute_sql( + conn, + """ ALTER TABLE lpc2 RENAME TO local_playlist_content - """) + """, + ) # MIGRATIONS FINISHED execute_sql(conn, f"PRAGMA user_version = {VERSION}") or die() + # additional server database # for data not stored in database servers = {} -def init_server_settings(sid:str, enable=None) -> bool: + +def init_server_settings(sid: str, enable=None) -> bool: """ Make sure server config is initialized (for this session) returns true if changed """ servers[sid] = deepcopy(server_settings_template) + + # DO NOT CALL # used internally def load_server(server): @@ -326,7 +407,8 @@ def load_server(server): # since 1.3.0 conn = connect_to_db() value = conn.execute( - "SELECT enabled FROM servers WHERE server = ?", (sid,)).fetchone() + "SELECT enabled FROM servers WHERE server = ?", (sid,) + ).fetchone() if value is None: # disable servers that require login by default if server.requires_login: @@ -334,22 +416,29 @@ def load_server(server): servers[sid]["enabled"] = False conn.close() notify("settings_changed") -def set_server_setting(sid:str, pref:str, value): + + +def set_server_setting(sid: str, pref: str, value): init_server_settings(sid) conn = connect_to_db() - execute_sql(conn, - "INSERT OR REPLACE INTO server_settings VALUES (?,?,?)", - (sid, pref, 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): + + +def get_server_settings(sid: str): init_server_settings(sid) base = servers[sid] 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() + "SELECT key, value FROM server_settings WHERE server = ?", (sid,) + ).fetchall() for setting in results: value = setting[1] try: @@ -360,35 +449,48 @@ def get_server_settings(sid:str): value = setting[1] base["custom"][setting[0]] = value return base + + def is_server_enabled(server_id: str): conn = connect_to_db() value = conn.execute( - "SELECT enabled FROM servers WHERE server = ?", (server_id,)).fetchone() + "SELECT enabled FROM servers WHERE server = ?", (server_id,) + ).fetchone() if not value is None: value = value[0] else: value = server_settings_template["enabled"] return bool(value) + + def ensure_server(server_id: str, mode: bool): if is_server_enabled(server_id) == mode: # nothing to do return conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO servers VALUES (?, ?) - """, (server_id, mode)) + """, + (server_id, mode), + ) # notify channels and playlists, because available channels might be different conn.close() notify("channels_changed") notify("playlists_changed") notify("settings_changed") + def ensure_server_disabled(server_id: str): ensure_server(server_id, False) + + def ensure_server_enabled(server_id: str): ensure_server(server_id, True) + def get_app_settings(): base = deepcopy(app_conf_template) conn = connect_to_db() @@ -396,94 +498,157 @@ def get_app_settings(): for setting in results: base[setting[0]] = setting[1] return base -def set_app_setting(pref:str, value): + + +def set_app_setting(pref: str, value): conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ UPDATE appconf SET value = ? WHERE key = ? - """, (value, pref)) + """, + (value, pref), + ) # notify channels and playlists, because available channels might be different conn.close() notify("channels_changed") notify("playlists_changed") notify("settings_changed") -def is_subscribed_to_channel(server_id: str, channel_id:str) -> bool: + +def is_subscribed_to_channel(server_id: str, channel_id: str) -> bool: conn = connect_to_db() - results = conn.execute(""" + results = conn.execute( + """ SELECT * FROM subscriptions WHERE server = ? AND video_id = ? - """, (server_id, channel_id)).fetchall() + """, + (server_id, channel_id), + ).fetchall() if len(results) == 0: return False return True + + def has_channel(channel_server, channel_id): conn = connect_to_db() - results = conn.execute(""" + results = conn.execute( + """ SELECT * FROM channels WHERE server = ? AND id = ? - """, (channel_server, channel_id)).fetchall() + """, + (channel_server, channel_id), + ).fetchall() return len(results) != 0 -def ensure_channel(channel:Channel): + + +def ensure_channel(channel: Channel): # insert channel data conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO channels VALUES (?,?,?,?,?,?) """, - (channel.server, channel.id, channel.url, channel.name, channel.bio, channel.avatar)) + ( + channel.server, + channel.id, + channel.url, + channel.name, + channel.bio, + channel.avatar, + ), + ) conn.close() notify("channels_changed") + + def ensure_subscribed_to_channel(channel: Channel): if is_subscribed_to_channel(channel.server, channel.id): return ensure_channel(channel) conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO subscriptions VALUES (?,?) - """, (channel.id, channel.server)) + """, + (channel.id, channel.server), + ) conn.close() notify("channels_changed") -def ensure_unsubscribed_from_channel(server_id: str, channel_id:str): + + +def ensure_unsubscribed_from_channel(server_id: str, channel_id: str): conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ DELETE FROM subscriptions WHERE server = ?, id = ? - """, (server_id, channel_id)) + """, + (server_id, channel_id), + ) conn.close() notify("channels_changed") + + def get_subscribed_channels() -> list[Channel]: conn = connect_to_db() - results = conn.execute(""" + results = conn.execute( + """ SELECT channels.server, channels.id, url, name, bio, avatar FROM channels, subscriptions WHERE channels.id = subscriptions.video_id AND channels.server == subscriptions.server - """).fetchall() + """ + ).fetchall() conn.close() - return [ Channel(d[0], d[2], d[1], d[3], d[4], d[5]) for d in results ] + return [Channel(d[0], d[2], d[1], d[3], d[4], d[5]) for d in results] + def has_video(server_id, video_id): conn = connect_to_db() - results = conn.execute(""" + results = conn.execute( + """ SELECT * FROM videos WHERE server = ? AND id = ? - """, (server_id, video_id)).fetchall() + """, + (server_id, video_id), + ).fetchall() return len(results) != 0 + + def ensure_video(vid): # create video conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO videos VALUES (?,?,?,?,?,?,?,?) """, - (vid.server, vid.id, vid.url, vid.title, vid.description, vid.thumbnail, vid.channel[1], vid.channel[0])) + ( + 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]): + + +def add_videos(vids: list[Video]): conn = connect_to_db() execute_sql( conn, @@ -491,87 +656,148 @@ def add_videos(vids:list[Video]): INSERT OR REPLACE INTO videos VALUES (?,?,?,?,?,?,?,?) """, - [ (vid.server, vid.id, vid.url, vid.title, vid.description, vid.thumbnail, vid.channel[1], vid.channel[0]) for vid in vids], - many=True) + [ + ( + 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: + +def has_bookmarked_external_playlist(server_id: str, playlist_id: str) -> bool: conn = connect_to_db() - results = conn.execute(""" + results = conn.execute( + """ SELECT * FROM playlists WHERE server = ? AND id = ? """, - (server_id, playlist_id)).fetchall() + (server_id, playlist_id), + ).fetchall() return len(results) != 0 + + def has_playlist(p: PlaylistWrapper) -> bool: conn = connect_to_db() if p.type == PlaylistWrapperType.LOCAL: - results = conn.execute(""" + results = conn.execute( + """ SELECT * FROM local_playlist WHERE id = ? - """, (p.inner.id)).fetchall() + """, + (p.inner.id), + ).fetchall() return len(results) != 0 else: - results = conn.execute(""" + results = conn.execute( + """ SELECT * FROM bookmarked_playlists WHERE server = ? AND playlist_id = ? - """, (p.inner.server, p.inner.id)).fetchall() + """, + (p.inner.server, p.inner.id), + ).fetchall() return len(results) != 0 + + def ensure_playlist(playlist: PlaylistWrapper): if playlist.type == PlaylistWrapperType.LOCAL: # insert new playlist conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO local_playlists VALUES (?,?,?,?) """, - (playlist.inner.id, playlist.inner.title, playlist.inner.description, playlist.inner.thumbnail)) + ( + playlist.inner.id, + playlist.inner.title, + playlist.inner.description, + playlist.inner.thumbnail, + ), + ) else: # insert new playlist conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO playlists 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)) + ( + 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) # NOTE: this will automatically notify listeners ensure_playlist(wrapped) conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO bookmarked_playlists VALUES (?,?) """, - (playlist.server, playlist.id)) + (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 conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ DELETE FROM bookmarked_playlists WHERE server = ?, playlist_id = ? """, - (server_id, 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""" id = len(get_playlists()) conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT INTO local_playlists VALUES (?,?,?,?) """, - (id, name, description, None)) + (id, name, description, None), + ) index = 0 for vid in videos: add_to_local_playlist(id, vid, index) @@ -579,90 +805,146 @@ def new_local_playlist(name: str, description: str, videos=[]) -> int: conn.close() notify("playlists_changed") return id -def ensure_delete_local_playlist(playlist_id:int): + + +def ensure_delete_local_playlist(playlist_id: int): conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ DELETE FROM local_playlist_content WHERE id = ? - """, (playlist_id,)) - execute_sql(conn,""" + """, + (playlist_id,), + ) + execute_sql( + conn, + """ DELETE FROM local_playlists WHERE id = ? - """, (playlist_id,)) + """, + (playlist_id,), + ) notify("playlists_changed") + + def get_playlists() -> list[PlaylistWrapper]: results = [] conn = connect_to_db() # get all local playlists - local = conn.execute(""" + local = conn.execute( + """ SELECT id, title, description, thumbnail FROM local_playlists - """).fetchall() + """ + ).fetchall() for collection in local: p = LocalPlaylist(collection[0], collection[1], collection[2]) p.thumbnail = collection[3] results.append(PlaylistWrapper.from_local(p)) # get all bookmark playlists - glob = conn.execute(""" + glob = conn.execute( + """ SELECT playlists.server, url, id, title, channel_id, channel_name, thumbnail FROM playlists, bookmarked_playlists WHERE playlists.server = bookmarked_playlists.server AND playlists.id = bookmarked_playlists.playlist_id - """) + """ + ) for collection in glob: - p = Playlist(collection[0], collection[1], collection[2], collection[3], (collection[5], collection[4]), collection[6]) + p = Playlist( + collection[0], + collection[1], + collection[2], + collection[3], + (collection[5], collection[4]), + collection[6], + ) results.append(PlaylistWrapper.from_external(p)) return results -def add_to_local_playlist(playlist_id:int, vid, pos=None): + + +def add_to_local_playlist(playlist_id: int, vid, pos=None): ensure_video(vid) conn = connect_to_db() if pos is None: - vids = conn.execute(""" + vids = conn.execute( + """ SELECT MAX(position) FROM local_playlist_content, videos WHERE local_playlist_content.id = ? AND local_playlist_content.video_id = videos.id AND local_playlist_content.video_server = videos.server - """, (playlist_id,)).fetchone() - pos = vids[0]+1 - execute_sql(conn, """ + """, + (playlist_id,), + ).fetchone() + pos = vids[0] + 1 + execute_sql( + conn, + """ INSERT INTO local_playlist_content VALUES (?, ?, ?, ?) - """, (playlist_id, vid.id, vid.server, pos)) + """, + (playlist_id, vid.id, vid.server, pos), + ) conn.close() notify("playlists_changed") -def delete_from_local_playlist(playlist_id: int, pos:int): + + +def delete_from_local_playlist(playlist_id: int, pos: int): conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ DELETE FROM local_playlist_content WHERE id = ? AND position = ? - """, (playlist_id, pos)) + """, + (playlist_id, pos), + ) conn.close() notify("playlists_changed") -def get_local_playlist(playlist_id:int) -> LocalPlaylist: + + +def get_local_playlist(playlist_id: int) -> LocalPlaylist: conn = connect_to_db() - resp = conn.execute(""" + resp = conn.execute( + """ SELECT id, title, description, thumbnail FROM local_playlists WHERE id = ? - """, (playlist_id,)).fetchone() + """, + (playlist_id,), + ).fetchone() p = LocalPlaylist(resp[0], resp[1], resp[2]) p.thumbnail = resp[3] - vids = conn.execute(""" + vids = conn.execute( + """ SELECT server, videos.id, url, title, description, thumbnail, channel_id, channel_name, position FROM local_playlist_content, videos WHERE local_playlist_content.id = ? AND local_playlist_content.video_id = videos.id AND local_playlist_content.video_server = videos.server ORDER BY position - """, (playlist_id,)).fetchall() - p.content = [ (Video(srv, url, vid, title, (cname, cid), desc, thumb), pos) for (srv, vid, url, title, desc, thumb, cid, cname, pos) in vids ] + """, + (playlist_id,), + ).fetchall() + p.content = [ + (Video(srv, url, vid, title, (cname, cid), desc, thumb), pos) + for (srv, vid, url, title, desc, thumb, cid, cname, pos) in vids + ] return p + + def set_local_playlist_thumbnail(playlist_id: int, thumb: str): conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ UPDATE local_playlists SET thumbnail = ? WHERE id = ? - """, (thumb, playlist_id)) + """, + (thumb, playlist_id), + ) conn.close() notify("playlists_changed") + def add_to_history(vid, uts=None): """ Takes :Video type and adds the video to the history @@ -672,15 +954,20 @@ def add_to_history(vid, uts=None): uts = datetime.now().timestamp() ensure_video(vid) conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT INTO history VALUES (?, ?, ?) """, - (uts, vid.id, vid.server)) + (uts, vid.id, vid.server), + ) conn.close() notify("history_changed") return False -def add_history_items(items:list[(Video, int)]): + + +def add_history_items(items: list[(Video, int)]): # NOTE: you have to ensure the videos exist yourself conn = connect_to_db() execute_sql( @@ -690,40 +977,59 @@ def add_history_items(items:list[(Video, int)]): VALUES (?, ?, ?) """, [(d[1], d[0].id, d[0].server) for d in items], - many=True) + many=True, + ) conn.close() notify("history_changed") + def get_history(): """ Returns a list of (Video, uts) tuples """ conn = connect_to_db() - results = conn.execute(""" + results = conn.execute( + """ SELECT timestamp, server, id, url, title, description, thumbnail, channel_id, channel_name FROM history,videos WHERE history.video_id = id AND history.video_server = server ORDER BY timestamp DESC - """).fetchall() - return [ (Video(d[1], d[3], d[2], d[4], (d[8], d[7]), d[5], d[6]), d[0]) for d in results ] + """ + ).fetchall() + return [ + (Video(d[1], d[3], d[2], d[4], (d[8], d[7]), d[5], d[6]), d[0]) for d in results + ] + def get_cached_feed(limit=100): """ Returns a list of (Video, uts) tuples """ conn = connect_to_db() - results = conn.execute(""" + results = conn.execute( + """ SELECT DISTINCT timestamp, server, id, url, title, description, thumbnail, channel_id, channel_name FROM news join videos on news.video_id = videos.id AND news.video_server = videos.server ORDER BY timestamp DESC LIMIT ? - """, (limit,)).fetchall() - return [ (Video(d[1], d[3], d[2], d[4], (d[8], d[7]), d[5], d[6]), d[0]) for d in results ] + """, + (limit,), + ).fetchall() + return [ + (Video(d[1], d[3], d[2], d[4], (d[8], d[7]), d[5], d[6]), d[0]) for d in results + ] + + def clear_cached_feed(): conn = connect_to_db() - execute_sql(conn, """ + execute_sql( + conn, + """ DELETE FROM news - """) + """, + ) + + def update_cached_feed(ls: list[(Video, int)]): uts = datetime.now().timestamp() conn = connect_to_db() @@ -737,74 +1043,104 @@ def update_cached_feed(ls: list[(Video, int)]): """ INSERT OR REPLACE INTO appconf VALUES (?,?) - """, ("news-feed-refresh", uts)) + """, + ("news-feed-refresh", uts), + ) # update cached items for entry in ls: vid = entry[0] uts = entry[1] ensure_video(vid) - execute_sql(conn, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO news VALUES (?, ?, ?) """, - (uts, vid.id, vid.server)) + (uts, vid.id, vid.server), + ) + + def get_last_feed_refresh() -> int: app_conf = get_app_settings() if "news-feed-refresh" in app_conf: return app_conf["news-feed-refresh"] return None -def get_video_duration(server_id:str, video_id:str) -> (float|None): + +def get_video_duration(server_id: str, video_id: str) -> float | None: conn = connect_to_db() - results = conn.execute(""" + results = conn.execute( + """ SELECT duration FROM playback WHERE video_id = ? AND video_server = ? - """, (video_id, server_id)).fetchone() + """, + (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): + +def get_video_playback_position(server_id: str, video_id: str) -> float | None: conn = connect_to_db() - results = conn.execute(""" + results = conn.execute( + """ SELECT position FROM playback WHERE video_id = ? AND video_server = ? - """, (video_id, server_id)).fetchone() + """, + (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, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO playback VALUES (?,?,?,?,?) - """, (uts, vid.server, vid.id, duration, position)) + """, + (uts, vid.server, vid.id, duration, position), + ) conn.close() notify("playback_changed") -def set_video_playback_position_force(server_id: str, video_id:str, position: float, duration: float): + +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, """ + execute_sql( + conn, + """ INSERT OR REPLACE INTO playback VALUES (?,?,?,?,?) - """, (uts, server_id, video_id, duration, position)) + """, + (uts, server_id, video_id, duration, position), + ) conn.close() notify("playback_changed") -def get_last_playback() -> (tuple[Video, float, float] | None): + +def get_last_playback() -> tuple[Video, float, float] | None: conn = connect_to_db() - results = conn.execute(""" + 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() + """ + ).fetchone() if results is None: return None d = results diff --git a/melon/models/callbacks.py b/melon/models/callbacks.py index ec2e045..a28976b 100644 --- a/melon/models/callbacks.py +++ b/melon/models/callbacks.py @@ -12,28 +12,38 @@ callbacks = { "history_changed": {}, "settings_changed": {}, "playback_changed": {}, - "quit": {} + "quit": {}, } frozen = False + + def freeze(): global frozen frozen = True + + def unfreeze(): global frozen frozen = False # notify everything - to refresh everything we might have missed for key in callbacks: notify(key) -def register_callback(target: str, callback_id:str, function): + + +def register_callback(target: str, callback_id: str, function): if not target in callbacks: return callbacks[target][callback_id] = function + + def unregister_callback(target: str, callback_id: str): if not target in callbacks: return if callback_id in callbacks[target]: callbacks[target].pop(callback_id) -def notify(target:str): + + +def notify(target: str): if frozen: return if not target in callbacks: diff --git a/melon/player/__init__.py b/melon/player/__init__.py index 6d23ab8..f38ab79 100644 --- a/melon/player/__init__.py +++ b/melon/player/__init__.py @@ -1,8 +1,9 @@ import gi + gi.require_version("WebKit", "6.0") -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') -gi.require_version('Gst', '1.0') +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +gi.require_version("Gst", "1.0") from gi.repository import Gtk, Adw, WebKit, GLib, Gst from unidecode import unidecode from gettext import gettext as _ @@ -15,9 +16,15 @@ 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, get_video_duration +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): def on_open_in_browser(self, arg): Gtk.UriLauncher.new(uri=self.video.url).launch() @@ -27,14 +34,16 @@ class PlayerScreen(Adw.NavigationPage): self.scrollview.set_child(self.box) # video details self.about = Adw.PreferencesGroup() - self.about.set_title(unidecode(self.video.title).replace("&","&")) + self.about.set_title(unidecode(self.video.title).replace("&", "&")) # expandable description field desc_field = Adw.ExpanderRow() desc_field.set_title(_("Description")) - desc_field.set_subtitle(unidecode(self.video.description[:40]).replace("&","&")+"...") + desc_field.set_subtitle( + unidecode(self.video.description[:40]).replace("&", "&") + "..." + ) desc = Adw.ActionRow() - desc.set_subtitle(unidecode(self.video.description).replace("&","&")) + desc.set_subtitle(unidecode(self.video.description).replace("&", "&")) desc_field.add_row(desc) self.about.add(desc_field) self.box.append(self.about) @@ -47,7 +56,8 @@ class PlayerScreen(Adw.NavigationPage): btn_bookmark.set_activatable(True) btn_bookmark.set_action_name("win.add_to_playlist") btn_bookmark.set_action_target_value( - GLib.Variant("as", [self.video.server, self.video.id])) + GLib.Variant("as", [self.video.server, self.video.id]) + ) self.about.add(btn_bookmark) def display_player(self): @@ -125,6 +135,7 @@ class PlayerScreen(Adw.NavigationPage): GLib.idle_add(self.display_channel) view = None + def __init__(self, server_id, video_id, *args, **kwargs): super().__init__(*args, **kwargs) @@ -178,7 +189,7 @@ class PlayerScreen(Adw.NavigationPage): 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 + consider_done = 0.99 * dur # video was probably watched till end # reset playback position if pos >= consider_done: @@ -193,7 +204,7 @@ class PlayerScreen(Adw.NavigationPage): if not self.external_btn is None: self.header_bar.remove(self.external_btn) - self.external_btn = IconButton("","modem-symbolic") + self.external_btn = IconButton("", "modem-symbolic") self.external_btn.connect("clicked", self.on_open_in_browser) self.header_bar.pack_end(self.external_btn) @@ -206,7 +217,7 @@ class PlayerScreen(Adw.NavigationPage): cb.set_center_widget(spinner) self.scrollview.set_child(cb) - self.box = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) + 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 padding = 12 diff --git a/melon/player/playlist.py b/melon/player/playlist.py index d81995f..dd016cb 100644 --- a/melon/player/playlist.py +++ b/melon/player/playlist.py @@ -1,7 +1,8 @@ import gi + gi.require_version("WebKit", "6.0") -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') +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 _ @@ -14,13 +15,18 @@ 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, +) 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 @@ -31,7 +37,7 @@ class PlaylistPlayerScreen(PlayerScreen): playlist_repeat = False playlist_repeat_single = False - def __init__(self, ref: (tuple[str, str] | int), index=0, *args, **kwargs): + 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 @@ -75,7 +81,7 @@ class PlaylistPlayerScreen(PlayerScreen): self.thread.start() # fetch playlist data & store it interally - def prepare_playlist_task(self, ref: (tuple[str, str] | int)): + def prepare_playlist_task(self, ref: tuple[str, str] | int): if isinstance(ref, int): playlist_id = ref # local playlist @@ -113,7 +119,7 @@ class PlaylistPlayerScreen(PlayerScreen): if not ind is None: self.prepare(ind) - def get_next_index(self,fallback=None): + def get_next_index(self, fallback=None): index = self.playlist_index + 1 if not fallback is None: index = fallback @@ -124,10 +130,10 @@ class PlaylistPlayerScreen(PlayerScreen): elif self.playlist_shuffle: # select a random title # NOTE: may just repeat the same song again - index = random.randrange(0,len(self.playlist_content), 1) + index = random.randrange(0, len(self.playlist_content), 1) return index - if self.playlist_index+1 >= len(self.playlist_content): + if self.playlist_index + 1 >= len(self.playlist_content): # playlist ended if self.playlist_repeat: # playlist is set to repeat so we continue picking songs @@ -202,6 +208,7 @@ class PlaylistPlayerScreen(PlayerScreen): def ctr_toggle_playlist_repeat(self, state): self.playlist_repeat = state + def ctr_toggle_playlist_repeat_single(self, state): self.playlist_repeat_single = state @@ -216,8 +223,12 @@ class PlaylistPlayerScreen(PlayerScreen): if self.playlist_index > 0 and not self.playlist_shuffle: prev_btn = Adw.ActionRow() prev_btn.set_title(_("Previous")) - prev_btn.set_subtitle(_("Play video that comes before this one in the playlist")) - prev_btn.add_suffix(Gtk.Image.new_from_icon_name("media-skip-backward-symbolic")) + prev_btn.set_subtitle( + _("Play video that comes before this one in the playlist") + ) + prev_btn.add_suffix( + Gtk.Image.new_from_icon_name("media-skip-backward-symbolic") + ) prev_btn.connect("activated", lambda _: self.ctr_previous_video()) prev_btn.set_activatable(True) self.ctr_group.add(prev_btn) @@ -225,11 +236,18 @@ class PlaylistPlayerScreen(PlayerScreen): # if the video isn't the last # show a next button # not shown when shuffle is enbaled - if self.playlist_index+1 < len(self.playlist_content) and not self.playlist_shuffle: + if ( + self.playlist_index + 1 < len(self.playlist_content) + and not self.playlist_shuffle + ): next_btn = Adw.ActionRow() next_btn.set_title(_("Next")) - next_btn.set_subtitle(_("Play video that comes after this one in the playlist")) - next_btn.add_suffix(Gtk.Image.new_from_icon_name("media-skip-forward-symbolic")) + next_btn.set_subtitle( + _("Play video that comes after this one in the playlist") + ) + next_btn.add_suffix( + Gtk.Image.new_from_icon_name("media-skip-forward-symbolic") + ) next_btn.connect("activated", lambda _: self.ctr_next_video()) next_btn.set_activatable(True) self.ctr_group.add(next_btn) @@ -239,7 +257,9 @@ class PlaylistPlayerScreen(PlayerScreen): skip_btn = Adw.ActionRow() skip_btn.set_title(_("Skip")) skip_btn.set_subtitle(_("Skip this video and pick a new one at random")) - skip_btn.add_suffix(Gtk.Image.new_from_icon_name("media-skip-forward-symbolic")) + skip_btn.add_suffix( + Gtk.Image.new_from_icon_name("media-skip-forward-symbolic") + ) skip_btn.connect("activated", lambda _: self.ctr_skip_video()) skip_btn.set_activatable(True) self.ctr_group.add(skip_btn) @@ -251,7 +271,8 @@ class PlaylistPlayerScreen(PlayerScreen): _("Chooses the next video at random"), PreferenceType.TOGGLE, self.playlist_shuffle, - self.playlist_shuffle) + self.playlist_shuffle, + ) widg_shuffle = PreferenceRow(pref_shuffle) widg_shuffle.set_callback(self.ctr_toggle_shuffle) self.ctr_group.add(widg_shuffle.get_widget()) @@ -262,7 +283,8 @@ class PlaylistPlayerScreen(PlayerScreen): _("Puts this video on loop"), PreferenceType.TOGGLE, self.playlist_repeat_single, - self.playlist_repeat_single) + self.playlist_repeat_single, + ) widg_repeat_single = PreferenceRow(pref_repeat_single) widg_repeat_single.set_callback(self.ctr_toggle_playlist_repeat_single) self.ctr_group.add(widg_repeat_single.get_widget()) @@ -274,10 +296,13 @@ class PlaylistPlayerScreen(PlayerScreen): pref_repeat = Preference( "playlist-repeat", _("Repeat playlist"), - _("Start playling the playlist from the beginning after reaching the end"), + _( + "Start playling the playlist from the beginning after reaching the end" + ), PreferenceType.TOGGLE, self.playlist_repeat, - self.playlist_repeat) + self.playlist_repeat, + ) widg_repeat = PreferenceRow(pref_repeat) widg_repeat.set_callback(self.ctr_toggle_playlist_repeat) self.ctr_group.add(widg_repeat.get_widget()) @@ -285,11 +310,15 @@ class PlaylistPlayerScreen(PlayerScreen): # expander with playlist content expander = Adw.ExpanderRow() expander.set_title(_("Playlist Content")) - expander.set_subtitle(_("Click on videos to continue playing the playlist from there")) + expander.set_subtitle( + _("Click on videos to continue playing the playlist from there") + ) for i in range(len(self.playlist_content)): item = self.playlist_content[i] # onClick: starts playling the playlist from - row = AdaptiveFeedItem(item, onClick=pass_me(lambda _, i: self.prepare(i), i)) + row = AdaptiveFeedItem( + item, onClick=pass_me(lambda _, i: self.prepare(i), i) + ) expander.add_row(row) self.ctr_group.add(expander) diff --git a/melon/playlist/__init__.py b/melon/playlist/__init__.py index 99e6a2d..2967672 100644 --- a/melon/playlist/__init__.py +++ b/melon/playlist/__init__.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib from unidecode import unidecode from gettext import gettext as _ @@ -11,10 +12,24 @@ from melon.servers import Preference, PreferenceType from melon.widgets.iconbutton import IconButton from melon.widgets.feeditem import AdaptiveFeedItem from melon.widgets.simpledialog import SimpleDialog -from melon.models import get_app_settings, get_local_playlist, PlaylistWrapper, ensure_playlist, ensure_delete_local_playlist, set_local_playlist_thumbnail, delete_from_local_playlist -from melon.models import is_server_enabled, ensure_server_disabled, ensure_server_enabled, register_callback +from melon.models import ( + get_app_settings, + get_local_playlist, + PlaylistWrapper, + ensure_playlist, + ensure_delete_local_playlist, + set_local_playlist_thumbnail, + delete_from_local_playlist, +) +from melon.models import ( + is_server_enabled, + ensure_server_disabled, + ensure_server_enabled, + register_callback, +) from melon.utils import pass_me + class LocalPlaylistScreen(Adw.NavigationPage): def __init__(self, playlist_id, *args, **kwargs): super().__init__(*args, **kwargs) @@ -50,16 +65,19 @@ 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 = 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.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*")) - status.set_description(_("You haven't added any videos to this playlist yet")) + status.set_description( + _("You haven't added any videos to this playlist yet") + ) status.set_icon_name("weather-few-clouds-night-symbolic") icon_button = IconButton(_("Start watching"), "video-display-symbolic") icon_button.set_action_name("win.home") @@ -81,11 +99,25 @@ class LocalPlaylistScreen(Adw.NavigationPage): app_conf = get_app_settings() self.box.add(group) # add playlist content to group as well - for (entry, index) in sorted(playlist.content, key=lambda x:x[1]): + for entry, index in sorted(playlist.content, key=lambda x: x[1]): item = AdaptiveFeedItem(entry) item.menuitems = [ - (_("Set as playlist thumbnail"), pass_me(lambda pid, thumb: set_local_playlist_thumbnail(pid, thumb), self.playlist_id, entry.thumbnail)), - (_("Remove from playlist"), pass_me(lambda pid, pos: delete_from_local_playlist(pid, pos), self.playlist_id, index)) + ( + _("Set as playlist thumbnail"), + pass_me( + lambda pid, thumb: set_local_playlist_thumbnail(pid, thumb), + self.playlist_id, + entry.thumbnail, + ), + ), + ( + _("Remove from playlist"), + pass_me( + lambda pid, pos: delete_from_local_playlist(pid, pos), + self.playlist_id, + index, + ), + ), ] group.add(item) @@ -114,7 +146,9 @@ class LocalPlaylistScreen(Adw.NavigationPage): input_group.add(self.input_desc) save_btn = Adw.ActionRow() save_btn.set_title(_("Save details")) - save_btn.set_subtitle(_("Change playlist title and description. (Closes the dialog)")) + save_btn.set_subtitle( + _("Change playlist title and description. (Closes the dialog)") + ) save_btn.add_suffix(Gtk.Image.new_from_icon_name("go-next-symbolic")) save_btn.set_activatable(True) save_btn.connect("activated", lambda _: self.save_details()) @@ -125,7 +159,9 @@ class LocalPlaylistScreen(Adw.NavigationPage): dgroup = Adw.PreferencesGroup() delete_btn = Adw.ActionRow() delete_btn.set_title(_("Delete Playlist")) - delete_btn.set_subtitle(_("Delete this playlist and it's content. This can NOT be undone.")) + delete_btn.set_subtitle( + _("Delete this playlist and it's content. This can NOT be undone.") + ) delete_btn.add_suffix(Gtk.Image.new_from_icon_name("go-next-symbolic")) delete_btn.set_activatable(True) delete_btn.connect("activated", lambda _: self.confirm_delete()) @@ -136,10 +172,12 @@ class LocalPlaylistScreen(Adw.NavigationPage): # to manually close the dialog # without applying changes bottom_bar = Gtk.Box() - btn_cancel = IconButton(_("Close"), "process-stop-symbolic", tooltip=_("Close without changing anything")) - btn_cancel.connect( - "clicked", - lambda x: self.edit_diag.hide()) + btn_cancel = IconButton( + _("Close"), + "process-stop-symbolic", + tooltip=_("Close without changing anything"), + ) + btn_cancel.connect("clicked", lambda x: self.edit_diag.hide()) padding = 12 btn_cancel.set_vexpand(True) btn_cancel.set_hexpand(True) @@ -174,27 +212,28 @@ class LocalPlaylistScreen(Adw.NavigationPage): self.dlt_diag.set_widget(info) bottom_bar = Gtk.Box() - btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not delete the playlist")) - btn_confirm = IconButton(_("Delete"), "list-add-symbolic", tooltip=_("Delete this playlist")) - btn_confirm.connect( - "clicked", - lambda _: self.delete() + btn_cancel = IconButton( + _("Cancel"), + "process-stop-symbolic", + tooltip=_("Do not delete the playlist"), + ) + btn_confirm = IconButton( + _("Delete"), "list-add-symbolic", tooltip=_("Delete this playlist") ) - btn_cancel.connect( - "clicked", - lambda _: self.dlt_diag.hide()) + btn_confirm.connect("clicked", lambda _: self.delete()) + btn_cancel.connect("clicked", lambda _: self.dlt_diag.hide()) padding = 12 btn_confirm.set_vexpand(True) btn_confirm.set_hexpand(True) btn_confirm.set_margin_end(padding) - btn_confirm.set_margin_start(padding/2) + btn_confirm.set_margin_start(padding / 2) btn_confirm.set_margin_top(padding) btn_confirm.set_margin_bottom(padding) btn_cancel.set_vexpand(True) btn_cancel.set_hexpand(True) btn_cancel.set_margin_start(padding) - btn_cancel.set_margin_end(padding/2) + btn_cancel.set_margin_end(padding / 2) btn_cancel.set_margin_top(padding) btn_cancel.set_margin_bottom(padding) bottom_bar.append(btn_cancel) diff --git a/melon/playlist/create.py b/melon/playlist/create.py index 2785a7b..c4b3065 100644 --- a/melon/playlist/create.py +++ b/melon/playlist/create.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, GLib from gettext import gettext as _ import threading @@ -12,21 +13,26 @@ from melon.widgets.feeditem import AdaptiveFeedItem from melon.models import new_local_playlist, Video from melon.servers.utils import get_app_settings + class PlaylistCreatorDialog(SimpleDialog): def display(self, video=None): page = Adw.PreferencesPage() self.content = [] if not video is None: - self.content = [ video ] + self.content = [video] # use preference group as preview for video element preview_group = Adw.PreferencesGroup() preview_group.set_title(_("Video")) - preview_group.set_description(_("The following video will be added to the new playlist")) - preview_group.add(AdaptiveFeedItem( - video, - clickable=False, - show_preview=get_app_settings()["show_images_in_feed"] - )) + preview_group.set_description( + _("The following video will be added to the new playlist") + ) + preview_group.add( + AdaptiveFeedItem( + video, + clickable=False, + show_preview=get_app_settings()["show_images_in_feed"], + ) + ) page.add(preview_group) # use preference group for input input_group = Adw.PreferencesGroup() @@ -44,27 +50,26 @@ class PlaylistCreatorDialog(SimpleDialog): page.add(input_group) bottom_bar = Gtk.Box() - btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not create playlist")) - btn_confirm = IconButton(_("Create"), "list-add-symbolic", tooltip=_("Create playlist")) - btn_confirm.connect( - "clicked", - self.create_playlist + btn_cancel = IconButton( + _("Cancel"), "process-stop-symbolic", tooltip=_("Do not create playlist") + ) + btn_confirm = IconButton( + _("Create"), "list-add-symbolic", tooltip=_("Create playlist") ) - btn_cancel.connect( - "clicked", - lambda x: self.hide()) + btn_confirm.connect("clicked", self.create_playlist) + btn_cancel.connect("clicked", lambda x: self.hide()) padding = 12 btn_confirm.set_vexpand(True) btn_confirm.set_hexpand(True) btn_confirm.set_margin_end(padding) - btn_confirm.set_margin_start(padding/2) + btn_confirm.set_margin_start(padding / 2) btn_confirm.set_margin_top(padding) btn_confirm.set_margin_bottom(padding) btn_cancel.set_vexpand(True) btn_cancel.set_hexpand(True) btn_cancel.set_margin_start(padding) - btn_cancel.set_margin_end(padding/2) + btn_cancel.set_margin_end(padding / 2) btn_cancel.set_margin_top(padding) btn_cancel.set_margin_bottom(padding) bottom_bar.append(btn_cancel) @@ -72,6 +77,7 @@ class PlaylistCreatorDialog(SimpleDialog): self.toolbar_view.add_bottom_bar(bottom_bar) self.set_widget(page) + def background(self, target=None): video = None if isinstance(target, Video): @@ -86,6 +92,7 @@ class PlaylistCreatorDialog(SimpleDialog): instance = get_server_instance(servers[server_id]) video = instance.get_video_info(video_id) GLib.idle_add(self.display, video) + def __init__(self, video=None, *args, **kwargs): super().__init__(*args, **kwargs) self.set_title(_("New Playlist")) @@ -106,7 +113,6 @@ class PlaylistCreatorDialog(SimpleDialog): def create_playlist(self, x): new_local_playlist( - self.input_title.get_text(), - self.input_desc.get_text(), - self.content) + self.input_title.get_text(), self.input_desc.get_text(), self.content + ) self.hide() diff --git a/melon/playlist/pick.py b/melon/playlist/pick.py index 0a54cde..47eb835 100644 --- a/melon/playlist/pick.py +++ b/melon/playlist/pick.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, GLib, Gdk from unidecode import unidecode from gettext import gettext as _ @@ -11,9 +12,15 @@ from melon.widgets.feeditem import AdaptiveFeedItem, AdaptivePlaylistFeedItem from melon.widgets.iconbutton import IconButton from melon.widgets.simpledialog import SimpleDialog from melon.models import get_playlists, PlaylistWrapperType, add_to_local_playlist -from melon.servers.utils import get_app_settings, pixbuf_from_url, get_servers_list, get_server_instance +from melon.servers.utils import ( + get_app_settings, + pixbuf_from_url, + get_servers_list, + get_server_instance, +) from melon.playlist.create import PlaylistCreatorDialog + class PlaylistPickerDialog(SimpleDialog): def open_creator(self, video): diag = PlaylistCreatorDialog(video) @@ -26,18 +33,26 @@ class PlaylistPickerDialog(SimpleDialog): # use preference group as preview for video element preview_group = Adw.PreferencesGroup() preview_group.set_title(_("Video")) - preview_group.set_description(_("The following video will be added to the playlist")) - preview_group.add(AdaptiveFeedItem( - video, - clickable=False, - show_preview=get_app_settings()["show_images_in_feed"] - )) + preview_group.set_description( + _("The following video will be added to the playlist") + ) + preview_group.add( + AdaptiveFeedItem( + video, + clickable=False, + show_preview=get_app_settings()["show_images_in_feed"], + ) + ) page.add(preview_group) group = Adw.PreferencesGroup() page.add(group) group.set_title(_("Add to playlist")) - group.set_description(_("Choose a playlist to add the video to. Note that you can only add videos to local playlists, not external bookmarked ones")) + group.set_description( + _( + "Choose a playlist to add the video to. Note that you can only add videos to local playlists, not external bookmarked ones" + ) + ) make_new = Adw.ActionRow() make_new.set_title(_("Create new playlist")) make_new.set_subtitle(_("Create a new playlist and add the video to it")) @@ -47,10 +62,7 @@ class PlaylistPickerDialog(SimpleDialog): # make_new.set_action_target_value( # GLib.Variant("as", [video.server, video.id])) # so we'll manually trigger this instead - make_new.connect( - "activated", - lambda _: self.open_creator(video) - ) + make_new.connect("activated", lambda _: self.open_creator(video)) make_new.set_activatable(True) group.add(make_new) @@ -61,13 +73,20 @@ class PlaylistPickerDialog(SimpleDialog): row = AdaptivePlaylistFeedItem( playlist, onClick=pass_me( - lambda _, playlist, video: add_to_local_playlist(playlist.inner.id, video) or self.hide(), - playlist, video) + lambda _, playlist, video: add_to_local_playlist( + playlist.inner.id, video + ) + or self.hide(), + playlist, + video, + ), ) group.add(row) bottom_bar = Gtk.Box() - btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not create playlist")) + btn_cancel = IconButton( + _("Cancel"), "process-stop-symbolic", tooltip=_("Do not create playlist") + ) padding = 12 btn_cancel.set_vexpand(True) btn_cancel.set_hexpand(True) @@ -76,9 +95,7 @@ class PlaylistPickerDialog(SimpleDialog): btn_cancel.set_margin_top(padding) btn_cancel.set_margin_bottom(padding) bottom_bar.append(btn_cancel) - btn_cancel.connect( - "clicked", - lambda x:self.hide()) + btn_cancel.connect("clicked", lambda x: self.hide()) self.toolbar_view.add_bottom_bar(bottom_bar) self.set_widget(page) @@ -119,5 +136,6 @@ class PlaylistPickerDialog(SimpleDialog): self.thread.daemon = True self.thread.start() + def pass_me(func, *args): return lambda x: func(x, *args) diff --git a/melon/servers/__init__.py b/melon/servers/__init__.py index 4bf7bc8..fbb4e25 100644 --- a/melon/servers/__init__.py +++ b/melon/servers/__init__.py @@ -1,9 +1,10 @@ -from enum import Flag,Enum,auto -from abc import ABC,abstractmethod +from enum import Flag, Enum, auto +from abc import ABC, abstractmethod import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') -gi.require_version('WebKit', '6.0') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +gi.require_version("WebKit", "6.0") from gi.repository import GObject, WebKit from typing import Callable @@ -12,11 +13,12 @@ from melon.import_providers import ImportProvider REQUESTS_TIMEOUT = 5 -USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0' -USER_AGENT_MOBILE = 'Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Build/KLP) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30' +USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0" +USER_AGENT_MOBILE = "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Build/KLP) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30" server_finder.install() + class SearchMode(Enum): # show all results ANY = auto() @@ -27,6 +29,7 @@ class SearchMode(Enum): # show videos VIDEOS = auto() + class PreferenceType(Enum): # insert text # default:str, value:str @@ -46,6 +49,7 @@ class PreferenceType(Enum): # default: map[str,str], value: str DROPDOWN = auto() + class Preference: # name of the settings entry name: str @@ -53,12 +57,13 @@ class Preference: description: str # preference type type: PreferenceType - # default value + # default value # NOTE: should correspond to type default: any # current value # will be updated from initial database when initalized (when used in a server) value: any + def __init__(self, id, name, description, type, default, value): self.id = id self.name = name @@ -67,6 +72,7 @@ class Preference: self.default = default self.value = value + class Resource(GObject.Object): # external url of the resource # used for "Open in Browser" @@ -76,13 +82,15 @@ class Resource(GObject.Object): # i.e when trying to get additional information id: str # id of server from where the resource comes - server:str + server: str + def __init__(self, server, url, id): super().__init__() self.server = server self.id = id self.url = url + class Video(Resource): # video title title: str @@ -91,13 +99,16 @@ class Video(Resource): # video thumbnail url (None if non existend) thumbnail: str # name and id of channel - channel: (str,str) - def __init__(self, server, url, id, title, channel,desc, thumb): + channel: (str, str) + + def __init__(self, server, url, id, title, channel, desc, thumb): super().__init__(server, url, id) self.title = title self.description = desc self.thumbnail = thumb self.channel = channel + + class Playlist(Resource): # video title title: str @@ -106,12 +117,15 @@ class Playlist(Resource): # preview image url (None if non existend) thumbnail: str # name and id of channel - channel: (str,str) + channel: (str, str) + def __init__(self, server, url, id, title, channel, thumb): super().__init__(server, url, id) self.title = title self.thumbnail = thumb self.channel = channel + + class Channel(Resource): # channel name name: str @@ -119,12 +133,14 @@ class Channel(Resource): bio: str # channel logo url (None if non existend) avatar: str + def __init__(self, server, url, id, name, bio, avatar): super().__init__(server, url, id) self.name = name self.bio = bio self.avatar = avatar + class Feed: # external url url: str @@ -134,24 +150,29 @@ class Feed: name: str # icon to display for category/feed icon: str = "starred-symbolic" + def __init__(self, id, name, icon=None): self.id = id self.name = name self.icon = icon + def with_url(self, url): n = Feed(self.id, self.name, self.icon) n.url = url return n + class Stream: # stream url used for playback url: str # quality name/id used in selector dropdown quality: str + def __init__(self, url, quality): self.url = url self.quality = quality + class Server(ABC): # required to indeitfy the server in the database id: str @@ -166,30 +187,35 @@ class Server(ABC): # set to true if the service only works after logging in requires_login: bool = False # list of Preference's - settings:dict[str,Preference] = {} + settings: dict[str, Preference] = {} + @abstractmethod def get_external_url(self) -> str: """ External homepage url """ pass + def get_public_feeds(self) -> list[Feed]: """ Returns a list of available feeds i.e Popular """ return [] - def get_public_feed_content(self,id:str) -> list[Resource]: + + def get_public_feed_content(self, id: str) -> list[Resource]: """Returns a list of videos in the given feed""" return [] + @abstractmethod - def search(self, query:str, mask: SearchMode, page=0) -> list[Resource]: + def search(self, query: str, mask: SearchMode, page=0) -> list[Resource]: """ Search for a given query filter for things according to mask should also support pagination (if available) """ return [] + @abstractmethod def get_channel_info(self, cid: str) -> Channel: """ @@ -197,25 +223,31 @@ class Server(ABC): including channel name, avatar and description """ pass + @abstractmethod - def get_channel_feeds(self, cid:str) -> list[Feed]: + def get_channel_feeds(self, cid: str) -> list[Feed]: """ Returns a list of available channel feeds i.e. Videos, Playlists """ return [] + @abstractmethod - def get_default_channel_feed(self, cid:str) -> str: + def get_default_channel_feed(self, cid: str) -> str: """ Returns the id of the default channel feed """ pass + @abstractmethod - def get_channel_feed_content(self, cid: str, feed_id: str, page=0) -> list[Resource]: + def get_channel_feed_content( + self, cid: str, feed_id: str, page=0 + ) -> list[Resource]: """ Returns list of resources in the channel feed """ return [] + @abstractmethod def get_playlist_info(self, pid: str) -> Playlist: """ @@ -223,12 +255,14 @@ class Server(ABC): including name and avatar """ pass + @abstractmethod def get_playlist_content(self, pid: str) -> list[Resource]: """ Returns a list of resources in the playlist """ return [] + @abstractmethod def get_timeline(self, cid: str) -> list[(Resource, int)]: """ @@ -237,19 +271,22 @@ class Server(ABC): and sorting them """ return [] + @abstractmethod def get_video_info(self, vid: str) -> Video: """ Returns video resource object """ pass + @abstractmethod - def get_video_streams(self, vid:str) -> list[Stream]: + def get_video_streams(self, vid: str) -> list[Stream]: """ Returns a list of available streams i.e hd, 720p """ return [] + def get_import_providers(self) -> list[ImportProvider]: """ Returns a list of import providers, diff --git a/melon/servers/invidious/__init__.py b/melon/servers/invidious/__init__.py index cef1d22..a73b6ef 100644 --- a/melon/servers/invidious/__init__.py +++ b/melon/servers/invidious/__init__.py @@ -1,9 +1,10 @@ from bs4 import BeautifulSoup import requests -from urllib.parse import urlparse,parse_qs +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 @@ -13,14 +14,17 @@ 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" # I think newpipe uses service_id 0 for youtube content service_id = 0 + def __init__(self, url): self.base_url = url self.img_url = url + # NOTE: uses beautifulsoup instead of the invidious api # because not all invidious servers provide the api # and even if they do, most of the time it is ratelimited @@ -34,19 +38,22 @@ class Invidious(Server): "instance": Preference( "instance", _("Instance"), - _("See https://docs.invidious.io/instances/ for a list of available instances"), + _( + "See https://docs.invidious.io/instances/ for a list of available instances" + ), PreferenceType.TEXT, "https://inv.tux.pizza", - "https://inv.tux.pizza"), - } + "https://inv.tux.pizza", + ), + } # youtube might contain 18+ content # so we have to indicate that this may contain nsfw content - is_nsfw = True, + is_nsfw = (True,) known_public_feeds = { "/feed/trending": Feed("trending", _("Trending"), "starred-symbolic"), - "/feed/popular": Feed("popular", _("Popular"), "emblem-favorite-symbolic") + "/feed/popular": Feed("popular", _("Popular"), "emblem-favorite-symbolic"), } def get_external_url(self): @@ -67,18 +74,19 @@ class Invidious(Server): if r.ok: soup = BeautifulSoup(r.text, "lxml") # extract all elements in the navigation header - links = soup.find_all("a", {"class":"feed-menu-item"}) + links = soup.find_all("a", {"class": "feed-menu-item"}) feeds = [] for elem in links: id = elem["href"] # lookup the entries and if it is valid save it if id in self.known_public_feeds: - feeds.append(self.known_public_feeds[id] - .with_url(f"{instance}{id}")) + feeds.append( + self.known_public_feeds[id].with_url(f"{instance}{id}") + ) return feeds return [] - def get_public_feed_content(self,id): + def get_public_feed_content(self, id): """Returns a list of videos in the given feed""" instance = self.get_external_url() return self.request_and_parse(f"{instance}/feed/{id}") @@ -93,27 +101,32 @@ class Invidious(Server): t = "playlist" elif mask == SearchMode.VIDEOS: t = "video" - return self.request_and_parse(f"{instance}/search?q={query}&page={page}&type={t}") + return self.request_and_parse( + f"{instance}/search?q={query}&page={page}&type={t}" + ) def get_channel_info(self, cid: str): instance = self.get_external_url() url = f"{instance}/channel/{cid}" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: soup = BeautifulSoup(r.text, "lxml") - desc_elem = soup.find("div", { "id": "descriptionWrapper" }) - desc = ''.join(desc_elem.strings).strip() - profile = soup.find("div", { "class": "channel-profile" }) + desc_elem = soup.find("div", {"id": "descriptionWrapper"}) + desc = "".join(desc_elem.strings).strip() + profile = soup.find("div", {"class": "channel-profile"}) name = profile.find("span").string avatar = profile.find("img")["src"] return Channel(self.id, url, cid, name, desc, avatar) - def get_channel_feeds(self, cid:str) : + + def get_channel_feeds(self, cid: str): instance = self.get_external_url() url = f"{instance}/channel/{cid}" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: soup = BeautifulSoup(r.text, "lxml") - wrapper = soup.find("div", { "id": "descriptionWrapper" }).parent.find_next_sibling() + wrapper = soup.find( + "div", {"id": "descriptionWrapper"} + ).parent.find_next_sibling() nav = wrapper.find("div") # NOTE: the icons do not matter results = [Feed("", "Videos").with_url(url)] @@ -126,12 +139,12 @@ class Invidious(Server): if feed_id == "community": # text based posts are currently not supported continue - results.append(Feed(feed_id, link.string).with_url(url+feed_id)) + results.append(Feed(feed_id, link.string).with_url(url + feed_id)) return results - + return [] - def get_default_channel_feed(self, cid:str): + def get_default_channel_feed(self, cid: str): return "" def get_channel_feed_content(self, cid: str, feed_id: str, page=0): @@ -141,10 +154,10 @@ class Invidious(Server): def get_playlist_info(self, pid: str): instance = self.get_external_url() url = f"{instance}/playlist?list={pid}" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: soup = BeautifulSoup(r.text, "lxml") - headline = soup.find("div", { "class": "title" }) + headline = soup.find("div", {"class": "title"}) title_elem = headline.find("h3") title = title_elem.string details_elem = headline.find_next_sibling() @@ -161,7 +174,7 @@ class Invidious(Server): def get_timeline(self, cid: str): instance = self.get_external_url() url = f"{instance}/feed/channel/{cid}/" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: soup = BeautifulSoup(r.text, features="xml") feed = [] @@ -183,31 +196,40 @@ class Invidious(Server): channel_id_elem = entry.find("yt:channelid") channel_id = channel_id_elem.string channel_name = entry.find("author").find("name").string - url = entry.find("link", { "rel": "alternate" })["href"] + url = entry.find("link", {"rel": "alternate"})["href"] vid_title = entry.find("title").string - desc = ''.join(entry.find("content").strings).strip() + desc = "".join(entry.find("content").strings).strip() thumb = entry.find("media:thumbnail")["url"] date_str = entry.find("published").string uts = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S%z").timestamp() - feed.append((Video( - self.id, - url, vid_id, - vid_title, - (channel_name, channel_id), desc, thumb), uts)) + feed.append( + ( + Video( + self.id, + url, + vid_id, + vid_title, + (channel_name, channel_id), + desc, + thumb, + ), + uts, + ) + ) return feed return [] - def request_and_parse(self,url, recurse = False): + def request_and_parse(self, url, recurse=False): """Fetches the URL and returns a list of Resources""" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: soup = BeautifulSoup(r.text, "lxml") # every result is wrapped in an h-box - listings = soup.find_all("div", {"class":"h-box"}) + listings = soup.find_all("div", {"class": "h-box"}) results = [] for listing in listings: # the resource name can be found in the video-card-row div - row = listing.find("div", { "class": "video-card-row" }) + row = listing.find("div", {"class": "video-card-row"}) # the top bar also contains an h-box # so we have to skip entries without video-card-row elements if row is None: @@ -216,7 +238,7 @@ class Invidious(Server): link = row.find("a") # beautiful soup can automatically flatten # single nested elements - name = ''.join(link.strings).strip() + name = "".join(link.strings).strip() # there is only one image per h-box # so we can just use the first one img = listing.find("img")["src"] @@ -235,17 +257,19 @@ class Invidious(Server): if t == "channel": # is channel bio_elem = listing.find("h5") - bio = ''.join(bio_elem.strings).strip() - res = Channel(self.id, full_url, dt.path.split("/")[2], name, bio, img) + bio = "".join(bio_elem.strings).strip() + res = Channel( + self.id, full_url, dt.path.split("/")[2], name, bio, img + ) elif t == "playlist": # is playlist pid = parse_qs(dt.query)["list"][0] # the channelname is tthe second video-card-row in the h-box - ch = listing.find_all("div", { "class": "video-card-row" })[1] + ch = listing.find_all("div", {"class": "video-card-row"})[1] channel_name_elem = ch.find("p", {"class": "channel-name"}) # we have to iterate over substrings as well, # because verified channels also contain an i element - channel_name = ''.join(channel_name_elem.strings).strip() + channel_name = "".join(channel_name_elem.strings).strip() # the channel link is absolute with the form /channel/$id channel_id = ch.find("a")["href"].split("/")[2] channel = (channel_name, channel_id) @@ -254,16 +278,16 @@ class Invidious(Server): # is video vid = parse_qs(dt.query)["v"][0] # the channelname is tthe second video-card-row in the h-box - ch = listing.find_all("div", { "class": "video-card-row" })[1] + ch = listing.find_all("div", {"class": "video-card-row"})[1] channel_name_elem = ch.find("p", {"class": "channel-name"}) # we have to iterate over substrings as well, # because verified channels also contain an i element - channel_name = ''.join(channel_name_elem.strings).strip() + channel_name = "".join(channel_name_elem.strings).strip() # the channel link is absolute with the form /channel/$id channel_id = ch.find("a")["href"].split("/")[2] channel = (channel_name, channel_id) # we cannot get the description of a video from a list - res = Video(self.id, full_url, vid, name, channel,"", img) + res = Video(self.id, full_url, vid, name, channel, "", img) else: continue results.append(res) @@ -278,7 +302,7 @@ class Invidious(Server): except Exception as e: current_page = 1 instance = self.get_external_url() - listings = soup.find_all("a", {"class":"pure-button"}) + listings = soup.find_all("a", {"class": "pure-button"}) for lis in listings: iurl = lis["href"] iurlres = urlparse(iurl) @@ -305,31 +329,30 @@ class Invidious(Server): def get_video_info(self, vid: str): instance = self.get_external_url() url = f"{instance}/watch?v={vid}" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: soup = BeautifulSoup(r.text, "lxml") - hbox = soup.find("div", {"id":"player-container"}).find_next_sibling() - title = ''.join(hbox.find("h1").strings).strip() - channel_profile = soup.find("div", { "class": "channel-profile" }) + hbox = soup.find("div", {"id": "player-container"}).find_next_sibling() + title = "".join(hbox.find("h1").strings).strip() + channel_profile = soup.find("div", {"class": "channel-profile"}) channel_id = channel_profile.parent["href"].split("/")[-1] - channel_name = ''.join(soup.find("span", {"id": "channel-name"}).strings).strip() - desc_elem = soup.find("div", { "id": "descriptionWrapper" }) - desc = ''.join(desc_elem.strings).strip() - thumb = soup.find("video", { "id": "player" })["poster"] + channel_name = "".join( + soup.find("span", {"id": "channel-name"}).strings + ).strip() + desc_elem = soup.find("div", {"id": "descriptionWrapper"}) + desc = "".join(desc_elem.strings).strip() + thumb = soup.find("video", {"id": "player"})["poster"] return Video( - self.id, url, - vid, - title, - (channel_name, channel_id), - desc, - thumb) - def get_video_streams(self, vid:str): + self.id, url, vid, title, (channel_name, channel_id), desc, thumb + ) + + def get_video_streams(self, vid: str): instance = self.get_external_url() url = f"{instance}/watch?v={vid}" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: soup = BeautifulSoup(r.text, "lxml") - video = soup.find("video", { "id": "player" }) + video = soup.find("video", {"id": "player"}) results = [] for src in video.find_all("source"): try: @@ -342,6 +365,4 @@ class Invidious(Server): return [] def get_import_providers(self): - return [ - NewpipeInvidiousImporter(self.get_external_url()) - ] + return [NewpipeInvidiousImporter(self.get_external_url())] diff --git a/melon/servers/loader.py b/melon/servers/loader.py index 1f3afb1..a0e6026 100644 --- a/melon/servers/loader.py +++ b/melon/servers/loader.py @@ -12,7 +12,7 @@ import types class ServerFinder(importlib.abc.MetaPathFinder): - _PREFIX = 'melon.servers.' + _PREFIX = "melon.servers." def __init__(self, path=None): if isinstance(path, str): @@ -34,10 +34,10 @@ class ServerFinder(importlib.abc.MetaPathFinder): if not fullname.startswith(self._PREFIX): return None - name = fullname[len(self._PREFIX):] - base_dir = name.replace('.', '/') + name = fullname[len(self._PREFIX) :] + base_dir = name.replace(".", "/") for path in self._paths: - candidate_path = os.path.join(path, base_dir, '__init__.py') + candidate_path = os.path.join(path, base_dir, "__init__.py") if os.path.exists(candidate_path): return importlib.machinery.ModuleSpec( fullname, @@ -69,4 +69,4 @@ class ServerLoader(importlib.machinery.SourceFileLoader): return module -server_finder = ServerFinder(os.environ.get('MELON_SERVERS_PATH')) +server_finder = ServerFinder(os.environ.get("MELON_SERVERS_PATH")) diff --git a/melon/servers/nebula/__init__.py b/melon/servers/nebula/__init__.py index ea7d2fe..9c305dd 100644 --- a/melon/servers/nebula/__init__.py +++ b/melon/servers/nebula/__init__.py @@ -9,6 +9,7 @@ from melon.servers import Server, Preference, PreferenceType from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode from melon.servers import USER_AGENT + # NOTE: uses beautifulsoup instead of the invidious api # because not all invidious servers provide the api # and even if they do, most of the time it is ratelimited @@ -16,7 +17,9 @@ class Nebula(Server): id = "nebula" name = "Nebula" - description = _("Home of smart, thoughtful videos, podcasts, and classes from your favorite creators") + description = _( + "Home of smart, thoughtful videos, podcasts, and classes from your favorite creators" + ) settings = { "email": Preference( @@ -24,14 +27,17 @@ class Nebula(Server): _("Email Address"), _("Email Address to login to your account"), PreferenceType.TEXT, - "","" + "", + "", ), "password": Preference( "password", _("Password"), _("Password associated with your account"), PreferenceType.PASSWORD, - "",""), + "", + "", + ), } # nebula might contain 18+ material @@ -58,23 +64,25 @@ class Nebula(Server): feeds = [] for listing in data: if listing["title"] == "Latest Videos": - feeds.append(Feed( - listing["id"], - _(listing["title"]), - icon="dialog-information-symbolic") - .with_url(listing["view_all_url"])) + feeds.append( + Feed( + listing["id"], + _(listing["title"]), + icon="dialog-information-symbolic", + ).with_url(listing["view_all_url"]) + ) elif listing["title"] == "Nebula Originals": - feeds.append(Feed( - listing["id"], - _(listing["title"]), - icon="starred-symbolic" - ).with_url(listing["view_all_url"])) + feeds.append( + Feed( + listing["id"], _(listing["title"]), icon="starred-symbolic" + ).with_url(listing["view_all_url"]) + ) elif listing["title"] == "Nebula Plus": - feeds.append(Feed( - listing["id"], - _(listing["title"]), - icon="list-add-symbolic" - ).with_url(listing["view_all_url"])) + feeds.append( + Feed( + listing["id"], _(listing["title"]), icon="list-add-symbolic" + ).with_url(listing["view_all_url"]) + ) return feeds return [] @@ -88,7 +96,8 @@ class Nebula(Server): entry["id"], entry["title"], entry["description"], - entry["images"]["avatar"]["src"]) + entry["images"]["avatar"]["src"], + ) return channel elif type == "video_episode": # video type @@ -99,7 +108,8 @@ class Nebula(Server): entry["title"], (entry["channel_title"], entry["channel_id"]), entry["short_description"], - entry["images"]["thumbnail"]["src"]) + entry["images"]["thumbnail"]["src"], + ) return video elif type == "video_playlist": # playlist type @@ -111,10 +121,11 @@ class Nebula(Server): entry["title"], None, # nebula doesn't support playlist thumbnails - None) + None, + ) return playlist - def get_public_feed_content(self,id): + def get_public_feed_content(self, id): """Returns a list of videos in the given feed""" # as fas as I can tell nebula doesn't have a different target # to access specific featured rails @@ -135,7 +146,7 @@ class Nebula(Server): return [] def search(self, query, mask, page=0): - page = page+1 + page = page + 1 # nebula doesn't provide playlist search # which means that this feed will always be empty if mask == SearchMode.PLAYLISTS: @@ -151,7 +162,7 @@ class Nebula(Server): # than there are videos when looking for a channel if mask == SearchMode.ANY or mask == SearchMode.CHANNELS: # fetch channels - r = requests.get(channel_search, headers={ "User-Agent": USER_AGENT }) + r = requests.get(channel_search, headers={"User-Agent": USER_AGENT}) if r.ok: data = json.loads(r.text) for entry in data["results"]: @@ -160,7 +171,7 @@ class Nebula(Server): results.append(res) if mask == SearchMode.ANY or mask == SearchMode.VIDEOS: # fetch videos - r = requests.get(video_search, headers={ "User-Agent": USER_AGENT }) + r = requests.get(video_search, headers={"User-Agent": USER_AGENT}) if r.ok: data = json.loads(r.text) for entry in data["results"]: @@ -171,7 +182,7 @@ class Nebula(Server): def get_channel_info(self, channel_id: str): url = f"https://content.api.nebula.app/video_channels/{channel_id}/" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: data = json.loads(r.text) return Channel( @@ -180,20 +191,21 @@ class Nebula(Server): data["id"], data["title"], data["description"], - data["images"]["avatar"]["src"]) + data["images"]["avatar"]["src"], + ) - def get_channel_feeds(self, channel_id:str): + def get_channel_feeds(self, channel_id: str): url = f"https://content.api.nebula.app/video_channels/{channel_id}/" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) video_feed = Feed("videos", _("Videos")) if r.ok: data = json.loads(r.text) if (not "playlist" in data) or len(data["playlist"]) == 0: - return [ video_feed ] + return [video_feed] else: - return [ video_feed, Feed("playlists", _("Playlists")) ] + return [video_feed, Feed("playlists", _("Playlists"))] - def get_default_channel_feed(self, cid:str): + def get_default_channel_feed(self, cid: str): return "videos" def get_channel_feed_content(self, channel_id: str, feed_id: str, page=0): @@ -203,7 +215,7 @@ class Nebula(Server): # apparently this call doesn't support page numbers # and instead we have to use the provided data["next"] cursor url = f"https://content.api.nebula.app/video_channels/{channel_id}/video_episodes/" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: data = json.loads(r.text) for entry in data["results"]: @@ -215,7 +227,7 @@ class Nebula(Server): # NOTE: All playlists are included in this call # no pagination available url = f"https://content.api.nebula.app/video_channels/{channel_id}/" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: data = json.loads(r.text) for entry in data["playlists"]: @@ -226,25 +238,27 @@ class Nebula(Server): def get_playlist_info(self, playlist_id: str): url = f"https://content.api.nebula.app/video_playlists/{playlist_id}/" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: data = json.loads(r.text) return Playlist( self.id, # nebula doesn't have external playlist urls None, - playlist_id, data["title"], + playlist_id, + data["title"], # nebula doesn't support getting the channel # when accessing the playlist None, # nebula doesn't support playlist thumbnails - None) + None, + ) def get_playlist_content(self, playlist_id: str): # apparently this call doesn't support page numbers # and instead we have to use the provided data["next"] cursor url = f"https://content.api.nebula.app/video_playlists/{playlist_id}/video_episodes/" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) results = [] if r.ok: data = json.loads(r.text) @@ -255,7 +269,7 @@ class Nebula(Server): def get_timeline(self, channel_id: str): url = f"https://content.api.nebula.app/video_channels/{channel_id}/video_episodes/" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) results = [] if r.ok: data = json.loads(r.text) @@ -266,10 +280,9 @@ class Nebula(Server): results.append((vid, uts)) return results - def get_video_info(self, video_id: str): url = f"https://content.api.nebula.app/video_episodes/{ video_id }/" - r = requests.get(url, headers={ "User-Agent": USER_AGENT }) + r = requests.get(url, headers={"User-Agent": USER_AGENT}) if r.ok: data = json.loads(r.text) return Video( @@ -279,9 +292,10 @@ class Nebula(Server): data["title"], (data["channel_title"], data["channel_id"]), data["description"], - data["images"]["thumbnail"]["src"]) + data["images"]["thumbnail"]["src"], + ) - def get_video_streams(self, video_id:str): + def get_video_streams(self, video_id: str): # THIS ACTION REQUIRES LOGIN DETAILS email = self.settings["email"].value passwd = self.settings["password"].value @@ -290,14 +304,10 @@ class Nebula(Server): # NOTE: the first time the app runs this, # it will send out the Login Detected email level1_auth = f"https://nebula.tv/auth/login/" - level1_data = { - "email": email, - "password": passwd - } + level1_data = {"email": email, "password": passwd} r1 = requests.post( - level1_auth, - json = level1_data, - headers = { "Content-Type": "application/json" }) + level1_auth, json=level1_data, headers={"Content-Type": "application/json"} + ) if not r1.ok: return [] key1 = r1.json()["key"] @@ -307,10 +317,7 @@ class Nebula(Server): level2_auth = "https://users.api.nebula.app/api/v1/authorization/" r2 = requests.post( level2_auth, - headers = { - "User-Agent": USER_AGENT, - "Authorization": f"Token {key1}" - } + headers={"User-Agent": USER_AGENT, "Authorization": f"Token {key1}"}, ) if not r2.ok: return [] @@ -319,7 +326,7 @@ class Nebula(Server): # it is easier to obtain the stream manifest using the video slug # so we fetch the video info url and get the slug vid = f"https://content.api.nebula.app/video_episodes/{ video_id }/" - r_vid = requests.get(vid, headers={ "User-Agent": USER_AGENT }) + r_vid = requests.get(vid, headers={"User-Agent": USER_AGENT}) if not r_vid.ok: return [] slug = r_vid.json()["slug"] diff --git a/melon/servers/peertube/__init__.py b/melon/servers/peertube/__init__.py index 9fc6a7c..9d1e702 100644 --- a/melon/servers/peertube/__init__.py +++ b/melon/servers/peertube/__init__.py @@ -1,5 +1,5 @@ import requests -from urllib.parse import urlparse,parse_qs +from urllib.parse import urlparse, parse_qs from datetime import datetime from gettext import gettext as _ @@ -8,6 +8,7 @@ from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode from melon.servers import USER_AGENT from melon.import_providers.newpipe import NewpipeImporter + class Peertube(Server): id = "peertube" @@ -18,27 +19,34 @@ class Peertube(Server): "instances": Preference( "instances", _("Instances"), - _("List of peertube instances, from which to fetch content. See https://joinpeertube.org/instances"), + _( + "List of peertube instances, from which to fetch content. See https://joinpeertube.org/instances" + ), PreferenceType.MULTI, ["https://tilvids.com/"], - ["https://tilvids.com/"]), + ["https://tilvids.com/"], + ), "nsfw": Preference( "nsfw", _("Show NSFW content"), _("Passes the nsfw filter to the peertube search API"), PreferenceType.TOGGLE, - False, False), + False, + False, + ), "federate": Preference( "federate", _("Enable Federation"), _("Returns content from federated instances instead of only local content"), PreferenceType.TOGGLE, - True, True) - } + True, + True, + ), + } # peertube instances may contain 18+ content # so we have to indicate that this may contain nsfw content - is_nsfw = True, + is_nsfw = (True,) def get_external_url(self): # because this plugin supports multiple instances, @@ -47,16 +55,14 @@ class Peertube(Server): return "https://joinpeertube.org/" def get_instance_list(self): - return [ inst.strip("/") for inst in self.settings["instances"].value ] + return [inst.strip("/") for inst in self.settings["instances"].value] def get_public_feeds(self): # because this plugin supports multiple instances # we cannot set n external URL # nor is it possible to show trending videos, # because they cannot be merged - return [ - Feed("latest", _("Latest"), "dialog-information-symbolic") - ] + return [Feed("latest", _("Latest"), "dialog-information-symbolic")] def get_query_config(self) -> str: local = not self.settings["federate"].value @@ -71,11 +77,11 @@ class Peertube(Server): nsfw = "false" return f"isLocal={local}&nsfw={nsfw}" - def get_public_feed_content(self,id): + def get_public_feed_content(self, id): if id != "latest": return [] conf = self.get_query_config() - videos:(Video,int) = [] + videos: (Video, int) = [] for instance in self.get_instance_list(): url = f"{instance}/feeds/videos.json?sort=-publishedAt&{conf}" r = requests.get(url) @@ -99,8 +105,8 @@ class Peertube(Server): uts = datetime.fromisoformat(date_str).timestamp() videos.append((v, uts)) - videos.sort(key=lambda x:x[1]) - return [ v[0] for v in videos ] + videos.sort(key=lambda x: x[1]) + return [v[0] for v in videos] def search(self, query, mask, page=1): vid_conf = self.get_query_config() @@ -118,15 +124,16 @@ class Peertube(Server): channel_id = f"https://{item_host}::{item_id}" avatar = None if "avatar" in item and not item["avatar"] is None: - path=item["avatar"]["path"] - avatar=f"{instance}{path}" + path = item["avatar"]["path"] + avatar = f"{instance}{path}" c = Channel( self.id, item["url"], channel_id, item["displayName"], item["description"] or "", - avatar) + avatar, + ) results.append(c) if mask == SearchMode.PLAYLISTS or mask == SearchMode.ANY: # playlist search @@ -148,7 +155,8 @@ class Peertube(Server): item_id, item["displayName"], (channel_name, channel_id), - thumb) + thumb, + ) results.append(p) if mask == SearchMode.VIDEOS or mask == SearchMode.ANY: # video search @@ -158,7 +166,7 @@ class Peertube(Server): for item in r.json()["data"]: video_host = instance video_slug = item["uuid"] - video_id=f"{video_host}::{video_slug}" + video_id = f"{video_host}::{video_slug}" thumb_path = item["thumbnailPath"] thumb = f"{instance}/{thumb_path}" channel_name = item["channel"]["displayName"] @@ -172,7 +180,8 @@ class Peertube(Server): item["name"], (channel_name, channel_id), item["description"] or "", - thumb) + thumb, + ) results.append(v) return results @@ -195,20 +204,16 @@ class Peertube(Server): channel_id, data["displayName"], data["description"], - avatar) + avatar, + ) - def get_channel_feeds(self, cid:str) : + def get_channel_feeds(self, cid: str): res = self.get_channel_feed_content(cid, "playlists") if len(res) != 0: - return [ - Feed("videos", _("Videos")), - Feed("playlists", _("Playlists")) - ] - return [ - Feed("videos", _("Videos")) - ] + return [Feed("videos", _("Videos")), Feed("playlists", _("Playlists"))] + return [Feed("videos", _("Videos"))] - def get_default_channel_feed(self, cid:str): + def get_default_channel_feed(self, cid: str): return "videos" def get_channel_feed_content(self, cid: str, feed_id: str, page=1): @@ -221,7 +226,7 @@ class Peertube(Server): for item in r.json()["data"]: video_host = instance video_slug = item["uuid"] - video_id=f"{video_host}::{video_slug}" + video_id = f"{video_host}::{video_slug}" thumb_path = item["thumbnailPath"] thumb = f"{instance}{thumb_path}" channel_name = item["channel"]["displayName"] @@ -235,7 +240,8 @@ class Peertube(Server): item["name"], (channel_name, channel_id), item["description"], - thumb) + thumb, + ) results.append(v) elif feed_id == "playlists": url = f"{instance}/api/v1/video-channels/{channel_id}/video-playlists" @@ -255,12 +261,12 @@ class Peertube(Server): item_id, item["displayName"], (channel_name, channel_id), - thumb) + thumb, + ) results.append(p) return results - def get_playlist_info(self, pid: str): instance, playlist_id = pid.split("::") url = f"{instance}/api/v1/video-playlists/{playlist_id}" @@ -281,7 +287,8 @@ class Peertube(Server): item_id, data["displayName"], (channel_name, channel_id), - thumb) + thumb, + ) def get_playlist_content(self, pid: str): instance, playlist_id = pid.split("::") @@ -294,7 +301,7 @@ class Peertube(Server): item = item["video"] video_host = instance video_slug = item["uuid"] - video_id=f"{video_host}::{video_slug}" + video_id = f"{video_host}::{video_slug}" thumb_path = item["thumbnailPath"] thumb = f"{instance}{thumb_path}" channel_name = item["channel"]["displayName"] @@ -315,7 +322,8 @@ class Peertube(Server): item["name"], (channel_name, channel_id), item["description"], - thumb) + thumb, + ) results.append(v) return results @@ -328,7 +336,7 @@ class Peertube(Server): for item in r.json()["data"]: video_host = instance video_slug = item["uuid"] - video_id=f"{video_host}::{video_slug}" + video_id = f"{video_host}::{video_slug}" thumb_path = item["thumbnailPath"] thumb = f"{instance}{thumb_path}" channel_name = item["channel"]["displayName"] @@ -342,7 +350,8 @@ class Peertube(Server): item["name"], (channel_name, channel_id), item["description"], - thumb) + thumb, + ) uts = datetime.fromisoformat(item["updatedAt"]).timestamp() results.append((v, uts)) return results @@ -355,7 +364,7 @@ class Peertube(Server): data = r.json() video_host = instance video_slug = data["uuid"] - video_id=f"{video_host}::{video_slug}" + video_id = f"{video_host}::{video_slug}" channel_name = data["channel"]["displayName"] channel_slug = data["channel"]["name"] channel_host = data["channel"]["host"] @@ -369,9 +378,10 @@ class Peertube(Server): data["name"], (channel_name, channel_id), data["description"], - thumb) + thumb, + ) - def get_video_streams(self, vid:str): + def get_video_streams(self, vid: str): instance, video_id = vid.split("::") url = f"{instance}/api/v1/videos/{video_id}" r = requests.get(url) diff --git a/melon/servers/utils.py b/melon/servers/utils.py index 55f25c3..0c30313 100644 --- a/melon/servers/utils.py +++ b/melon/servers/utils.py @@ -10,11 +10,17 @@ import requests import urllib from datetime import datetime from melon.servers.loader import server_finder -from melon.models import get_server_settings, get_subscribed_channels, get_app_settings, load_server +from melon.models import ( + get_server_settings, + get_subscribed_channels, + get_app_settings, + load_server, +) from melon.servers import Server from melon.servers import Channel, Playlist, Resource -def pixbuf_from_url(url:str) -> GdkPixbuf.Pixbuf: + +def pixbuf_from_url(url: str) -> GdkPixbuf.Pixbuf: """ Fetches the image and constructs a Pixbuf using the PixbufLoader @@ -36,31 +42,37 @@ def pixbuf_from_url(url:str) -> GdkPixbuf.Pixbuf: except: return None + def get_server_instance(server) -> Server: """ Returns a constructed class of the server """ - instance = getattr(server['module'], server['class_name'])() + instance = getattr(server["module"], server["class_name"])() # get server settings and update the values in here for key, dt in get_server_settings(server["id"])["custom"].items(): if key in instance.settings: instance.settings[key].value = dt return instance + import threading + + class NewsWorkerPool: - threads=[] - tasks=[] - results=[] + threads = [] + tasks = [] + results = [] + def __init__(self, size, tasks): - self.results=[] - self.threads=[] + self.results = [] + self.threads = [] self.tasks = tasks for i in range(size): thread = threading.Thread(target=self.run, name=str(i)) thread.daemon = True thread.start() self.threads.append(thread) + def run(self): name = threading.currentThread().getName() while self.tasks: @@ -70,10 +82,12 @@ class NewsWorkerPool: dts = instance.get_timeline(channel.id) for entry in dts: self.results.append(entry) + def wait(self): for thread in self.threads: thread.join() + def fetch_home_feed() -> list[Resource]: subs = get_subscribed_channels() # generate {server}:[channel] database @@ -82,7 +96,7 @@ def fetch_home_feed() -> list[Resource]: if sub.server in db: db[sub.server].append(sub) else: - db[sub.server] = [ sub ] + db[sub.server] = [sub] servers = get_allowed_servers_list(get_app_settings()) # generate list of tasks tasks = [] @@ -91,23 +105,27 @@ def fetch_home_feed() -> list[Resource]: continue instance = get_server_instance(server) channels = db[server["id"]] - tasks = tasks + [ (instance, channel) for channel in channels ] + tasks = tasks + [(instance, channel) for channel in channels] pool = NewsWorkerPool(6, tasks) pool.wait() feed = pool.results feed.sort(key=lambda dts: dts[1]) return feed + + def group_by_date(dataset): db = {} for entry in dataset: dt = entry[0] uts = entry[1] date = datetime.fromtimestamp(uts) - local_date_time = date.date().strftime("%c").replace("00:00:00","").replace(" ", " ") + local_date_time = ( + date.date().strftime("%c").replace("00:00:00", "").replace(" ", " ") + ) if local_date_time in db: db[local_date_time].append(dt) else: - db[local_date_time] = [ dt ] + db[local_date_time] = [dt] return db @@ -117,21 +135,31 @@ def server_is_allowed(server_id, app_settings): if server_settings["enabled"] is False: return False # skip servers that may contain nsfw content (if user disabled them) - if app_settings["nsfw_content"] is False and server_settings['nsfw_content']: + if app_settings["nsfw_content"] is False and server_settings["nsfw_content"]: return False # skip servers that contain only nsfw content (if user disable them) - if app_settings["nsfw_only_content"] is False and server_settings['nsfw_only_content']: + if ( + app_settings["nsfw_only_content"] is False + and server_settings["nsfw_only_content"] + ): return False # skip servers that require login if the user disabled them - if app_settings["login_required"] is False and server_settings['login_required']: + if app_settings["login_required"] is False and server_settings["login_required"]: return False return True -def filter_resources(reslist:list[Resource], app_settings, access=None) ->list[Resource]: + +def filter_resources( + reslist: list[Resource], app_settings, access=None +) -> list[Resource]: if access is None: - return [ res for res in reslist if server_is_allowed(res.server, app_settings) ] + return [res for res in reslist if server_is_allowed(res.server, app_settings)] else: - return [ res for res in reslist if server_is_allowed(access(res).server, app_settings) ] + return [ + res + for res in reslist + if server_is_allowed(access(res).server, app_settings) + ] # mostly taken from https://codeberg.org/valos/Komikku/src/commit/a8a5adfc814cf049241119a92fccddcd34efc6db/komikku/servers/utils.py @@ -140,6 +168,7 @@ def filter_resources(reslist:list[Resource], app_settings, access=None) ->list[R # SPDX-License-Identifier: GPL-3.0-only or GPL-3.0-or-later # Author: Valéry Febvre + def get_allowed_servers_list(settings): servers = [] for id, server_data in get_servers_list().items(): @@ -167,32 +196,33 @@ def get_server_class_name_by_id(id): - `module_name` is the name of the module in which the server is defined (optional). Only useful if `module_name` is different from `name`. """ - return id.split(':')[0].capitalize() + return id.split(":")[0].capitalize() def get_server_dir_name_by_id(id): - name = id.split(':')[0] + name = id.split(":")[0] # Remove _whatever - name = '_'.join(filter(None, name.split('_')[:2])) + name = "_".join(filter(None, name.split("_")[:2])) return name def get_server_main_id_by_id(id): - return id.split(':')[0].split('_')[0] + return id.split(":")[0].split("_")[0] def get_server_module_name_by_id(id): - return id.split(':')[-1].split('_')[0] + return id.split(":")[-1].split("_")[0] + @cache -def get_servers_list(include_disabled=False, order_by='name'): +def get_servers_list(include_disabled=False, order_by="name"): def iter_namespace(ns_pkg): # Specifying the second argument (prefix) to iter_modules makes the # returned name an absolute name instead of a relative one. This allows # import_module to work without having to do additional modification to # the name. - return iter_modules(ns_pkg.__path__, ns_pkg.__name__ + '.') + return iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".") modules = [] if server_finder in sys.meta_path: @@ -203,15 +233,17 @@ def get_servers_list(include_disabled=False, order_by='name'): count = 0 for path, _dirs, _files in os.walk(servers_path): - relpath = path[len(servers_path):] + relpath = path[len(servers_path) :] if not relpath: continue - relname = relpath.replace(os.path.sep, '.') - if relname == '.multi': + relname = relpath.replace(os.path.sep, ".") + if relname == ".multi": continue - modules.append(importlib.import_module(relname, package='melon.servers')) + modules.append( + importlib.import_module(relname, package="melon.servers") + ) count += 1 else: # fallback to local exploration @@ -223,13 +255,16 @@ def get_servers_list(include_disabled=False, order_by='name'): servers = {} for module in modules: for _name, obj in dict(inspect.getmembers(module)).items(): - if not hasattr(obj, 'id') or not hasattr(obj, 'name'): + if not hasattr(obj, "id") or not hasattr(obj, "name"): continue if NotImplemented in (obj.id, obj.name): continue if inspect.isclass(obj): - logo_path = os.path.join(os.path.dirname(os.path.abspath(module.__file__)), get_server_main_id_by_id(obj.id) + '.png') + logo_path = os.path.join( + os.path.dirname(os.path.abspath(module.__file__)), + get_server_main_id_by_id(obj.id) + ".png", + ) servers[obj.id] = dict( id=obj.id, diff --git a/melon/settings/__init__.py b/melon/settings/__init__.py index a999162..c24e817 100644 --- a/melon/settings/__init__.py +++ b/melon/settings/__init__.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw from gettext import gettext as _ @@ -9,46 +10,68 @@ from melon.servers.utils import get_servers_list, get_server_instance from melon.servers import Preference, PreferenceType from melon.widgets.preferencerow import PreferenceRow from melon.models import get_app_settings, set_app_setting, set_server_setting -from melon.models import is_server_enabled, ensure_server_disabled, ensure_server_enabled +from melon.models import ( + is_server_enabled, + ensure_server_disabled, + ensure_server_enabled, +) global_prefs = { - # True if images should be shown when searching or browsing public feeds - "show_images_in_browse": Preference( - "show_images_in_browse", - _("Show Previews when browsing public feeds (incl. search)"), - _("Set to true to show previews when viewing channel contents, public feeds and searching"), - PreferenceType.TOGGLE, - True, True), - # True if images should be shown when browsing channels/playlists/home feed - "show_images_in_feed": Preference( - "show_images_in_feed", - _("Show Previews in local feeds"), - _("Set to true to show previews in the new feed, for subscribed channels, and saved playlists"), - PreferenceType.TOGGLE, - True, True), - - "nsfw_content": Preference( - "nsfw_content", - _("Show servers that may contain nsfw content"), - _("Lists/Delists servers in the browse servers list, if they contain some nsfw content"), - PreferenceType.TOGGLE, - True, True), - "nsfw_only_content": Preference( - "nsfw_only_content", - _("Show servers that only contain nsfw content"), - _("Lists/Delists servers in the browse servers list, if they contain only/mostly nsfw content"), - PreferenceType.TOGGLE, - True, True + # True if images should be shown when searching or browsing public feeds + "show_images_in_browse": Preference( + "show_images_in_browse", + _("Show Previews when browsing public feeds (incl. search)"), + _( + "Set to true to show previews when viewing channel contents, public feeds and searching" + ), + PreferenceType.TOGGLE, + True, + True, + ), + # True if images should be shown when browsing channels/playlists/home feed + "show_images_in_feed": Preference( + "show_images_in_feed", + _("Show Previews in local feeds"), + _( + "Set to true to show previews in the new feed, for subscribed channels, and saved playlists" + ), + PreferenceType.TOGGLE, + True, + True, + ), + "nsfw_content": Preference( + "nsfw_content", + _("Show servers that may contain nsfw content"), + _( + "Lists/Delists servers in the browse servers list, if they contain some nsfw content" + ), + PreferenceType.TOGGLE, + True, + True, + ), + "nsfw_only_content": Preference( + "nsfw_only_content", + _("Show servers that only contain nsfw content"), + _( + "Lists/Delists servers in the browse servers list, if they contain only/mostly nsfw content" ), - "login_required": Preference( - "login_required", - _("Show servers that require login"), - _("Lists/Delists servers in the browse servers list, if they require login to function"), - PreferenceType.TOGGLE, - True, True - ) + PreferenceType.TOGGLE, + True, + True, + ), + "login_required": Preference( + "login_required", + _("Show servers that require login"), + _( + "Lists/Delists servers in the browse servers list, if they require login to function" + ), + PreferenceType.TOGGLE, + True, + True, + ), } + class SettingsScreen(Adw.NavigationPage): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -76,9 +99,9 @@ class SettingsScreen(Adw.NavigationPage): # add callback when value changed # HACK: pass contextual arguments from for loop to helper function # fixes problem where pref_id would always have value of last element - row.set_callback(pass_me( - lambda val, other: set_app_setting(other[0], val), - pref_id)) + row.set_callback( + pass_me(lambda val, other: set_app_setting(other[0], val), pref_id) + ) self.general.add(row.get_widget()) self.box.add(self.general) @@ -91,14 +114,15 @@ class SettingsScreen(Adw.NavigationPage): epref = Preference( "server-enabled", _("Enable Server"), - _("Disabled servers won't show up in the browser or on the local/home screen"), + _( + "Disabled servers won't show up in the browser or on the local/home screen" + ), PreferenceType.TOGGLE, - True, is_server_enabled(server_id)) + True, + is_server_enabled(server_id), + ) er = PreferenceRow(epref) - er.set_callback(pass_me( - self.toggle_server, - server_id - )) + er.set_callback(pass_me(self.toggle_server, server_id)) group.add(er.get_widget()) for pref_id, pref in instance.settings.items(): # parse preference type and show fitting row @@ -108,10 +132,13 @@ class SettingsScreen(Adw.NavigationPage): # add callback when value changed # HACK: pass contextual arguments from for loop to helper function # fixes problem where pref_id and server_id would always have value of last element - row.set_callback(pass_me( - lambda val, other: set_server_setting(other[0], other[1], val), - server_id, pref_id - )) + row.set_callback( + pass_me( + lambda val, other: set_server_setting(other[0], other[1], val), + server_id, + pref_id, + ) + ) group.add(row.get_widget()) self.box.add(group) @@ -119,6 +146,7 @@ class SettingsScreen(Adw.NavigationPage): self.wrapper.set_child(self.scrollview) self.toolbar_view.set_content(self.wrapper) self.set_child(self.toolbar_view) + def toggle_server(self, value, other): server_id = other[0] if value: @@ -126,5 +154,6 @@ class SettingsScreen(Adw.NavigationPage): else: ensure_server_disabled(server_id) + def pass_me(func, *args): return lambda y: func(y, args) diff --git a/melon/utils.py b/melon/utils.py index 70dba15..a61b9ca 100644 --- a/melon/utils.py +++ b/melon/utils.py @@ -1,11 +1,14 @@ import gi -gi.require_version('Gdk', '4.0') -from gi.repository import Gio,GLib + +gi.require_version("Gdk", "4.0") +from gi.repository import Gio, GLib from functools import cache import os + def is_flatpak(): - return os.path.exists(os.path.join(GLib.get_user_runtime_dir(), 'flatpak-info')) + return os.path.exists(os.path.join(GLib.get_user_runtime_dir(), "flatpak-info")) + @cache def get_data_dir(): @@ -13,18 +16,21 @@ def get_data_dir(): if not is_flatpak(): base_path = data_dir_path - data_dir_path = os.path.join(base_path, 'melon') + data_dir_path = os.path.join(base_path, "melon") if not os.path.exists(data_dir_path): 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 diff --git a/melon/widgets/feeditem.py b/melon/widgets/feeditem.py index ddeba42..09457a8 100644 --- a/melon/widgets/feeditem.py +++ b/melon/widgets/feeditem.py @@ -1,23 +1,34 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib, Gdk -from melon.servers import Resource,Video,Playlist,Channel +from melon.servers import Resource, Video, Playlist, Channel from melon.servers.utils import pixbuf_from_url from melon.widgets.iconbutton import IconButton -from melon.models import PlaylistWrapperType,PlaylistWrapper +from melon.models import PlaylistWrapperType, PlaylistWrapper import threading from melon.background import queue from unidecode import unidecode from gettext import gettext as _ from melon.utils import pass_me, many + class AdaptiveFeedItem(Adw.ActionRow): """ FeedItem with automatic adjustment to resource type """ - def __init__(self, resource:Resource, show_preview=True, clickable=True, onClick=None, *args, **kwargs): + + def __init__( + self, + resource: Resource, + show_preview=True, + clickable=True, + onClick=None, + *args, + **kwargs + ): super().__init__(*args, **kwargs) self.res = resource self.show_preview = show_preview @@ -25,9 +36,11 @@ class AdaptiveFeedItem(Adw.ActionRow): self.onClick = onClick self.menuitems = [] self.update() + def set_resource(self, resource: Resource): self.res = resource self.update() + def update(self): if self.res is None: return @@ -37,12 +50,13 @@ class AdaptiveFeedItem(Adw.ActionRow): self.set_activatable(clickable) if self.onClick is None: self.set_action_target_value( - GLib.Variant("as", [resource.server, resource.id])) + GLib.Variant("as", [resource.server, resource.id]) + ) thumb_url = None if isinstance(resource, Video): thumb_url = resource.thumbnail - self.set_title(unidecode(resource.title).replace("&","&")) - self.set_subtitle(unidecode(resource.channel[0]).replace("&","&")) + self.set_title(unidecode(resource.title).replace("&", "&")) + self.set_subtitle(unidecode(resource.channel[0]).replace("&", "&")) if self.onClick is None: self.set_action_name("win.player") # only videos support rightclickt/long press @@ -51,33 +65,35 @@ class AdaptiveFeedItem(Adw.ActionRow): # set the click gesture handler to only listen for right clicks click_ctr.set_button(3) self.add_controller(click_ctr) - click_ctr.connect("pressed", lambda e,n,x,y: self._show_video_bottom_sheet()) + click_ctr.connect( + "pressed", lambda e, n, x, y: self._show_video_bottom_sheet() + ) long_ctr = Gtk.GestureLongPress() self.add_controller(long_ctr) - long_ctr.connect("pressed", lambda e,x,y: self._show_video_bottom_sheet()) + long_ctr.connect("pressed", lambda e, x, y: self._show_video_bottom_sheet()) elif isinstance(resource, Playlist): thumb_url = resource.thumbnail - self.set_title(unidecode(resource.title).replace("&","&")) + self.set_title(unidecode(resource.title).replace("&", "&")) pad = "" if len(resource.description) > 80: pad = "..." # NOTE: this might be a bad idea # because it could possibly cut the unicode in half? I think? sub = unidecode(resource.description)[:80] + pad - self.set_subtitle(sub.replace("&","&")) + self.set_subtitle(sub.replace("&", "&")) if self.onClick is None: self.set_action_name("win.browse_playlist") elif isinstance(resource, Channel): thumb_url = resource.avatar - self.set_title(unidecode(resource.name).replace("&","&")) + self.set_title(unidecode(resource.name).replace("&", "&")) pad = "" if len(resource.bio) > 80: pad = "..." # NOTE: this might be a bad idea # because it could possibly cut the unicode in half? I think? sub = unidecode(resource.bio)[:80] + pad - self.set_subtitle(sub.replace("&","&")) + self.set_subtitle(sub.replace("&", "&")) if self.onClick is None: self.set_action_name("win.browse_channel") @@ -97,6 +113,7 @@ class AdaptiveFeedItem(Adw.ActionRow): if not pixbuf is None: texture = Gdk.Texture.new_for_pixbuf(pixbuf) GLib.idle_add(self.complete_avatar, texture) + def complete_avatar(self, texture): self.preview.set_custom_image(texture) # return false to cancel the idle job @@ -111,17 +128,37 @@ class AdaptiveFeedItem(Adw.ActionRow): tbv = Adw.ToolbarView() hb = Adw.HeaderBar() tbv.add_top_bar(hb) - diag.set_child(tbv); + diag.set_child(tbv) page = Adw.PreferencesPage() group = Adw.PreferencesGroup() group.set_title(self.res.title) group.set_description(self.res.channel[0]) # add predefined options to list predefined = [ - (_("Watch now"), lambda: self.activate_action("win.player", GLib.Variant("as", [self.res.server, self.res.id]))), - (_("Add to playlist"), lambda: self.activate_action("win.add_to_playlist", GLib.Variant("as", [self.res.server, self.res.id]))), - (_("Open in browser"), lambda: Gtk.UriLauncher.new(uri=self.res.url).launch()), - (_("View channel"), lambda: self.activate_action("win.browse_channel", GLib.Variant("as", [self.res.server, self.res.channel[1]]))) + ( + _("Watch now"), + lambda: self.activate_action( + "win.player", GLib.Variant("as", [self.res.server, self.res.id]) + ), + ), + ( + _("Add to playlist"), + lambda: self.activate_action( + "win.add_to_playlist", + GLib.Variant("as", [self.res.server, self.res.id]), + ), + ), + ( + _("Open in browser"), + lambda: Gtk.UriLauncher.new(uri=self.res.url).launch(), + ), + ( + _("View channel"), + lambda: self.activate_action( + "win.browse_channel", + GLib.Variant("as", [self.res.server, self.res.channel[1]]), + ), + ), ] # add predefined list before custom menu entries and add them for dt in predefined + self.menuitems: @@ -129,29 +166,34 @@ class AdaptiveFeedItem(Adw.ActionRow): row.set_title(dt[0]) row.set_activatable(True) # run callback & close dialog on click - row.connect("activated", pass_me(lambda _, cb: many( - cb(), - diag.close() - ), dt[1])) + row.connect( + "activated", pass_me(lambda _, cb: many(cb(), diag.close()), dt[1]) + ) group.add(row) page.add(group) tbv.set_content(page) + class AdaptivePlaylistFeedItem(Adw.ActionRow): - def __init__(self, playlist:PlaylistWrapper, show_preview=True, onClick=None, *args, **kwargs): + def __init__( + self, + playlist: PlaylistWrapper, + show_preview=True, + onClick=None, + *args, + **kwargs + ): super().__init__(*args, **kwargs) - self.set_title(unidecode(playlist.inner.title).replace("&","&")) - self.set_subtitle(unidecode(playlist.inner.description).replace("&","&")) + self.set_title(unidecode(playlist.inner.title).replace("&", "&")) + self.set_subtitle(unidecode(playlist.inner.description).replace("&", "&")) if not onClick is None: # use custom click callback - self.connect( - "activated", - onClick - ) + self.connect("activated", onClick) elif playlist.type == PlaylistWrapperType.EXTERNAL: self.set_action_name("win.browse_playlist") self.set_action_target_value( - GLib.Variant("as", [playlist.inner.server, playlist.inner.id])) + GLib.Variant("as", [playlist.inner.server, playlist.inner.id]) + ) else: self.set_action_name("win.view_local_playlist") self.set_action_target_value(GLib.Variant("u", playlist.inner.id)) @@ -169,6 +211,7 @@ class AdaptivePlaylistFeedItem(Adw.ActionRow): if not pixbuf is None: texture = Gdk.Texture.new_for_pixbuf(pixbuf) GLib.idle_add(self.complete_avatar, texture) + def complete_avatar(self, texture): self.preview.set_custom_image(texture) # return false to cancel the idle job diff --git a/melon/widgets/filterbutton.py b/melon/widgets/filterbutton.py index f2f603e..2b30b0b 100644 --- a/melon/widgets/filterbutton.py +++ b/melon/widgets/filterbutton.py @@ -1,7 +1,9 @@ import gi -gi.require_version('Gtk', '4.0') + +gi.require_version("Gtk", "4.0") from gi.repository import Gtk + class FilterButton(Gtk.CheckButton): def __init__(self, name, state, cb, current, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/melon/widgets/iconbutton.py b/melon/widgets/iconbutton.py index f0b4b12..7068aa0 100644 --- a/melon/widgets/iconbutton.py +++ b/melon/widgets/iconbutton.py @@ -1,9 +1,11 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw + class IconButton(Gtk.Button): def __init__(self, name, icon, tooltip=None, *args, **kwargs): super().__init__(*args, **kwargs) @@ -12,12 +14,16 @@ class IconButton(Gtk.Button): self.set_can_shrink(True) self.set_child(self.inner) self.update(name, icon, tooltip) + def set_icon(self, icon): self.inner.set_icon_name(icon) + def set_name(self, name): self.inner.set_label(name) + def set_tooltip(self, tooltip): self.set_tooltip_text(tooltip) + def update(self, name, icon, tooltip=None): self.set_name(name) self.set_icon(icon) diff --git a/melon/widgets/player.py b/melon/widgets/player.py index ea68170..1305336 100644 --- a/melon/widgets/player.py +++ b/melon/widgets/player.py @@ -1,8 +1,9 @@ import gi + gi.require_version("WebKit", "6.0") -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') -gi.require_version('Gst', '1.0') +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") +gi.require_version("Gst", "1.0") from gi.repository import Gtk, Adw, WebKit, GLib, Gst, Gio from unidecode import unidecode from gettext import gettext as _ @@ -14,7 +15,8 @@ from melon.widgets.iconbutton import IconButton from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference from melon.utils import pass_me -def format_seconds(secs:float) -> str: + +def format_seconds(secs: float) -> str: secs = int(secs) seconds = secs % 60 mins_hours = secs // 60 @@ -27,9 +29,11 @@ def format_seconds(secs:float) -> str: s = f"{seconds:02}" return f"{h}{m}:{s}" + def clamp(lower, value, upper): return max(lower, min(value, upper)) + class OverlayDispay(Gtk.Box): def __init__(self, icon, text, *args, **kwargs): super().__init__(*args, **kwargs) @@ -44,45 +48,56 @@ class OverlayDispay(Gtk.Box): self.set_icon(icon) self.set_text(text) self.show() + def show(self): self.set_opacity(1.0) + def hide(self): self.set_opacity(0.0) - def set_icon(self, icon:str): + + def set_icon(self, icon: str): self._icon.set_from_icon_name(icon) - def set_text(self, text:str): + + def set_text(self, text: str): self._label.set_label(text) + class BrightnessDisplay(OverlayDispay): - def __init__(self, value:float, *args, **kwargs): + def __init__(self, value: float, *args, **kwargs): initial_icon = self.get_icon_for(value) initial_text = self.get_text_for(value) super().__init__(initial_icon, initial_text, *args, **kwargs) - def update(self, value:float): + + def update(self, value: float): icon = self.get_icon_for(value) text = self.get_text_for(value) self.set_icon(icon) self.set_text(text) - def get_icon_for(self, value:float) -> str: + + def get_icon_for(self, value: float) -> str: if value < 0.4: return "night-light-symbolic" if value < 0.8: return "weather-clear-symbolic" return "display-brightness-symbolic" - def get_text_for(self, value:float) -> str: + + def get_text_for(self, value: float) -> str: return f"{int(value * 100)}%" + class VolumeDisplay(OverlayDispay): - def __init__(self, value:float, *args, **kwargs): + def __init__(self, value: float, *args, **kwargs): initial_icon = self.get_icon_for(value) initial_text = self.get_text_for(value) super().__init__(initial_icon, initial_text, *args, **kwargs) - def update(self, value:float): + + def update(self, value: float): icon = self.get_icon_for(value) text = self.get_text_for(value) self.set_icon(icon) self.set_text(text) - def get_icon_for(self, value:float) -> str: + + def get_icon_for(self, value: float) -> str: if value == 0.0: return "audio-volume-muted-symbolic" if value < 0.4: @@ -90,9 +105,11 @@ class VolumeDisplay(OverlayDispay): if value < 0.8: return "audio-volume-medium-symbolic" return "audio-volume-high-symbolic" - def get_text_for(self, value:float) -> str: + + def get_text_for(self, value: float) -> str: return f"{int(value * 100)}%" + class VideoPlayerBase(Gtk.Overlay): volume = 1.0 # inverse opacity of overlay @@ -106,8 +123,8 @@ class VideoPlayerBase(Gtk.Overlay): self.duration = None self.paused = True self.stopped = True - self.update_callback=None - self.ended_callback=None + self.update_callback = None + self.ended_callback = None self.toggle_fullscreen = None self.toggle_popout = None @@ -177,11 +194,15 @@ class VideoPlayerBase(Gtk.Overlay): self.win_controls.set_halign(Gtk.Align.END) self.win_controls.set_valign(Gtk.Align.START) - self.popout_ctr = IconButton("", "view-paged-symbolic", tooltip=_("Toggle floating window")) + self.popout_ctr = IconButton( + "", "view-paged-symbolic", tooltip=_("Toggle floating window") + ) self.popout_ctr.connect("clicked", lambda _: self._toggle_popout()) self.win_controls.append(self.popout_ctr) - self.fullscreen_ctr = IconButton("", "view-fullscreen-symbolic", tooltip=_("Toggle fullscreen")) + self.fullscreen_ctr = IconButton( + "", "view-fullscreen-symbolic", tooltip=_("Toggle fullscreen") + ) self.fullscreen_ctr.connect("clicked", lambda _: self._toggle_fullscreen()) self.win_controls.append(self.fullscreen_ctr) @@ -201,7 +222,9 @@ class VideoPlayerBase(Gtk.Overlay): self.controls.set_valign(Gtk.Align.END) # add play/pause indicator - self.playpause_display = IconButton("", "media-playback-start-symbolic", tooltip=_("Play")) + self.playpause_display = IconButton( + "", "media-playback-start-symbolic", tooltip=_("Play") + ) self.playpause_display.connect("clicked", self._toggle_playpause) self.controls.append(self.playpause_display) @@ -241,18 +264,25 @@ class VideoPlayerBase(Gtk.Overlay): # volume control slider # (as an alternative to the slide gesture) # mapped to 0-100 scale - self.volume_ctr = Gtk.ScaleButton.new(0, 100, 2, [ - # The first item in the array will be used in the button when the current value is the lowest value, - "audio-volume-muted-symbolic", - # the second item for the highest value - "audio-volume-high-symbolic", - # All the subsequent icons will be used for all the other values, spread evenly over the range of values. - "audio-volume-low-symbolic", - "audio-volume-medium-symbolic", - ]) + self.volume_ctr = Gtk.ScaleButton.new( + 0, + 100, + 2, + [ + # The first item in the array will be used in the button when the current value is the lowest value, + "audio-volume-muted-symbolic", + # the second item for the highest value + "audio-volume-high-symbolic", + # All the subsequent icons will be used for all the other values, spread evenly over the range of values. + "audio-volume-low-symbolic", + "audio-volume-medium-symbolic", + ], + ) # self.volume uses the [0,1] interval, but the slider uses [0,100] self.volume_ctr.set_value(self.volume * 100) - self.volume_ctr.connect("value-changed", lambda w, val: self.change_volume(val/100)) + self.volume_ctr.connect( + "value-changed", lambda w, val: self.change_volume(val / 100) + ) self.volume_ctr.set_direction(Gtk.ArrowType.UP) self.volume_ctr.get_popup().set_position(Gtk.PositionType.TOP) self.controls.append(self.volume_ctr) @@ -279,7 +309,7 @@ class VideoPlayerBase(Gtk.Overlay): # click handler self.click_ctr = Gtk.GestureClick() self.add_controller(self.click_ctr) - self.click_ctr.connect("pressed", lambda e,n, x,y :self._onclick(n, x,y)) + self.click_ctr.connect("pressed", lambda e, n, x, y: self._onclick(n, x, y)) # initialize volume & brightness self.change_brightness(self.brightness) @@ -290,17 +320,19 @@ class VideoPlayerBase(Gtk.Overlay): def connect_update(self, callback=None): self.update_callback = callback + def connect_ended(self, callback=None): self.ended_callback = callback def _toggle_fullscreen(self): if not self.toggle_fullscreen is None: state = self.toggle_fullscreen() + def _toggle_popout(self): if not self.toggle_popout is None: state = self.toggle_popout() - def set_toggle_states(self, popout:bool, fullscreen:bool): + def set_toggle_states(self, popout: bool, fullscreen: bool): if popout: # window now popped out self.popout_ctr.set_icon("view-dual-symbolic") @@ -314,12 +346,14 @@ class VideoPlayerBase(Gtk.Overlay): def connect_toggle_fullscreen(self, callback=None): self.toggle_fullscreen = callback + def connect_toggle_popout(self, callback=None): self.toggle_popout = callback def _show_controls(self): self.controls.set_opacity(1) self.win_controls.set_opacity(1) + def _hide_controls(self, force=False): if self.hover_ctr.get_property("contains-pointer") and not force: # mouse is still hovering on player @@ -327,15 +361,16 @@ class VideoPlayerBase(Gtk.Overlay): return True self.controls.set_opacity(0) self.win_controls.set_opacity(0) + def _schedule_hide_controls(self, timeout=2): - GLib.timeout_add(timeout*1000, self._hide_controls) + GLib.timeout_add(timeout * 1000, self._hide_controls) def _update_swipe(self, e, s): bounds = self.swipe_ctr.get_bounding_box_center() if not bounds[0]: return width = self.picture.get_width() - left_bound = width*(2/5) + left_bound = width * (2 / 5) right_bound = width - left_bound vel = self.swipe_ctr.get_velocity() @@ -343,7 +378,7 @@ class VideoPlayerBase(Gtk.Overlay): return # randomly selected values to make it feel responsive - fact = vel[2]/10000 + fact = vel[2] / 10000 fact = clamp(-0.2, fact, 0.2) if abs(fact) < 0.01: # little to no movement @@ -390,12 +425,14 @@ class VideoPlayerBase(Gtk.Overlay): def _mouse_enter(self, w, x, y): self._show_controls() + def _mouse_leave(self, w): self._schedule_hide_controls() _click_opened_controls = False + def _onclick(self, n, x, y): - if n==0: + if n == 0: # should never happen return if n == 1: @@ -414,9 +451,9 @@ class VideoPlayerBase(Gtk.Overlay): # douple tap triggers 10 second seek # each additional tap seeks another 10 seconds - distance = (n-1) * 10 + distance = (n - 1) * 10 width = self.picture.get_width() - left_bound = width*(2/5) + left_bound = width * (2 / 5) right_bound = width - left_bound if x <= left_bound: self.seek_backwards(distance) @@ -425,8 +462,10 @@ class VideoPlayerBase(Gtk.Overlay): def seek(self, delta): self.goto(self.position + delta) + def seek_forwards(self, delta=30): self.seek(delta) + def seek_backwards(self, delta=30): self.seek(-delta) @@ -437,7 +476,9 @@ class VideoPlayerBase(Gtk.Overlay): # maybe using playbin3? for stream in self.streams: item = Gio.MenuItem.new(stream.quality, None) - item.set_action_and_target_value("quality", GLib.Variant("s", stream.quality)) + item.set_action_and_target_value( + "quality", GLib.Variant("s", stream.quality) + ) quality_model.append_item(item) model.append_submenu(_("Resolution"), quality_model) self.options_menu.set_menu_model(model) @@ -451,7 +492,7 @@ class VideoPlayerBase(Gtk.Overlay): self.play() self._build_menu() - def _find_stream(self, widg, action:str, variant): + def _find_stream(self, widg, action: str, variant): if action != "quality": return # get resolution id @@ -468,6 +509,7 @@ class VideoPlayerBase(Gtk.Overlay): self.stopped = False # run once every second GLib.timeout_add(1000, self._loop) + def _loop(self): if self.stopped: return False @@ -479,12 +521,12 @@ class VideoPlayerBase(Gtk.Overlay): self.ended_callback() if dur[0]: # convert nanoseconds to senconds - dur = dur[1]/(10**9) + dur = dur[1] / (10**9) else: dur = None if pos[0]: # convert nanoseconds to senconds - pos = pos[1]/(10**9) + pos = pos[1] / (10**9) # override position with target # so the preview shows the correct timestamp # has the amazing side effect of also sending @@ -503,9 +545,8 @@ class VideoPlayerBase(Gtk.Overlay): position = self.target_position self.target_position = None self.source.seek_simple( - Gst.Format.TIME, - Gst.SeekFlags.FLUSH, - position*Gst.SECOND) + Gst.Format.TIME, Gst.SeekFlags.FLUSH, position * Gst.SECOND + ) elif not pos is None: self.set_position(pos) elif not dur is None: @@ -517,14 +558,17 @@ class VideoPlayerBase(Gtk.Overlay): self.position = pos self.duration = dur self._update_playhead() + def set_position(self, pos): self.position = pos self._update_playhead() + def set_duration(self, dur): self.duration = dur self._update_playhead() target_position = None + def goto(self, position): # the seeking will happen in the _loop function self.target_position = position @@ -556,19 +600,27 @@ class VideoPlayerBase(Gtk.Overlay): self.source.set_state(Gst.State.PLAYING) # show pause button # because if playing the onclick action is to pause - self.playpause_display.update("", "media-playback-pause-symbolic", tooltip=_("Pause")) + self.playpause_display.update( + "", "media-playback-pause-symbolic", tooltip=_("Pause") + ) + def pause(self): self.paused = True self.source.set_state(Gst.State.PAUSED) # show play button # because if paused the onclick action is to start playing - self.playpause_display.update("", "media-playback-start-symbolic", tooltip=_("Play")) + self.playpause_display.update( + "", "media-playback-start-symbolic", tooltip=_("Play") + ) + def stop(self): self.paused = True self.stopped = True self.source.set_state(Gst.State.NULL) self.source.set_property("instant-uri", None) - self.playpause_display.update("", "media-playback-start-symbolic", tooltip=_("Play")) + self.playpause_display.update( + "", "media-playback-start-symbolic", tooltip=_("Play") + ) def _toggle_playpause(self, _): if self.stopped: @@ -578,6 +630,7 @@ class VideoPlayerBase(Gtk.Overlay): else: self.pause() + class VideoPlayer(Gtk.Stack): def __init__(self, streams: list[Stream], *args, **kwargs): super().__init__(*args, **kwargs) @@ -589,6 +642,7 @@ class VideoPlayer(Gtk.Stack): placeholder.set_label(_("The video is playing in separate window")) self.add_named(placeholder, "placeholder") self._show_player() + def _show_player(self): has_player = self.get_child_by_name("player") if not has_player is None: @@ -598,34 +652,47 @@ class VideoPlayer(Gtk.Stack): self.player.unparent() self.add_named(self.player, "player") self.set_visible_child_name("player") + def _popout_player(self): has_player = self.get_child_by_name("player") if not has_player is None: self.remove(has_player) self.set_visible_child_name("placeholder") + # bridge methods def connect_update(self, callback=None): self.player.connect_update(callback) + def connect_ended(self, callback=None): self.player.connect_ended(callback) + def change_volume(self, vol): self.player.change_volume(vol) + def change_brightness(self, bright): self.player.change_brightness(bright) + def seek(self, delta): self.player.seek(delta) + def seek_forwards(self, delta=30): self.player.seek_forwards(delta) + def seek_backwards(self, delta=30): self.player.seek_backwards(delta) + def goto(self, position): self.player.goto(position) + def play(self): self.player.play() + def pause(self): self.player.pause() + def stop(self): self.player.stop() + def select_stream(self, stream): self.player.select_stream(stream) @@ -638,6 +705,7 @@ class VideoPlayer(Gtk.Stack): self.window.conenct_full(lambda s: self.player.set_toggle_states(True, s)) _only_fullscreen = False + def toggle_fullscreen(self) -> bool: if self.window is None: # currently fixed @@ -675,6 +743,7 @@ class VideoPlayer(Gtk.Stack): res = self.window.is_fullscreen() self.player.set_toggle_states(True, res) return res + def toggle_popout(self) -> bool: if self.window is None: # currently fixed @@ -694,7 +763,7 @@ class VideoPlayer(Gtk.Stack): class VideoPlayerWindow(Adw.Window): - def __init__(self, player:VideoPlayerBase, *args, **kwargs): + def __init__(self, player: VideoPlayerBase, *args, **kwargs): super().__init__(*args, **kwargs) self.player = player self.onclose = None @@ -705,7 +774,7 @@ class VideoPlayerWindow(Adw.Window): # this should block the application self.set_modal(True) self.connect("close-request", lambda _: self.close()) - self.connect("notify", lambda a,b: self._notify()) + self.connect("notify", lambda a, b: self._notify()) self.set_visible(True) self.set_title(_("Player")) @@ -723,5 +792,6 @@ class VideoPlayerWindow(Adw.Window): def connect_close(self, callback=None): self.onclose = callback + def conenct_full(self, callback=None): self.onfull = callback diff --git a/melon/widgets/preferencerow.py b/melon/widgets/preferencerow.py index f577c2c..d728126 100644 --- a/melon/widgets/preferencerow.py +++ b/melon/widgets/preferencerow.py @@ -1,19 +1,21 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, GLib, Gdk from gettext import gettext as _ from copy import deepcopy as copy -from melon.servers import Resource,Video,Playlist,Channel,Preference,PreferenceType +from melon.servers import Resource, Video, Playlist, Channel, Preference, PreferenceType 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(): + +class PreferenceRow: def __init__(self, pref: Preference, *args, **kwargs): self.pref = pref # keep track of own value @@ -27,7 +29,8 @@ class PreferenceRow(): self.widget.set_active(pref.value) self.widget.connect( "notify::active", - lambda a,p: self.pass_to_callback(self.widget.get_active())) + lambda a, p: self.pass_to_callback(self.widget.get_active()), + ) elif pref.type is PreferenceType.DROPDOWN: # implemented as Combo Row self.widget = Adw.ComboRow() @@ -41,7 +44,7 @@ class PreferenceRow(): # there is not better signal available # NOTE: this signal is emitted multiple times "notify", - lambda a,p: self.pass_to_callback(opts[self.widget.get_selected()]) + lambda a, p: self.pass_to_callback(opts[self.widget.get_selected()]), ) elif pref.type is PreferenceType.TEXT or pref.type is PreferenceType.PASSWORD: if pref.type is PreferenceType.TEXT: @@ -52,8 +55,7 @@ class PreferenceRow(): self.widget.set_tooltip_text(pref.description) self.widget.set_text(pref.value) self.widget.connect( - "changed", - lambda a: self.pass_to_callback(self.widget.get_text()) + "changed", lambda a: self.pass_to_callback(self.widget.get_text()) ) elif pref.type is PreferenceType.NUMBER: # implemented as Spin Row @@ -64,8 +66,7 @@ class PreferenceRow(): self.current_value = float(pref.value) self.widget.set_value(self.current_value) self.widget.connect( - "changed", - lambda a: self.pass_to_callback(self.widget.get_value()) + "changed", lambda a: self.pass_to_callback(self.widget.get_value()) ) elif pref.type is PreferenceType.MULTI: self.widget = MultiRow(pref) @@ -75,6 +76,7 @@ class PreferenceRow(): self.widget = Adw.ActionRow() self.widget.set_title(pref.name) self.widget.set_subtitle(pref.description) + def pass_to_callback(self, value): if value == self.current_value: # we don't have to update anything if the value didn't change @@ -85,24 +87,31 @@ class PreferenceRow(): self.current_value = copy(value) if not self.callback is None: self.callback(value) + def get_widget(self): return self.widget - def set_callback(self,cb): + + def set_callback(self, cb): self.callback = cb + def on_active(self): if not self.callback is None: self.callback(self.get_active()) + class MultiRow(Adw.PreferencesRow): callback = None values = [] + def __init__(self, pref, *args, **kwargs): super().__init__(*args, **kwargs) self.pref = pref self.values = copy(pref.value) self.update() + def connect(self, cb): self.callback = cb + def update(self): self.inner = Adw.PreferencesGroup() padding = 12 @@ -126,24 +135,18 @@ class MultiRow(Adw.PreferencesRow): # move up button # not shown for first element if counter > 0: - move_up = IconButton("","go-up-symbolic") + move_up = IconButton("", "go-up-symbolic") move_up.set_tooltip_text(_("Move up")) move_up.add_css_class("circular") - move_up.connect( - "clicked", - pass_me(self.move_up, item, counter) - ) + move_up.connect("clicked", pass_me(self.move_up, item, counter)) actions.append(move_up) # move down button # not shown for last element - if counter < length-1: + if counter < length - 1: move_down = IconButton("", "go-down-symbolic") move_down.set_tooltip_text(_("Move down")) move_down.add_css_class("circular") - move_down.connect( - "clicked", - pass_me(self.move_down, item, counter) - ) + move_down.connect("clicked", pass_me(self.move_down, item, counter)) actions.append(move_down) # remove button remove = IconButton("", "list-remove-symbolic") @@ -154,15 +157,17 @@ class MultiRow(Adw.PreferencesRow): row.add_suffix(actions) self.inner.add(row) counter += 1 + def notify(self): if not self.callback is None: self.callback(copy(self.values)) self.update() + def open_add(self): diag = SimpleDialog() diag.set_title(_("Add Item")) # preview prferences group - box = Adw.PreferencesGroup(); + box = Adw.PreferencesGroup() box.set_title(_("Create a new list entry")) box.set_description(_("Enter the new value here")) # text input @@ -172,29 +177,30 @@ class MultiRow(Adw.PreferencesRow): diag.set_widget(box) # place button bar in toolbar bottom_bar = Gtk.Box() - btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip="Do not create a new item") - btn_confirm = IconButton(_("Create"), "list-add-symbolic", tooltip="Add entry to list") + btn_cancel = IconButton( + _("Cancel"), "process-stop-symbolic", tooltip="Do not create a new item" + ) + btn_confirm = IconButton( + _("Create"), "list-add-symbolic", tooltip="Add entry to list" + ) btn_confirm.connect( "clicked", lambda _: many( - self.values.append(inp.get_text()), - self.notify(), - diag.hide()) + self.values.append(inp.get_text()), self.notify(), diag.hide() + ), ) - btn_cancel.connect( - "clicked", - lambda x: diag.hide()) + btn_cancel.connect("clicked", lambda x: diag.hide()) padding = 12 btn_confirm.set_vexpand(True) btn_confirm.set_hexpand(True) btn_confirm.set_margin_end(padding) - btn_confirm.set_margin_start(padding/2) + btn_confirm.set_margin_start(padding / 2) btn_confirm.set_margin_top(padding) btn_confirm.set_margin_bottom(padding) btn_cancel.set_vexpand(True) btn_cancel.set_hexpand(True) btn_cancel.set_margin_start(padding) - btn_cancel.set_margin_end(padding/2) + btn_cancel.set_margin_end(padding / 2) btn_cancel.set_margin_top(padding) btn_cancel.set_margin_bottom(padding) bottom_bar.append(btn_cancel) @@ -202,11 +208,12 @@ class MultiRow(Adw.PreferencesRow): diag.toolbar_view.add_bottom_bar(bottom_bar) diag.show() - def confirm_delete(self, _, item:str, counter:int): + + def confirm_delete(self, _, item: str, counter: int): diag = SimpleDialog() diag.set_title(_("Delete")) # preview prferences group - preview = Adw.PreferencesGroup(); + preview = Adw.PreferencesGroup() preview.set_title(_("Do you really want to delete this item?")) preview.set_description(_("You won't be able to restore it afterwards")) row = Adw.ActionRow() @@ -215,29 +222,28 @@ class MultiRow(Adw.PreferencesRow): diag.set_widget(preview) # place button bar in toolbar bottom_bar = Gtk.Box() - btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not remove item")) - btn_confirm = IconButton(_("Delete"), "list-add-symbolic", tooltip=_("Remove item from list")) + btn_cancel = IconButton( + _("Cancel"), "process-stop-symbolic", tooltip=_("Do not remove item") + ) + btn_confirm = IconButton( + _("Delete"), "list-add-symbolic", tooltip=_("Remove item from list") + ) btn_confirm.connect( "clicked", - lambda _: many( - self.values.pop(counter), - self.notify(), - diag.hide()) + lambda _: many(self.values.pop(counter), self.notify(), diag.hide()), ) - btn_cancel.connect( - "clicked", - lambda x: diag.hide()) + btn_cancel.connect("clicked", lambda x: diag.hide()) padding = 12 btn_confirm.set_vexpand(True) btn_confirm.set_hexpand(True) btn_confirm.set_margin_end(padding) - btn_confirm.set_margin_start(padding/2) + btn_confirm.set_margin_start(padding / 2) btn_confirm.set_margin_top(padding) btn_confirm.set_margin_bottom(padding) btn_cancel.set_vexpand(True) btn_cancel.set_hexpand(True) btn_cancel.set_margin_start(padding) - btn_cancel.set_margin_end(padding/2) + btn_cancel.set_margin_end(padding / 2) btn_cancel.set_margin_top(padding) btn_cancel.set_margin_bottom(padding) bottom_bar.append(btn_cancel) @@ -245,11 +251,13 @@ class MultiRow(Adw.PreferencesRow): diag.toolbar_view.add_bottom_bar(bottom_bar) diag.show() pass - def move_up(self, _, item:str, counter:int): + + def move_up(self, _, item: str, counter: int): val = self.values.pop(counter) - self.values.insert(counter-1, val) + self.values.insert(counter - 1, val) self.notify() - def move_down(self, _, item:str, counter:int): + + def move_down(self, _, item: str, counter: int): val = self.values.pop(counter) - self.values.insert(counter+1, val) + self.values.insert(counter + 1, val) self.notify() diff --git a/melon/widgets/simpledialog.py b/melon/widgets/simpledialog.py index cf8e7f6..e7e1eeb 100644 --- a/melon/widgets/simpledialog.py +++ b/melon/widgets/simpledialog.py @@ -1,10 +1,12 @@ import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, GLib + class SimpleDialog(Adw.Window): - def __init__(self,*args, **kwargs): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.toolbar_view = Adw.ToolbarView() self.header_bar = Adw.HeaderBar() @@ -15,13 +17,17 @@ class SimpleDialog(Adw.Window): # this is a dialog and thus the main window should not be usable # when the dialog is opened self.set_modal(True) + def set_title(self, text): self.title.set_title(text) - def set_widget(self, child,padding=12): + + def set_widget(self, child, padding=12): self.toolbar_view.set_content(child) child.set_margin_end(padding) child.set_margin_start(padding) + def show(self): self.set_visible(True) + def hide(self): self.set_visible(False) diff --git a/melon/widgets/viewstackpage.py b/melon/widgets/viewstackpage.py index 4e6127a..74d3c18 100644 --- a/melon/widgets/viewstackpage.py +++ b/melon/widgets/viewstackpage.py @@ -1,16 +1,13 @@ -class ViewStackPage(): +class ViewStackPage: name = "" title = "" icon_name = "" widget = None + def __init__(self, name, title, icon): self.name = name self.title = title self.icon_name = icon + def bind_to(self, parent): - parent.add_titled_with_icon( - self.widget, - self.name, - self.title, - self.icon_name - ) + parent.add_titled_with_icon(self.widget, self.name, self.title, self.icon_name) diff --git a/melon/window.py b/melon/window.py index 1d0ed54..64f0909 100644 --- a/melon/window.py +++ b/melon/window.py @@ -1,7 +1,8 @@ import sys import gi -gi.require_version('Gtk', '4.0') -gi.require_version('Adw', '1') + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") from gi.repository import Gtk, Adw, Gio, GLib from gettext import gettext as _ @@ -21,25 +22,31 @@ from melon.playlist import LocalPlaylistScreen from melon.playlist.pick import PlaylistPickerDialog from melon.playlist.create import PlaylistCreatorDialog + class MainWindow(Adw.ApplicationWindow): # Opens the server list page - def open_browse(self,action,prefs): + def open_browse(self, action, prefs): self.view.push(BrowseScreen()) + def open_global_search(self, action, prefs): self.view.push(GlobalSearchScreen()) + # Opens a specific server browse window def open_server_browse(self, action, prefs): # manually convert GLib.Variant to value id = prefs[:] self.view.push(BrowseServerScreen(id, window=self)) + def open_channel_browse(self, action, prefs): plugin = prefs[0] id = prefs[1] self.view.push(BrowseChannelScreen(plugin, id)) + def open_playlist_browse(self, action, prefs): plugin = prefs[0] id = prefs[1] self.view.push(BrowsePlaylistScreen(plugin, id)) + def open_player(self, action, prefs): plugin = prefs[0] id = prefs[1] @@ -52,6 +59,7 @@ class MainWindow(Adw.ApplicationWindow): video = (server_id, video_id) diag = PlaylistPickerDialog(video) diag.show() + def open_playlist_creator(self, action, prefs): act = action.get_name() # video to be automatically added (if available) @@ -64,18 +72,20 @@ class MainWindow(Adw.ApplicationWindow): # create basic dialog diag = PlaylistCreatorDialog(video) diag.show() + 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): + def open_about(self, action, prefs): dialog = Adw.AboutWindow() dialog.set_application_name("Melon") dialog.set_version("0.2.0") @@ -84,26 +94,29 @@ class MainWindow(Adw.ApplicationWindow): dialog.set_comments(_("Stream videos on the go")) dialog.set_website("https://codeberg.org/comcloudway/melon") dialog.set_issue_url("https://codeberg.org/comcloudway/melon/issues") - #dialog.add_credit_section("Contributors", ["Name1 url"]) - #dialog.set_translator_credits("Name1 url") + # dialog.add_credit_section("Contributors", ["Name1 url"]) + # dialog.set_translator_credits("Name1 url") dialog.set_copyright("© 2024 Jakob Meier (@comcloudway)") dialog.set_developers(["Jakob Meier (@comcloudway)"]) # TODO: icon must be uploaded in ~/.local/share/icons or /usr/share/icons dialog.set_application_icon("icu.ccw.Melon") dialog.set_visible(True) + # opens the setting panel - def open_settings(self,action,prefs): + def open_settings(self, action, prefs): self.view.push(SettingsScreen()) + # opens the data import panel - def open_import(self,action,prefs): + def open_import(self, action, prefs): self.view.push(ImporterScreen()) + # navigate back to the home screen - def go_home(self,action,prefs): + def go_home(self, action, prefs): self.view.pop_to_page(self.home) self.home.go_home() def __init__(self, application, *args, **kwargs): - super().__init__(application=application,*args, **kwargs) + super().__init__(application=application, *args, **kwargs) self.view = Adw.NavigationView() self.home = HomeScreen() self.view.add(self.home) @@ -138,7 +151,9 @@ class MainWindow(Adw.ApplicationWindow): 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") + 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/de.po b/po/de.po index b40414a..ed4e60d 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-06-12 21:13+0200\n" +"POT-Creation-Date: 2024-07-15 07:54+0200\n" "PO-Revision-Date: 2024-04-20 14:18+0000\n" "Last-Translator: hurzelchen \n" @@ -86,486 +86,486 @@ msgid "" "button" 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:29 -#: ../melon/home/playlists.py:21 ../melon/home/subs.py:20 -#: ../melon/importer.py:61 ../melon/player/__init__.py:102 -#: ../melon/player/playlist.py:158 ../melon/player/playlist.py:165 -#: ../melon/playlist/__init__.py:61 +#: ../melon/browse/__init__.py:20 ../melon/browse/search.py:66 +#: ../melon/browse/server.py:115 ../melon/browse/server.py:143 +#: ../melon/home/history.py:25 ../melon/home/new.py:36 +#: ../melon/home/playlists.py:23 ../melon/home/subs.py:22 +#: ../melon/importer.py:63 ../melon/player/__init__.py:112 +#: ../melon/player/playlist.py:164 ../melon/player/playlist.py:171 +#: ../melon/playlist/__init__.py:77 msgid "*crickets chirping*" msgstr "*Grillen zirpen*" -#: ../melon/browse/__init__.py:19 +#: ../melon/browse/__init__.py:21 msgid "There are no available servers" msgstr "Es sind keine Dienste vorhanden" -#: ../melon/browse/__init__.py:21 ../melon/browse/search.py:61 -#: ../melon/importer.py:64 +#: ../melon/browse/__init__.py:24 ../melon/browse/search.py:72 +#: ../melon/importer.py:67 msgid "Enable servers in the settings menu" msgstr "Aktiviere Dienste im Einstellungsmenü" -#: ../melon/browse/__init__.py:29 +#: ../melon/browse/__init__.py:33 msgid "Available Servers" msgstr "Verfügbare Dienste" -#: ../melon/browse/__init__.py:30 +#: ../melon/browse/__init__.py:35 msgid "You can enable/disable and filter servers in the settings menu" msgstr "" "Du kannst Dienste in den Einstellungen aktivieren, deaktivieren und filtern" -#: ../melon/browse/__init__.py:48 +#: ../melon/browse/__init__.py:56 msgid "Servers" msgstr "Dienste" -#: ../melon/browse/__init__.py:56 ../melon/browse/search.py:106 +#: ../melon/browse/__init__.py:64 ../melon/browse/search.py:126 msgid "Global Search" msgstr "Globale Suche" -#: ../melon/browse/channel.py:93 +#: ../melon/browse/channel.py:104 msgid "Subscribe to channel" msgstr "Kanal abonnieren" -#: ../melon/browse/channel.py:94 +#: ../melon/browse/channel.py:105 msgid "Add latest uploads to home feed" msgstr "Zeige neue Videos im Home-Feed an" -#: ../melon/browse/channel.py:106 +#: ../melon/browse/channel.py:117 msgid "Channel feed" msgstr "" -#: ../melon/browse/channel.py:107 +#: ../melon/browse/channel.py:118 msgid "This channel provides multiple feeds, choose which one to view" msgstr "" -#: ../melon/browse/channel.py:159 +#: ../melon/browse/channel.py:170 msgid "Channel" msgstr "Kanal" -#: ../melon/browse/playlist.py:37 ../melon/playlist/__init__.py:53 +#: ../melon/browse/playlist.py:48 ../melon/playlist/__init__.py:69 msgid "Start playing" msgstr "Wiedergeben" -#: ../melon/browse/playlist.py:56 ../melon/player/__init__.py:44 +#: ../melon/browse/playlist.py:69 ../melon/player/__init__.py:53 msgid "Bookmark" msgstr "Merken" -#: ../melon/browse/playlist.py:57 +#: ../melon/browse/playlist.py:70 msgid "Add Playlist to your local playlist collection" msgstr "Wiedergabeliste zur lokalen Sammlung hinzufügen" -#: ../melon/browse/playlist.py:111 +#: ../melon/browse/playlist.py:124 msgid "Playlist" msgstr "Wiedergabeliste" -#: ../melon/browse/search.py:44 +#: ../melon/browse/search.py:49 msgid "No results" msgstr "Keine Ergebnisse" -#: ../melon/browse/search.py:46 +#: ../melon/browse/search.py:52 #, python-brace-format msgid "{count} result" msgid_plural "{count} results" msgstr[0] "{count} Ergebnis" msgstr[1] "{count} Ergebnisse" -#: ../melon/browse/search.py:59 +#: ../melon/browse/search.py:68 msgid "There are no available servers, a search would yield no results" msgstr "" "Es gibt keine verfügbaren Dienste, eine Suche würde keine Ergebnisse liefern" -#: ../melon/browse/search.py:83 ../melon/browse/server.py:43 +#: ../melon/browse/search.py:96 ../melon/browse/server.py:47 msgid "Any" msgstr "Alle" -#: ../melon/browse/search.py:85 ../melon/browse/server.py:45 +#: ../melon/browse/search.py:100 ../melon/browse/server.py:51 msgid "Channels" msgstr "Kanäle" -#: ../melon/browse/search.py:88 ../melon/browse/server.py:48 -#: ../melon/home/playlists.py:32 ../melon/home/playlists.py:64 -#: ../melon/servers/nebula/__init__.py:194 -#: ../melon/servers/peertube/__init__.py:205 +#: ../melon/browse/search.py:105 ../melon/browse/server.py:56 +#: ../melon/home/playlists.py:34 ../melon/home/playlists.py:78 +#: ../melon/servers/nebula/__init__.py:206 +#: ../melon/servers/peertube/__init__.py:213 msgid "Playlists" msgstr "Wiedergabelisten" -#: ../melon/browse/search.py:91 ../melon/browse/server.py:51 -#: ../melon/servers/nebula/__init__.py:188 -#: ../melon/servers/peertube/__init__.py:204 -#: ../melon/servers/peertube/__init__.py:208 +#: ../melon/browse/search.py:110 ../melon/browse/server.py:61 +#: ../melon/servers/nebula/__init__.py:200 +#: ../melon/servers/peertube/__init__.py:213 +#: ../melon/servers/peertube/__init__.py:214 msgid "Videos" msgstr "Videos" -#: ../melon/browse/server.py:23 +#: ../melon/browse/server.py:26 msgid "Search" msgstr "Suchen" -#: ../melon/browse/server.py:102 +#: ../melon/browse/server.py:116 msgid "Try searching for a term" msgstr "Suche nach einem Suchbegriff" -#: ../melon/browse/server.py:104 +#: ../melon/browse/server.py:118 msgid "Try using a different query" msgstr "Versuche nach einem anderen Begriff zu suchen" -#: ../melon/browse/server.py:128 +#: ../melon/browse/server.py:144 msgid "This feed is empty" msgstr "" -#: ../melon/home/__init__.py:18 +#: ../melon/home/__init__.py:20 msgid "Home" msgstr "Startseite" -#: ../melon/home/__init__.py:39 ../melon/home/history.py:26 -#: ../melon/home/new.py:36 ../melon/home/subs.py:23 +#: ../melon/home/__init__.py:41 ../melon/home/history.py:28 +#: ../melon/home/new.py:49 ../melon/home/subs.py:25 msgid "Browse Servers" msgstr "Dienste durchsuchen" -#: ../melon/home/__init__.py:46 +#: ../melon/home/__init__.py:48 msgid "Preferences" msgstr "Einstellungen" -#: ../melon/home/__init__.py:47 +#: ../melon/home/__init__.py:49 msgid "Import Data" msgstr "Daten Importieren" -#: ../melon/home/__init__.py:48 +#: ../melon/home/__init__.py:50 msgid "About Melon" msgstr "Über Melon" -#: ../melon/home/history.py:24 +#: ../melon/home/history.py:26 msgid "You haven't watched any videos yet" msgstr "Du hast noch keine Videos geschaut" -#: ../melon/home/history.py:36 ../melon/home/history.py:117 +#: ../melon/home/history.py:38 ../melon/home/history.py:125 msgid "History" msgstr "Verlauf" -#: ../melon/home/history.py:37 +#: ../melon/home/history.py:39 msgid "These are the videos you opened in the past" msgstr "Diese Videos hast du in der Vergangenheit geöffnet" -#: ../melon/home/history.py:76 +#: ../melon/home/history.py:83 msgid "Show more" msgstr "Mehr anzeigen" -#: ../melon/home/history.py:77 +#: ../melon/home/history.py:84 msgid "Load older videos" msgstr "" -#: ../melon/home/new.py:23 +#: ../melon/home/new.py:30 msgid "Refresh" msgstr "Aktualisieren" -#: ../melon/home/new.py:32 +#: ../melon/home/new.py:40 msgid "Subscribe to a channel first, to view new uploads" msgstr "Du musst einem Kanal folgen, um neue Videos zu sehen." -#: ../melon/home/new.py:34 +#: ../melon/home/new.py:45 msgid "The channels you are subscribed to haven't uploaded anything yet" msgstr "Die Kanäle, die du abonniert hast, haben noch nichts hochgeladen" -#: ../melon/home/new.py:55 +#: ../melon/home/new.py:68 #, python-brace-format msgid "(Last refresh: {last_refresh})" msgstr "(Zuletzt aktualisiert: {last_refresh})" -#: ../melon/home/new.py:57 ../melon/home/new.py:146 +#: ../melon/home/new.py:72 ../melon/home/new.py:172 msgid "What's new" msgstr "Neues" -#: ../melon/home/new.py:58 +#: ../melon/home/new.py:74 msgid "These are the latest videos of channels you follow" msgstr "Hier sind die neusten Videos von Kanälen, denen du folgst" -#: ../melon/home/new.py:137 +#: ../melon/home/new.py:162 msgid "Pick up where you left off" msgstr "" -#: ../melon/home/new.py:138 +#: ../melon/home/new.py:163 msgid "Watch" msgstr "" -#: ../melon/home/playlists.py:22 +#: ../melon/home/playlists.py:24 msgid "You don't have any playlists yet" msgstr "Du hast noch keine Wiedergabelisten" -#: ../melon/home/playlists.py:24 ../melon/home/playlists.py:34 +#: ../melon/home/playlists.py:26 ../melon/home/playlists.py:39 msgid "Create a new playlist" msgstr "Neue Wiedergabeliste erstellen" -#: ../melon/home/playlists.py:33 +#: ../melon/home/playlists.py:36 msgid "Here are playlists you've bookmarked or created yourself" msgstr "" "Hier ist eine Liste an Wiedergabelisten, die du dir gemerkt oder selber " "erstellt hast" -#: ../melon/home/playlists.py:34 +#: ../melon/home/playlists.py:39 msgid "New" msgstr "Neu" -#: ../melon/home/subs.py:21 +#: ../melon/home/subs.py:23 msgid "You aren't yet subscribed to channels" msgstr "Du folgst noch keinen Kanälen" -#: ../melon/home/subs.py:31 ../melon/home/subs.py:63 +#: ../melon/home/subs.py:33 ../melon/home/subs.py:68 msgid "Subscriptions" msgstr "Kanäle" -#: ../melon/home/subs.py:32 +#: ../melon/home/subs.py:34 msgid "You are subscribed to the following channels" msgstr "Du folgst den folgenden Kanälen" -#: ../melon/import_providers/newpipe.py:28 +#: ../melon/import_providers/newpipe.py:42 msgid "Newpipe Database importer" msgstr "NewPipe Datenbank Import" -#: ../melon/import_providers/newpipe.py:29 +#: ../melon/import_providers/newpipe.py:44 msgid "" "Import the .db file from inside the newpipe .zip export (as invidious " "content)" msgstr "" "Importiere die .db Datei aus dem NewPipe .zip Export (als Invidious Inhalte)" -#: ../melon/import_providers/newpipe.py:30 +#: ../melon/import_providers/newpipe.py:46 msgid "Select .db file" msgstr "Wähle eine .db Datei" -#: ../melon/import_providers/newpipe.py:45 +#: ../melon/import_providers/newpipe.py:60 msgid "Newpipe Database" msgstr "NewPipe Datenbank" -#: ../melon/importer.py:17 ../melon/importer.py:35 +#: ../melon/importer.py:19 ../melon/importer.py:37 msgid "Import" msgstr "Importieren" -#: ../melon/importer.py:36 +#: ../melon/importer.py:38 #, fuzzy msgid "The following import methods have been found" msgstr "Die folgenden Import-Methoden wurden gefunden" -#: ../melon/importer.py:62 +#: ../melon/importer.py:64 #, fuzzy msgid "There are no available importer methods" msgstr "Es gibt keine verfügbaren Import-Methoden" -#: ../melon/player/__init__.py:34 +#: ../melon/player/__init__.py:41 msgid "Description" msgstr "Beschreibung" -#: ../melon/player/__init__.py:45 +#: ../melon/player/__init__.py:54 msgid "Add this video to a playlist" msgstr "Füge dieses Video einer Wiedergabeliste hinzu" -#: ../melon/player/__init__.py:103 +#: ../melon/player/__init__.py:113 msgid "Video could not be loaded" msgstr "Das Video konnte nicht geladen werden" -#: ../melon/player/__init__.py:140 ../melon/player/__init__.py:174 -#: ../melon/player/playlist.py:51 +#: ../melon/player/__init__.py:151 ../melon/player/__init__.py:185 +#: ../melon/player/playlist.py:57 msgid "Loading..." msgstr "" -#: ../melon/player/playlist.py:159 +#: ../melon/player/playlist.py:165 msgid "This playlist is empty" msgstr "Diese Wiedergabeliste ist leer" -#: ../melon/player/playlist.py:166 +#: ../melon/player/playlist.py:172 msgid "There was an error loading the playlist" msgstr "Beim Laden der Wiedergabeliste ist ein Fehler aufgetreten" -#: ../melon/player/playlist.py:218 +#: ../melon/player/playlist.py:225 msgid "Previous" msgstr "Vorheriges" -#: ../melon/player/playlist.py:219 +#: ../melon/player/playlist.py:227 msgid "Play video that comes before this one in the playlist" msgstr "Spielt das Video, was in der Wiedergabeliste vor diesem kommt" -#: ../melon/player/playlist.py:230 +#: ../melon/player/playlist.py:244 msgid "Next" msgstr "Nächstes" -#: ../melon/player/playlist.py:231 +#: ../melon/player/playlist.py:246 msgid "Play video that comes after this one in the playlist" msgstr "Spielt das Video, was in der Wiedergabeliste nach diesem kommt" -#: ../melon/player/playlist.py:240 +#: ../melon/player/playlist.py:258 msgid "Skip" msgstr "Überspringen" -#: ../melon/player/playlist.py:241 +#: ../melon/player/playlist.py:259 msgid "Skip this video and pick a new one at random" msgstr "Überspringt dieses Video und wählt zufällig ein Neues aus" -#: ../melon/player/playlist.py:250 +#: ../melon/player/playlist.py:270 msgid "Shuffle" msgstr "" -#: ../melon/player/playlist.py:251 +#: ../melon/player/playlist.py:271 msgid "Chooses the next video at random" msgstr "Wählt das nächste Video zufällig aus" -#: ../melon/player/playlist.py:261 +#: ../melon/player/playlist.py:282 msgid "Repeat current video" msgstr "" -#: ../melon/player/playlist.py:262 +#: ../melon/player/playlist.py:283 msgid "Puts this video on loop" msgstr "" -#: ../melon/player/playlist.py:276 +#: ../melon/player/playlist.py:298 msgid "Repeat playlist" msgstr "" -#: ../melon/player/playlist.py:277 +#: ../melon/player/playlist.py:300 msgid "Start playling the playlist from the beginning after reaching the end" msgstr "" "Spielt die Wiedergabeliste erneut von Beginn, wenn das Ende erreicht wurde" -#: ../melon/player/playlist.py:287 +#: ../melon/player/playlist.py:312 msgid "Playlist Content" msgstr "" -#: ../melon/player/playlist.py:288 +#: ../melon/player/playlist.py:314 msgid "Click on videos to continue playing the playlist from there" msgstr "Klicke auf ein Video, um die Wiedergabeliste von dort fortzusetzen" -#: ../melon/playlist/__init__.py:50 +#: ../melon/playlist/__init__.py:65 msgid "Edit" msgstr "Bearbeiten" -#: ../melon/playlist/__init__.py:62 +#: ../melon/playlist/__init__.py:79 msgid "You haven't added any videos to this playlist yet" msgstr "Du hast noch keine Videos zu dieser Wiedergabeliste hinzugefügt" -#: ../melon/playlist/__init__.py:64 +#: ../melon/playlist/__init__.py:82 msgid "Start watching" msgstr "Videos anschauen" -#: ../melon/playlist/__init__.py:87 +#: ../melon/playlist/__init__.py:106 msgid "Set as playlist thumbnail" msgstr "" -#: ../melon/playlist/__init__.py:88 +#: ../melon/playlist/__init__.py:114 msgid "Remove from playlist" msgstr "" -#: ../melon/playlist/__init__.py:99 +#: ../melon/playlist/__init__.py:131 msgid "Edit Playlist" msgstr "Wiedergabeliste bearbeiten" -#: ../melon/playlist/__init__.py:105 +#: ../melon/playlist/__init__.py:137 msgid "Playlist details" msgstr "Wiedergabelistendetails" -#: ../melon/playlist/__init__.py:106 +#: ../melon/playlist/__init__.py:138 msgid "Change playlist information" msgstr "Wiedergabelistendetails ändern" -#: ../melon/playlist/__init__.py:108 ../melon/playlist/create.py:37 +#: ../melon/playlist/__init__.py:140 ../melon/playlist/create.py:43 msgid "Playlist name" msgstr "Name der Wiedergabeliste" -#: ../melon/playlist/__init__.py:111 ../melon/playlist/create.py:40 +#: ../melon/playlist/__init__.py:143 ../melon/playlist/create.py:46 msgid "Playlist description" msgstr "Beschreibung der Wiedergabeliste" -#: ../melon/playlist/__init__.py:116 +#: ../melon/playlist/__init__.py:148 msgid "Save details" msgstr "Details speichern" -#: ../melon/playlist/__init__.py:117 +#: ../melon/playlist/__init__.py:150 msgid "Change playlist title and description. (Closes the dialog)" msgstr "" "Titel und Beschreibung der Wiedergabeliste ändern. (Schließt den Dialog)" -#: ../melon/playlist/__init__.py:127 ../melon/playlist/__init__.py:169 +#: ../melon/playlist/__init__.py:161 ../melon/playlist/__init__.py:207 msgid "Delete Playlist" msgstr "Wiedergabeliste löschen" -#: ../melon/playlist/__init__.py:128 +#: ../melon/playlist/__init__.py:163 msgid "Delete this playlist and it's content. This can NOT be undone." msgstr "" "Diese Wiedergabeliste und ihren Inhalt löschen. Diese Aktion kann nicht " "rückgängig gemacht werden." -#: ../melon/playlist/__init__.py:139 +#: ../melon/playlist/__init__.py:176 msgid "Close" msgstr "Schließen" -#: ../melon/playlist/__init__.py:139 +#: ../melon/playlist/__init__.py:178 msgid "Close without changing anything" msgstr "" -#: ../melon/playlist/__init__.py:172 +#: ../melon/playlist/__init__.py:210 msgid "Do you really want to delete this playlist?" msgstr "Möchtest du diese Wiedergabeliste wirklich löschen?" -#: ../melon/playlist/__init__.py:173 +#: ../melon/playlist/__init__.py:211 msgid "This cannot be undone" msgstr "Dies kann nicht rückgängig gemacht werden" -#: ../melon/playlist/__init__.py:177 ../melon/playlist/create.py:47 -#: ../melon/playlist/pick.py:70 ../melon/widgets/preferencerow.py:175 -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/playlist/__init__.py:216 ../melon/playlist/create.py:54 +#: ../melon/playlist/pick.py:88 ../melon/widgets/preferencerow.py:181 +#: ../melon/widgets/preferencerow.py:226 msgid "Cancel" msgstr "Abbruch" -#: ../melon/playlist/__init__.py:177 +#: ../melon/playlist/__init__.py:218 msgid "Do not delete the playlist" msgstr "Wiedergabeliste nicht löschen" -#: ../melon/playlist/__init__.py:178 ../melon/widgets/preferencerow.py:207 -#: ../melon/widgets/preferencerow.py:219 +#: ../melon/playlist/__init__.py:221 ../melon/widgets/preferencerow.py:214 +#: ../melon/widgets/preferencerow.py:229 msgid "Delete" msgstr "Löschen" -#: ../melon/playlist/__init__.py:178 +#: ../melon/playlist/__init__.py:221 msgid "Delete this playlist" msgstr "Diese Wiedergabeliste löschen" -#: ../melon/playlist/create.py:23 ../melon/playlist/pick.py:28 +#: ../melon/playlist/create.py:25 ../melon/playlist/pick.py:35 msgid "Video" msgstr "Video" -#: ../melon/playlist/create.py:24 +#: ../melon/playlist/create.py:27 msgid "The following video will be added to the new playlist" msgstr "Das folgende Video wird zu der neuen Wiedergabeliste hinzugefügt" -#: ../melon/playlist/create.py:33 ../melon/playlist/create.py:91 +#: ../melon/playlist/create.py:39 ../melon/playlist/create.py:98 msgid "New Playlist" msgstr "Neue Wiedergabeliste" -#: ../melon/playlist/create.py:34 +#: ../melon/playlist/create.py:40 msgid "Enter more playlist information" msgstr "Gibt weitere Wiedergabelistendetails an" -#: ../melon/playlist/create.py:38 +#: ../melon/playlist/create.py:44 msgid "Unnamed Playlist" msgstr "Unbenannte Wiedergabeliste" -#: ../melon/playlist/create.py:47 ../melon/playlist/pick.py:70 +#: ../melon/playlist/create.py:54 ../melon/playlist/pick.py:88 msgid "Do not create playlist" msgstr "Wiedergabeliste nicht erstellen" -#: ../melon/playlist/create.py:48 ../melon/widgets/preferencerow.py:176 +#: ../melon/playlist/create.py:57 ../melon/widgets/preferencerow.py:184 msgid "Create" msgstr "Erstellen" -#: ../melon/playlist/create.py:48 +#: ../melon/playlist/create.py:57 msgid "Create playlist" msgstr "Wiedergabeliste erstellen" -#: ../melon/playlist/pick.py:29 +#: ../melon/playlist/pick.py:37 msgid "The following video will be added to the playlist" msgstr "Das folgende Video wird zur Wiedergabeliste hinzugefügt" -#: ../melon/playlist/pick.py:39 ../melon/widgets/feeditem.py:122 +#: ../melon/playlist/pick.py:50 ../melon/widgets/feeditem.py:145 msgid "Add to playlist" msgstr "Zur Wiedergabeliste hinzufügen" -#: ../melon/playlist/pick.py:40 +#: ../melon/playlist/pick.py:53 msgid "" "Choose a playlist to add the video to. Note that you can only add videos to " "local playlists, not external bookmarked ones" @@ -573,42 +573,42 @@ msgstr "" "Wähle die Wiedergabeliste, zu der das Video hinzugefügt werden soll. (Du " "kannst Videos nur zu lokalen Wiedergabelisten hinzufügen, nicht in gemerkte)" -#: ../melon/playlist/pick.py:42 +#: ../melon/playlist/pick.py:57 msgid "Create new playlist" msgstr "Neue Wiedergabeliste erstellen" -#: ../melon/playlist/pick.py:43 +#: ../melon/playlist/pick.py:58 msgid "Create a new playlist and add the video to it" msgstr "Neue Wiedergabeliste erstellen und Video hinzufügen" -#: ../melon/playlist/pick.py:102 +#: ../melon/playlist/pick.py:119 msgid "Add to Playlist" msgstr "Zur Wiedergabeliste hinzufügen" -#: ../melon/servers/invidious/__init__.py:31 +#: ../melon/servers/invidious/__init__.py:35 msgid "Open source alternative front-end to YouTube" msgstr "Quelloffenes alternatives Frontend für YouTube" -#: ../melon/servers/invidious/__init__.py:36 +#: ../melon/servers/invidious/__init__.py:40 msgid "Instance" msgstr "Instanz" -#: ../melon/servers/invidious/__init__.py:37 +#: ../melon/servers/invidious/__init__.py:42 msgid "" "See https://docs.invidious.io/instances/ for a list of available instances" msgstr "" "Auf https://docs.invidious.io/instances/ kannst du eine liste verfügbarer " "Instanzen finden" -#: ../melon/servers/invidious/__init__.py:48 +#: ../melon/servers/invidious/__init__.py:55 msgid "Trending" msgstr "Trends" -#: ../melon/servers/invidious/__init__.py:49 +#: ../melon/servers/invidious/__init__.py:56 msgid "Popular" msgstr "Beliebt" -#: ../melon/servers/nebula/__init__.py:19 +#: ../melon/servers/nebula/__init__.py:21 msgid "" "Home of smart, thoughtful videos, podcasts, and classes from your favorite " "creators" @@ -616,32 +616,32 @@ msgstr "" "Zu Hause von intelligenten, durchdachten Videos, Podcasts und Kursen von " "deinen Lieblingskanälen" -#: ../melon/servers/nebula/__init__.py:24 +#: ../melon/servers/nebula/__init__.py:27 msgid "Email Address" msgstr "E-Mail-Adresse" -#: ../melon/servers/nebula/__init__.py:25 +#: ../melon/servers/nebula/__init__.py:28 msgid "Email Address to login to your account" msgstr "E-Mail-Adresse zur Anmeldung mit deinem Konto" -#: ../melon/servers/nebula/__init__.py:31 +#: ../melon/servers/nebula/__init__.py:35 msgid "Password" msgstr "Passwort" -#: ../melon/servers/nebula/__init__.py:32 +#: ../melon/servers/nebula/__init__.py:36 msgid "Password associated with your account" msgstr "Passwort zur Anmeldung mit deinem Konto" -#: ../melon/servers/peertube/__init__.py:15 +#: ../melon/servers/peertube/__init__.py:16 msgid "Decentralized video hosting network, based on free/libre software" msgstr "" "Dezentralisiertes Video-Hosting-Netzwerk, basierend auf freier/libre Software" -#: ../melon/servers/peertube/__init__.py:20 +#: ../melon/servers/peertube/__init__.py:21 msgid "Instances" msgstr "Instanzen" -#: ../melon/servers/peertube/__init__.py:21 +#: ../melon/servers/peertube/__init__.py:23 msgid "" "List of peertube instances, from which to fetch content. See https://" "joinpeertube.org/instances" @@ -649,32 +649,32 @@ msgstr "" "Liste von PeerTube Instanzen, von denen Inhalte geladen werden sollen. Siehe " "https://joinpeertube.org/instances" -#: ../melon/servers/peertube/__init__.py:27 +#: ../melon/servers/peertube/__init__.py:31 msgid "Show NSFW content" msgstr "NSFW Inhalte anzeigen" -#: ../melon/servers/peertube/__init__.py:28 +#: ../melon/servers/peertube/__init__.py:32 #, fuzzy msgid "Passes the nsfw filter to the peertube search API" msgstr "Gibt den NSFW Filter an die PeerTube Such-API weiter" -#: ../melon/servers/peertube/__init__.py:33 +#: ../melon/servers/peertube/__init__.py:39 msgid "Enable Federation" msgstr "" -#: ../melon/servers/peertube/__init__.py:34 +#: ../melon/servers/peertube/__init__.py:40 msgid "Returns content from federated instances instead of only local content" msgstr "" -#: ../melon/servers/peertube/__init__.py:58 +#: ../melon/servers/peertube/__init__.py:65 msgid "Latest" msgstr "Neuste" -#: ../melon/settings/__init__.py:18 +#: ../melon/settings/__init__.py:23 msgid "Show Previews when browsing public feeds (incl. search)" msgstr "Vorschaubilder für externe Inhalte anzeigen" -#: ../melon/settings/__init__.py:19 +#: ../melon/settings/__init__.py:25 msgid "" "Set to true to show previews when viewing channel contents, public feeds and " "searching" @@ -682,11 +682,11 @@ msgstr "" "Aktiviert Vorschaubilder beim Durchsuchen von Diensten, Ansehen von Kanälen " "und Wiedergabelisten" -#: ../melon/settings/__init__.py:25 +#: ../melon/settings/__init__.py:34 msgid "Show Previews in local feeds" msgstr "Vorschaubilder für lokale Inhalte anzeigen" -#: ../melon/settings/__init__.py:26 +#: ../melon/settings/__init__.py:36 msgid "" "Set to true to show previews in the new feed, for subscribed channels, and " "saved playlists" @@ -694,12 +694,12 @@ msgstr "" "Aktiviert Vorschaubilder im Neues Tab, im Kanäle Tab und für selbst " "erstellte Wiedergabelisten" -#: ../melon/settings/__init__.py:32 +#: ../melon/settings/__init__.py:44 #, fuzzy msgid "Show servers that may contain nsfw content" msgstr "Dienste anzeigen, die teilweise NSFW Inhalte enthalten" -#: ../melon/settings/__init__.py:33 +#: ../melon/settings/__init__.py:46 #, fuzzy msgid "" "Lists/Delists servers in the browse servers list, if they contain some nsfw " @@ -707,12 +707,12 @@ msgid "" msgstr "" "Zeige Dienste, die vereinzelt NSFW Inhalte beinhalten, beim Durchsuchen an" -#: ../melon/settings/__init__.py:38 +#: ../melon/settings/__init__.py:54 #, fuzzy msgid "Show servers that only contain nsfw content" msgstr "Dienste anzeigen, die ausschließlich NSFW Inhalte enthalten" -#: ../melon/settings/__init__.py:39 +#: ../melon/settings/__init__.py:56 #, fuzzy msgid "" "Lists/Delists servers in the browse servers list, if they contain only/" @@ -720,137 +720,137 @@ msgid "" msgstr "" "Zeige Dienste, die (fast) nur NSFW Inhalte beinhalten, beim Durchsuchen an" -#: ../melon/settings/__init__.py:45 +#: ../melon/settings/__init__.py:64 msgid "Show servers that require login" msgstr "Dienste anzeigen, die ein Konto benötigen" -#: ../melon/settings/__init__.py:46 +#: ../melon/settings/__init__.py:66 msgid "" "Lists/Delists servers in the browse servers list, if they require login to " "function" msgstr "Zeige Dienste, für die du einen Account brauchst, beim Durchsuchen an" -#: ../melon/settings/__init__.py:55 +#: ../melon/settings/__init__.py:78 msgid "Settings" msgstr "Einstellungen" -#: ../melon/settings/__init__.py:68 +#: ../melon/settings/__init__.py:91 msgid "General" msgstr "Allgemein" -#: ../melon/settings/__init__.py:69 +#: ../melon/settings/__init__.py:92 msgid "Global app settings" msgstr "Globale App Einstellungen" -#: ../melon/settings/__init__.py:93 +#: ../melon/settings/__init__.py:116 msgid "Enable Server" msgstr "Dienst aktivieren" -#: ../melon/settings/__init__.py:94 +#: ../melon/settings/__init__.py:118 msgid "" "Disabled servers won't show up in the browser or on the local/home screen" msgstr "" "Deaktivierte Dienste werden beim Durchsuchen und auf dem Startbildschirm " "nicht angezeigt" -#: ../melon/widgets/feeditem.py:121 +#: ../melon/widgets/feeditem.py:139 msgid "Watch now" msgstr "" -#: ../melon/widgets/feeditem.py:123 +#: ../melon/widgets/feeditem.py:152 msgid "Open in browser" msgstr "" -#: ../melon/widgets/feeditem.py:124 +#: ../melon/widgets/feeditem.py:156 msgid "View channel" msgstr "" -#: ../melon/widgets/player.py:119 +#: ../melon/widgets/player.py:136 msgid "No streams available" msgstr "" -#: ../melon/widgets/player.py:180 +#: ../melon/widgets/player.py:198 msgid "Toggle floating window" msgstr "" -#: ../melon/widgets/player.py:184 +#: ../melon/widgets/player.py:204 msgid "Toggle fullscreen" msgstr "" -#: ../melon/widgets/player.py:204 ../melon/widgets/player.py:565 -#: ../melon/widgets/player.py:571 +#: ../melon/widgets/player.py:226 ../melon/widgets/player.py:613 +#: ../melon/widgets/player.py:622 msgid "Play" msgstr "" -#: ../melon/widgets/player.py:230 +#: ../melon/widgets/player.py:253 msgid "Stream options" msgstr "" -#: ../melon/widgets/player.py:442 +#: ../melon/widgets/player.py:483 msgid "Resolution" msgstr "Auflösung" -#: ../melon/widgets/player.py:559 +#: ../melon/widgets/player.py:604 msgid "Pause" msgstr "Pause" -#: ../melon/widgets/player.py:589 +#: ../melon/widgets/player.py:642 msgid "The video is playing in separate window" msgstr "Dieses Video spielt in einem separaten Fenster" -#: ../melon/widgets/player.py:710 +#: ../melon/widgets/player.py:779 msgid "Player" msgstr "" -#: ../melon/widgets/preferencerow.py:116 +#: ../melon/widgets/preferencerow.py:125 msgid "Add" msgstr "Hinzufügen" -#: ../melon/widgets/preferencerow.py:130 +#: ../melon/widgets/preferencerow.py:139 msgid "Move up" msgstr "Nach oben verschieben" -#: ../melon/widgets/preferencerow.py:141 +#: ../melon/widgets/preferencerow.py:147 msgid "Move down" msgstr "Nach unten verschieben" -#: ../melon/widgets/preferencerow.py:150 +#: ../melon/widgets/preferencerow.py:153 msgid "Remove from list" msgstr "Von der Liste entfernen" -#: ../melon/widgets/preferencerow.py:163 +#: ../melon/widgets/preferencerow.py:168 msgid "Add Item" msgstr "Eintrag hinzufügen" -#: ../melon/widgets/preferencerow.py:166 +#: ../melon/widgets/preferencerow.py:171 msgid "Create a new list entry" msgstr "Neuen Listeneintrag erstellen" -#: ../melon/widgets/preferencerow.py:167 +#: ../melon/widgets/preferencerow.py:172 msgid "Enter the new value here" msgstr "Gibt den neuen Wert hier ein" -#: ../melon/widgets/preferencerow.py:170 +#: ../melon/widgets/preferencerow.py:175 msgid "Value" msgstr "Wert" -#: ../melon/widgets/preferencerow.py:210 +#: ../melon/widgets/preferencerow.py:217 msgid "Do you really want to delete this item?" msgstr "Möchtest du den Eintrag wirklich löschen?" -#: ../melon/widgets/preferencerow.py:211 +#: ../melon/widgets/preferencerow.py:218 msgid "You won't be able to restore it afterwards" msgstr "Eine Wiederherstellung ist nicht möglich" -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/widgets/preferencerow.py:226 msgid "Do not remove item" msgstr "Eintrag nicht entfernen" -#: ../melon/widgets/preferencerow.py:219 +#: ../melon/widgets/preferencerow.py:229 msgid "Remove item from list" msgstr "Eintrag von der Liste entfernen" -#: ../melon/window.py:84 +#: ../melon/window.py:94 msgid "Stream videos on the go" msgstr "Videos unterwegs streamen" diff --git a/po/fa.po b/po/fa.po index a2299d1..13e61ce 100644 --- a/po/fa.po +++ b/po/fa.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Melon 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-12 21:13+0200\n" +"POT-Creation-Date: 2024-07-15 07:54+0200\n" "PO-Revision-Date: 2024-05-30 09:18+0000\n" "Last-Translator: sohrabbehdani \n" @@ -92,477 +92,477 @@ msgid "" 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:29 -#: ../melon/home/playlists.py:21 ../melon/home/subs.py:20 -#: ../melon/importer.py:61 ../melon/player/__init__.py:102 -#: ../melon/player/playlist.py:158 ../melon/player/playlist.py:165 -#: ../melon/playlist/__init__.py:61 +#: ../melon/browse/__init__.py:20 ../melon/browse/search.py:66 +#: ../melon/browse/server.py:115 ../melon/browse/server.py:143 +#: ../melon/home/history.py:25 ../melon/home/new.py:36 +#: ../melon/home/playlists.py:23 ../melon/home/subs.py:22 +#: ../melon/importer.py:63 ../melon/player/__init__.py:112 +#: ../melon/player/playlist.py:164 ../melon/player/playlist.py:171 +#: ../melon/playlist/__init__.py:77 #, fuzzy msgid "*crickets chirping*" msgstr "*جیرجیرک جیرجیرک*" -#: ../melon/browse/__init__.py:19 +#: ../melon/browse/__init__.py:21 msgid "There are no available servers" msgstr "" -#: ../melon/browse/__init__.py:21 ../melon/browse/search.py:61 -#: ../melon/importer.py:64 +#: ../melon/browse/__init__.py:24 ../melon/browse/search.py:72 +#: ../melon/importer.py:67 msgid "Enable servers in the settings menu" msgstr "فعال کردن کارساز در منوی تنظیمات" -#: ../melon/browse/__init__.py:29 +#: ../melon/browse/__init__.py:33 msgid "Available Servers" msgstr "کارساز های موجود" -#: ../melon/browse/__init__.py:30 +#: ../melon/browse/__init__.py:35 msgid "You can enable/disable and filter servers in the settings menu" msgstr "شما می‌توانید کارسازها را در تنظیمات فعال/غیرفعال و پالایش کنید." -#: ../melon/browse/__init__.py:48 +#: ../melon/browse/__init__.py:56 msgid "Servers" msgstr "کارسازها" -#: ../melon/browse/__init__.py:56 ../melon/browse/search.py:106 +#: ../melon/browse/__init__.py:64 ../melon/browse/search.py:126 msgid "Global Search" msgstr "جستجو سراسری" -#: ../melon/browse/channel.py:93 +#: ../melon/browse/channel.py:104 msgid "Subscribe to channel" msgstr "مشترک کانال شوید" -#: ../melon/browse/channel.py:94 +#: ../melon/browse/channel.py:105 msgid "Add latest uploads to home feed" msgstr "افزودن آخرین بارگذاری‌ها به صفحه‌خانه" -#: ../melon/browse/channel.py:106 +#: ../melon/browse/channel.py:117 msgid "Channel feed" msgstr "خوراک کانال" -#: ../melon/browse/channel.py:107 +#: ../melon/browse/channel.py:118 msgid "This channel provides multiple feeds, choose which one to view" msgstr "" -#: ../melon/browse/channel.py:159 +#: ../melon/browse/channel.py:170 msgid "Channel" msgstr "کانال" -#: ../melon/browse/playlist.py:37 ../melon/playlist/__init__.py:53 +#: ../melon/browse/playlist.py:48 ../melon/playlist/__init__.py:69 msgid "Start playing" msgstr "شروع به پخش کردن" -#: ../melon/browse/playlist.py:56 ../melon/player/__init__.py:44 +#: ../melon/browse/playlist.py:69 ../melon/player/__init__.py:53 msgid "Bookmark" msgstr "نشانک گذاری" -#: ../melon/browse/playlist.py:57 +#: ../melon/browse/playlist.py:70 msgid "Add Playlist to your local playlist collection" msgstr "افزودن این لیست‌پخش به مجموعه لیست‌پخش های محلی" -#: ../melon/browse/playlist.py:111 +#: ../melon/browse/playlist.py:124 msgid "Playlist" msgstr "سیاهه‌ٔ پخش" -#: ../melon/browse/search.py:44 +#: ../melon/browse/search.py:49 msgid "No results" msgstr "بدون نتیجه" -#: ../melon/browse/search.py:46 +#: ../melon/browse/search.py:52 #, python-brace-format msgid "{count} result" msgid_plural "{count} results" msgstr[0] "" msgstr[1] "" -#: ../melon/browse/search.py:59 +#: ../melon/browse/search.py:68 msgid "There are no available servers, a search would yield no results" msgstr "" -#: ../melon/browse/search.py:83 ../melon/browse/server.py:43 +#: ../melon/browse/search.py:96 ../melon/browse/server.py:47 msgid "Any" msgstr "همه" -#: ../melon/browse/search.py:85 ../melon/browse/server.py:45 +#: ../melon/browse/search.py:100 ../melon/browse/server.py:51 msgid "Channels" msgstr "کانال‌ها" -#: ../melon/browse/search.py:88 ../melon/browse/server.py:48 -#: ../melon/home/playlists.py:32 ../melon/home/playlists.py:64 -#: ../melon/servers/nebula/__init__.py:194 -#: ../melon/servers/peertube/__init__.py:205 +#: ../melon/browse/search.py:105 ../melon/browse/server.py:56 +#: ../melon/home/playlists.py:34 ../melon/home/playlists.py:78 +#: ../melon/servers/nebula/__init__.py:206 +#: ../melon/servers/peertube/__init__.py:213 msgid "Playlists" msgstr "سیاههٔ پخش" -#: ../melon/browse/search.py:91 ../melon/browse/server.py:51 -#: ../melon/servers/nebula/__init__.py:188 -#: ../melon/servers/peertube/__init__.py:204 -#: ../melon/servers/peertube/__init__.py:208 +#: ../melon/browse/search.py:110 ../melon/browse/server.py:61 +#: ../melon/servers/nebula/__init__.py:200 +#: ../melon/servers/peertube/__init__.py:213 +#: ../melon/servers/peertube/__init__.py:214 msgid "Videos" msgstr "ویدیوها" -#: ../melon/browse/server.py:23 +#: ../melon/browse/server.py:26 msgid "Search" msgstr "جست‌وجو" -#: ../melon/browse/server.py:102 +#: ../melon/browse/server.py:116 msgid "Try searching for a term" msgstr "" -#: ../melon/browse/server.py:104 +#: ../melon/browse/server.py:118 msgid "Try using a different query" msgstr "" -#: ../melon/browse/server.py:128 +#: ../melon/browse/server.py:144 msgid "This feed is empty" msgstr "" -#: ../melon/home/__init__.py:18 +#: ../melon/home/__init__.py:20 msgid "Home" msgstr "خانه" -#: ../melon/home/__init__.py:39 ../melon/home/history.py:26 -#: ../melon/home/new.py:36 ../melon/home/subs.py:23 +#: ../melon/home/__init__.py:41 ../melon/home/history.py:28 +#: ../melon/home/new.py:49 ../melon/home/subs.py:25 msgid "Browse Servers" msgstr "جستجو در کارساز ها" -#: ../melon/home/__init__.py:46 +#: ../melon/home/__init__.py:48 msgid "Preferences" msgstr "تنظیمات" -#: ../melon/home/__init__.py:47 +#: ../melon/home/__init__.py:49 msgid "Import Data" msgstr "وارد کردن دیتا" -#: ../melon/home/__init__.py:48 +#: ../melon/home/__init__.py:50 msgid "About Melon" msgstr "درباره Melon" -#: ../melon/home/history.py:24 +#: ../melon/home/history.py:26 msgid "You haven't watched any videos yet" msgstr "" -#: ../melon/home/history.py:36 ../melon/home/history.py:117 +#: ../melon/home/history.py:38 ../melon/home/history.py:125 msgid "History" msgstr "تاریخچه" -#: ../melon/home/history.py:37 +#: ../melon/home/history.py:39 msgid "These are the videos you opened in the past" msgstr "" -#: ../melon/home/history.py:76 +#: ../melon/home/history.py:83 msgid "Show more" msgstr "نمایش بیشتر" -#: ../melon/home/history.py:77 +#: ../melon/home/history.py:84 msgid "Load older videos" msgstr "دریافت ویدئوهای قدیمی‌تر" -#: ../melon/home/new.py:23 +#: ../melon/home/new.py:30 msgid "Refresh" msgstr "بازخوانی" -#: ../melon/home/new.py:32 +#: ../melon/home/new.py:40 msgid "Subscribe to a channel first, to view new uploads" msgstr "برای تماشای محتوای بارگذاری جدید، در کانال‌ها مشترک شوید." -#: ../melon/home/new.py:34 +#: ../melon/home/new.py:45 msgid "The channels you are subscribed to haven't uploaded anything yet" msgstr "کانالی که مشترک آن شدید هیچ محتوای بارگذاری شده ای ندارد" -#: ../melon/home/new.py:55 +#: ../melon/home/new.py:68 #, python-brace-format msgid "(Last refresh: {last_refresh})" msgstr "(آخرین بازخوانی: {last_refresh})" -#: ../melon/home/new.py:57 ../melon/home/new.py:146 +#: ../melon/home/new.py:72 ../melon/home/new.py:172 msgid "What's new" msgstr "چه چیزی جدید است؟" -#: ../melon/home/new.py:58 +#: ../melon/home/new.py:74 msgid "These are the latest videos of channels you follow" msgstr "" -#: ../melon/home/new.py:137 +#: ../melon/home/new.py:162 msgid "Pick up where you left off" msgstr "" -#: ../melon/home/new.py:138 +#: ../melon/home/new.py:163 msgid "Watch" msgstr "تماشا" -#: ../melon/home/playlists.py:22 +#: ../melon/home/playlists.py:24 msgid "You don't have any playlists yet" msgstr "شما هنوز هیچ سیاههٔ پخشی ندارید." -#: ../melon/home/playlists.py:24 ../melon/home/playlists.py:34 +#: ../melon/home/playlists.py:26 ../melon/home/playlists.py:39 msgid "Create a new playlist" msgstr "ایجاد یک لیست‌پخش جدید" -#: ../melon/home/playlists.py:33 +#: ../melon/home/playlists.py:36 msgid "Here are playlists you've bookmarked or created yourself" msgstr "اینها لیست پخش هایی هستند که توسط شما نشانه‌گذاری و یا ایجاد شده‌اند." -#: ../melon/home/playlists.py:34 +#: ../melon/home/playlists.py:39 msgid "New" msgstr "جدید" -#: ../melon/home/subs.py:21 +#: ../melon/home/subs.py:23 msgid "You aren't yet subscribed to channels" msgstr "شما مشترک هیچ کانالی نیستید" -#: ../melon/home/subs.py:31 ../melon/home/subs.py:63 +#: ../melon/home/subs.py:33 ../melon/home/subs.py:68 msgid "Subscriptions" msgstr "اشتراک‌ها" -#: ../melon/home/subs.py:32 +#: ../melon/home/subs.py:34 msgid "You are subscribed to the following channels" msgstr "شما در کانال‌های زیر مشترک هستید" -#: ../melon/import_providers/newpipe.py:28 +#: ../melon/import_providers/newpipe.py:42 msgid "Newpipe Database importer" msgstr "درون‌ریز پایگاه‌دادهٔ نیوپایپ" -#: ../melon/import_providers/newpipe.py:29 +#: ../melon/import_providers/newpipe.py:44 msgid "" "Import the .db file from inside the newpipe .zip export (as invidious " "content)" msgstr "" "درون‌ریزی پروندهٔ ‪.db‬ از داخل خروجی ‬‪.zip‬ نیوپایپ (به عنوان محتوای اینویدیوس)" -#: ../melon/import_providers/newpipe.py:30 +#: ../melon/import_providers/newpipe.py:46 msgid "Select .db file" msgstr "انتخاب فایل .db" -#: ../melon/import_providers/newpipe.py:45 +#: ../melon/import_providers/newpipe.py:60 msgid "Newpipe Database" msgstr "پایگاه‌دادهٔ نیوپایپ" -#: ../melon/importer.py:17 ../melon/importer.py:35 +#: ../melon/importer.py:19 ../melon/importer.py:37 msgid "Import" msgstr "واردکردن" -#: ../melon/importer.py:36 +#: ../melon/importer.py:38 msgid "The following import methods have been found" msgstr "روش های واردکردن پیدا شدند" -#: ../melon/importer.py:62 +#: ../melon/importer.py:64 msgid "There are no available importer methods" msgstr "" -#: ../melon/player/__init__.py:34 +#: ../melon/player/__init__.py:41 msgid "Description" msgstr "توضیحات" -#: ../melon/player/__init__.py:45 +#: ../melon/player/__init__.py:54 msgid "Add this video to a playlist" msgstr "افزودن این ویدئو به لیست‌پخش" -#: ../melon/player/__init__.py:103 +#: ../melon/player/__init__.py:113 msgid "Video could not be loaded" msgstr "" -#: ../melon/player/__init__.py:140 ../melon/player/__init__.py:174 -#: ../melon/player/playlist.py:51 +#: ../melon/player/__init__.py:151 ../melon/player/__init__.py:185 +#: ../melon/player/playlist.py:57 msgid "Loading..." msgstr "در حال بارکردن..." -#: ../melon/player/playlist.py:159 +#: ../melon/player/playlist.py:165 msgid "This playlist is empty" msgstr "" -#: ../melon/player/playlist.py:166 +#: ../melon/player/playlist.py:172 msgid "There was an error loading the playlist" msgstr "" -#: ../melon/player/playlist.py:218 +#: ../melon/player/playlist.py:225 msgid "Previous" msgstr "قبلی" -#: ../melon/player/playlist.py:219 +#: ../melon/player/playlist.py:227 msgid "Play video that comes before this one in the playlist" msgstr "" -#: ../melon/player/playlist.py:230 +#: ../melon/player/playlist.py:244 msgid "Next" msgstr "بعدی" -#: ../melon/player/playlist.py:231 +#: ../melon/player/playlist.py:246 msgid "Play video that comes after this one in the playlist" msgstr "ویدیو بعد از این را که در سیاههٔ‌پخش وجود دارد پخش کن." -#: ../melon/player/playlist.py:240 +#: ../melon/player/playlist.py:258 msgid "Skip" msgstr "ردکردن" -#: ../melon/player/playlist.py:241 +#: ../melon/player/playlist.py:259 msgid "Skip this video and pick a new one at random" msgstr "رد کردن این ویدئو و انتخاب یک عدد به صورت تصادفی" -#: ../melon/player/playlist.py:250 +#: ../melon/player/playlist.py:270 msgid "Shuffle" msgstr "مخلوط" -#: ../melon/player/playlist.py:251 +#: ../melon/player/playlist.py:271 msgid "Chooses the next video at random" msgstr "انتخاب ویدئو بعدی به صورت تصادفی" -#: ../melon/player/playlist.py:261 +#: ../melon/player/playlist.py:282 msgid "Repeat current video" msgstr "بازپخش این ویدئو" -#: ../melon/player/playlist.py:262 +#: ../melon/player/playlist.py:283 msgid "Puts this video on loop" msgstr "ویدئو را در حلقه قرار دهید." -#: ../melon/player/playlist.py:276 +#: ../melon/player/playlist.py:298 msgid "Repeat playlist" msgstr "بازپخش لیست‌پخش" -#: ../melon/player/playlist.py:277 +#: ../melon/player/playlist.py:300 msgid "Start playling the playlist from the beginning after reaching the end" msgstr "" -#: ../melon/player/playlist.py:287 +#: ../melon/player/playlist.py:312 msgid "Playlist Content" msgstr "محتوای سیاههٔ‌پخش" -#: ../melon/player/playlist.py:288 +#: ../melon/player/playlist.py:314 msgid "Click on videos to continue playing the playlist from there" msgstr "بر روی ویدئو ضربه بزنید تا پخش از لیست پخش از آن ویدئو ادامه پیدا کند." -#: ../melon/playlist/__init__.py:50 +#: ../melon/playlist/__init__.py:65 msgid "Edit" msgstr "ویرایش" -#: ../melon/playlist/__init__.py:62 +#: ../melon/playlist/__init__.py:79 msgid "You haven't added any videos to this playlist yet" msgstr "" -#: ../melon/playlist/__init__.py:64 +#: ../melon/playlist/__init__.py:82 msgid "Start watching" msgstr "شروع به تماشا کنید" -#: ../melon/playlist/__init__.py:87 +#: ../melon/playlist/__init__.py:106 msgid "Set as playlist thumbnail" msgstr "" -#: ../melon/playlist/__init__.py:88 +#: ../melon/playlist/__init__.py:114 msgid "Remove from playlist" msgstr "" -#: ../melon/playlist/__init__.py:99 +#: ../melon/playlist/__init__.py:131 msgid "Edit Playlist" msgstr "حذف لیست‌پخش" -#: ../melon/playlist/__init__.py:105 +#: ../melon/playlist/__init__.py:137 msgid "Playlist details" msgstr "جزئیات لیست‌پخش" -#: ../melon/playlist/__init__.py:106 +#: ../melon/playlist/__init__.py:138 msgid "Change playlist information" msgstr "تغییر اطلاعات لیست پخش" -#: ../melon/playlist/__init__.py:108 ../melon/playlist/create.py:37 +#: ../melon/playlist/__init__.py:140 ../melon/playlist/create.py:43 msgid "Playlist name" msgstr "نام لیست‌پخش" -#: ../melon/playlist/__init__.py:111 ../melon/playlist/create.py:40 +#: ../melon/playlist/__init__.py:143 ../melon/playlist/create.py:46 msgid "Playlist description" msgstr "اطلاعات سیاههٔ پخش" -#: ../melon/playlist/__init__.py:116 +#: ../melon/playlist/__init__.py:148 msgid "Save details" msgstr "ذخیره جزئیات" -#: ../melon/playlist/__init__.py:117 +#: ../melon/playlist/__init__.py:150 msgid "Change playlist title and description. (Closes the dialog)" msgstr "تغییر عنوان و توضیحات لیست پخش (این پنجره بسته می شود)" -#: ../melon/playlist/__init__.py:127 ../melon/playlist/__init__.py:169 +#: ../melon/playlist/__init__.py:161 ../melon/playlist/__init__.py:207 msgid "Delete Playlist" msgstr "حذف لیست پخش" -#: ../melon/playlist/__init__.py:128 +#: ../melon/playlist/__init__.py:163 msgid "Delete this playlist and it's content. This can NOT be undone." msgstr "این عملیات این لیست‌پخش و محتویات آن را حذف می‌کند و قابل بازگشت نیست." -#: ../melon/playlist/__init__.py:139 +#: ../melon/playlist/__init__.py:176 msgid "Close" msgstr "بستن" -#: ../melon/playlist/__init__.py:139 +#: ../melon/playlist/__init__.py:178 msgid "Close without changing anything" msgstr "بستن بدون تغییر دادن چیزی" -#: ../melon/playlist/__init__.py:172 +#: ../melon/playlist/__init__.py:210 msgid "Do you really want to delete this playlist?" msgstr "آیا از حذف این لیست‌پخش اطمینان دارید؟" -#: ../melon/playlist/__init__.py:173 +#: ../melon/playlist/__init__.py:211 msgid "This cannot be undone" msgstr "" -#: ../melon/playlist/__init__.py:177 ../melon/playlist/create.py:47 -#: ../melon/playlist/pick.py:70 ../melon/widgets/preferencerow.py:175 -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/playlist/__init__.py:216 ../melon/playlist/create.py:54 +#: ../melon/playlist/pick.py:88 ../melon/widgets/preferencerow.py:181 +#: ../melon/widgets/preferencerow.py:226 msgid "Cancel" msgstr "لغو" -#: ../melon/playlist/__init__.py:177 +#: ../melon/playlist/__init__.py:218 msgid "Do not delete the playlist" msgstr "لیست‌پخش را حذف نکن." -#: ../melon/playlist/__init__.py:178 ../melon/widgets/preferencerow.py:207 -#: ../melon/widgets/preferencerow.py:219 +#: ../melon/playlist/__init__.py:221 ../melon/widgets/preferencerow.py:214 +#: ../melon/widgets/preferencerow.py:229 msgid "Delete" msgstr "حذف" -#: ../melon/playlist/__init__.py:178 +#: ../melon/playlist/__init__.py:221 msgid "Delete this playlist" msgstr "حذف این لیست‌پخش" -#: ../melon/playlist/create.py:23 ../melon/playlist/pick.py:28 +#: ../melon/playlist/create.py:25 ../melon/playlist/pick.py:35 msgid "Video" msgstr "ویدیو" -#: ../melon/playlist/create.py:24 +#: ../melon/playlist/create.py:27 msgid "The following video will be added to the new playlist" msgstr "" -#: ../melon/playlist/create.py:33 ../melon/playlist/create.py:91 +#: ../melon/playlist/create.py:39 ../melon/playlist/create.py:98 msgid "New Playlist" msgstr "سیاههٔ پخش جدید" -#: ../melon/playlist/create.py:34 +#: ../melon/playlist/create.py:40 msgid "Enter more playlist information" msgstr "وارد کردن توضیحات بیشتر برای لیست‌پخش" -#: ../melon/playlist/create.py:38 +#: ../melon/playlist/create.py:44 msgid "Unnamed Playlist" msgstr "سیاههٔ پخش بدون نام" -#: ../melon/playlist/create.py:47 ../melon/playlist/pick.py:70 +#: ../melon/playlist/create.py:54 ../melon/playlist/pick.py:88 msgid "Do not create playlist" msgstr "لیست پخش را ایجاد نکن." -#: ../melon/playlist/create.py:48 ../melon/widgets/preferencerow.py:176 +#: ../melon/playlist/create.py:57 ../melon/widgets/preferencerow.py:184 msgid "Create" msgstr "ایجاد" -#: ../melon/playlist/create.py:48 +#: ../melon/playlist/create.py:57 msgid "Create playlist" msgstr "ایجاد لیست‌پخش" -#: ../melon/playlist/pick.py:29 +#: ../melon/playlist/pick.py:37 msgid "The following video will be added to the playlist" msgstr "" -#: ../melon/playlist/pick.py:39 ../melon/widgets/feeditem.py:122 +#: ../melon/playlist/pick.py:50 ../melon/widgets/feeditem.py:145 msgid "Add to playlist" msgstr "افزودن به لیست‌پخش" -#: ../melon/playlist/pick.py:40 +#: ../melon/playlist/pick.py:53 msgid "" "Choose a playlist to add the video to. Note that you can only add videos to " "local playlists, not external bookmarked ones" @@ -571,73 +571,73 @@ msgstr "" "نکته: شما تنها می‌توانید ویدئو را به لیست پخش های محلی اضافه کنید، نه به " "نشانک گذاری شده های خارجی." -#: ../melon/playlist/pick.py:42 +#: ../melon/playlist/pick.py:57 msgid "Create new playlist" msgstr "ایجاد یک لیست‌پخش جدید" -#: ../melon/playlist/pick.py:43 +#: ../melon/playlist/pick.py:58 msgid "Create a new playlist and add the video to it" msgstr "ایجاد یک لیست‌پخش جدید و افزودن ویدئو به آن" -#: ../melon/playlist/pick.py:102 +#: ../melon/playlist/pick.py:119 msgid "Add to Playlist" msgstr "افزودن به لیست‌پخش" -#: ../melon/servers/invidious/__init__.py:31 +#: ../melon/servers/invidious/__init__.py:35 msgid "Open source alternative front-end to YouTube" msgstr "" -#: ../melon/servers/invidious/__init__.py:36 +#: ../melon/servers/invidious/__init__.py:40 msgid "Instance" msgstr "نمونه" -#: ../melon/servers/invidious/__init__.py:37 +#: ../melon/servers/invidious/__init__.py:42 msgid "" "See https://docs.invidious.io/instances/ for a list of available instances" msgstr "" "برای مشاهده لیستی از کارساز های موجود به https://docs.invidious.io/" "instances/ مراجعه کنید" -#: ../melon/servers/invidious/__init__.py:48 +#: ../melon/servers/invidious/__init__.py:55 msgid "Trending" msgstr "" -#: ../melon/servers/invidious/__init__.py:49 +#: ../melon/servers/invidious/__init__.py:56 msgid "Popular" msgstr "معروف" -#: ../melon/servers/nebula/__init__.py:19 +#: ../melon/servers/nebula/__init__.py:21 msgid "" "Home of smart, thoughtful videos, podcasts, and classes from your favorite " "creators" msgstr "" "خانه ویدیوها، پادکست‌ها و کلاس‌های هوشمند و متفکر از سازندگان مورد علاقه‌تان" -#: ../melon/servers/nebula/__init__.py:24 +#: ../melon/servers/nebula/__init__.py:27 msgid "Email Address" msgstr "آدرس رایانامه" -#: ../melon/servers/nebula/__init__.py:25 +#: ../melon/servers/nebula/__init__.py:28 msgid "Email Address to login to your account" msgstr "آدرس رایانامه برای ورود به حساب کاربری" -#: ../melon/servers/nebula/__init__.py:31 +#: ../melon/servers/nebula/__init__.py:35 msgid "Password" msgstr "" -#: ../melon/servers/nebula/__init__.py:32 +#: ../melon/servers/nebula/__init__.py:36 msgid "Password associated with your account" msgstr "" -#: ../melon/servers/peertube/__init__.py:15 +#: ../melon/servers/peertube/__init__.py:16 msgid "Decentralized video hosting network, based on free/libre software" msgstr "شبکه میزبانی ویدیو غیرمتمرکز، بر اساس نرم افزار رایگان/آزاد" -#: ../melon/servers/peertube/__init__.py:20 +#: ../melon/servers/peertube/__init__.py:21 msgid "Instances" msgstr "نمونه‌ها" -#: ../melon/servers/peertube/__init__.py:21 +#: ../melon/servers/peertube/__init__.py:23 msgid "" "List of peertube instances, from which to fetch content. See https://" "joinpeertube.org/instances" @@ -645,31 +645,31 @@ msgstr "" "فهرست نمونه‌های پیرتیوب که محتوا از آن‌ها واکشی می‌شود. این نشانی را ببینید: " "https://joinpeertube.org/instances" -#: ../melon/servers/peertube/__init__.py:27 +#: ../melon/servers/peertube/__init__.py:31 msgid "Show NSFW content" msgstr "نمایش محتوای nsfw" -#: ../melon/servers/peertube/__init__.py:28 +#: ../melon/servers/peertube/__init__.py:32 msgid "Passes the nsfw filter to the peertube search API" msgstr "" -#: ../melon/servers/peertube/__init__.py:33 +#: ../melon/servers/peertube/__init__.py:39 msgid "Enable Federation" msgstr "فعال کردن فدریشن" -#: ../melon/servers/peertube/__init__.py:34 +#: ../melon/servers/peertube/__init__.py:40 msgid "Returns content from federated instances instead of only local content" msgstr "محتوا را از کارساز های فدریتد بجای محلی نشان می‌دهد." -#: ../melon/servers/peertube/__init__.py:58 +#: ../melon/servers/peertube/__init__.py:65 msgid "Latest" msgstr "آخرین" -#: ../melon/settings/__init__.py:18 +#: ../melon/settings/__init__.py:23 msgid "Show Previews when browsing public feeds (incl. search)" msgstr "نمایش پیش نمایش ها هنگام مرور خوراک‌های عمومی (از جمله جستجو)" -#: ../melon/settings/__init__.py:19 +#: ../melon/settings/__init__.py:25 msgid "" "Set to true to show previews when viewing channel contents, public feeds and " "searching" @@ -677,11 +677,11 @@ msgstr "" "برای نمایش پیش نمایش ها هنگام مشاهده محتوای کانال، فیدهای عمومی و جستجو، روی " "بله تنظیم کنید" -#: ../melon/settings/__init__.py:25 +#: ../melon/settings/__init__.py:34 msgid "Show Previews in local feeds" msgstr "نمایش پیشنمایش ها در خوراک های محلی" -#: ../melon/settings/__init__.py:26 +#: ../melon/settings/__init__.py:36 msgid "" "Set to true to show previews in the new feed, for subscribed channels, and " "saved playlists" @@ -689,156 +689,156 @@ msgstr "" "برای نمایش پیش‌نمایش‌ها در فید جدید، برای کانال‌های مشترک و لیست‌های پخش " "ذخیره‌شده، روی بله تنظیم کنید" -#: ../melon/settings/__init__.py:32 +#: ../melon/settings/__init__.py:44 msgid "Show servers that may contain nsfw content" msgstr "نمایش کارساز هایی که ممکن است دارای محتوای NSFW باشد" -#: ../melon/settings/__init__.py:33 +#: ../melon/settings/__init__.py:46 msgid "" "Lists/Delists servers in the browse servers list, if they contain some nsfw " "content" msgstr "" -#: ../melon/settings/__init__.py:38 +#: ../melon/settings/__init__.py:54 msgid "Show servers that only contain nsfw content" msgstr "نمایش کارساز هایی که تنها دارای محتوای NSFW هستند" -#: ../melon/settings/__init__.py:39 +#: ../melon/settings/__init__.py:56 msgid "" "Lists/Delists servers in the browse servers list, if they contain only/" "mostly nsfw content" msgstr "" -#: ../melon/settings/__init__.py:45 +#: ../melon/settings/__init__.py:64 msgid "Show servers that require login" msgstr "نمایش کارساز هایی که به ورود احتیاج دارند" -#: ../melon/settings/__init__.py:46 +#: ../melon/settings/__init__.py:66 msgid "" "Lists/Delists servers in the browse servers list, if they require login to " "function" msgstr "" -#: ../melon/settings/__init__.py:55 +#: ../melon/settings/__init__.py:78 msgid "Settings" msgstr "تنظیمات" -#: ../melon/settings/__init__.py:68 +#: ../melon/settings/__init__.py:91 msgid "General" msgstr "کلی" -#: ../melon/settings/__init__.py:69 +#: ../melon/settings/__init__.py:92 msgid "Global app settings" msgstr "تنظیمات کلی کاره" -#: ../melon/settings/__init__.py:93 +#: ../melon/settings/__init__.py:116 msgid "Enable Server" msgstr "فعال کردن کارساز" -#: ../melon/settings/__init__.py:94 +#: ../melon/settings/__init__.py:118 msgid "" "Disabled servers won't show up in the browser or on the local/home screen" msgstr "" "کارساز های غیرفعال شده دیگر به شما در بخش محلی و صفحه‌خانه نشان داده نمی‌شوند." -#: ../melon/widgets/feeditem.py:121 +#: ../melon/widgets/feeditem.py:139 msgid "Watch now" msgstr "" -#: ../melon/widgets/feeditem.py:123 +#: ../melon/widgets/feeditem.py:152 msgid "Open in browser" msgstr "" -#: ../melon/widgets/feeditem.py:124 +#: ../melon/widgets/feeditem.py:156 msgid "View channel" msgstr "" -#: ../melon/widgets/player.py:119 +#: ../melon/widgets/player.py:136 msgid "No streams available" msgstr "جریان پخشی موجود نیست" -#: ../melon/widgets/player.py:180 +#: ../melon/widgets/player.py:198 msgid "Toggle floating window" msgstr "" -#: ../melon/widgets/player.py:184 +#: ../melon/widgets/player.py:204 msgid "Toggle fullscreen" msgstr "" -#: ../melon/widgets/player.py:204 ../melon/widgets/player.py:565 -#: ../melon/widgets/player.py:571 +#: ../melon/widgets/player.py:226 ../melon/widgets/player.py:613 +#: ../melon/widgets/player.py:622 msgid "Play" msgstr "" -#: ../melon/widgets/player.py:230 +#: ../melon/widgets/player.py:253 msgid "Stream options" msgstr "تنظیمات استریم" -#: ../melon/widgets/player.py:442 +#: ../melon/widgets/player.py:483 msgid "Resolution" msgstr "وضوح" -#: ../melon/widgets/player.py:559 +#: ../melon/widgets/player.py:604 msgid "Pause" msgstr "" -#: ../melon/widgets/player.py:589 +#: ../melon/widgets/player.py:642 msgid "The video is playing in separate window" msgstr "" -#: ../melon/widgets/player.py:710 +#: ../melon/widgets/player.py:779 msgid "Player" msgstr "پخش‌کننده" -#: ../melon/widgets/preferencerow.py:116 +#: ../melon/widgets/preferencerow.py:125 msgid "Add" msgstr "افزودن" -#: ../melon/widgets/preferencerow.py:130 +#: ../melon/widgets/preferencerow.py:139 msgid "Move up" msgstr "انتقال به بالا" -#: ../melon/widgets/preferencerow.py:141 +#: ../melon/widgets/preferencerow.py:147 msgid "Move down" msgstr "انتقال به پایین" -#: ../melon/widgets/preferencerow.py:150 +#: ../melon/widgets/preferencerow.py:153 msgid "Remove from list" msgstr "حذف از لیست" -#: ../melon/widgets/preferencerow.py:163 +#: ../melon/widgets/preferencerow.py:168 msgid "Add Item" msgstr "افزودن مورد" -#: ../melon/widgets/preferencerow.py:166 +#: ../melon/widgets/preferencerow.py:171 msgid "Create a new list entry" msgstr "ایجاد یک ورودی لیست جدید" -#: ../melon/widgets/preferencerow.py:167 +#: ../melon/widgets/preferencerow.py:172 msgid "Enter the new value here" msgstr "مقادیر را اینجا وارد کنید." -#: ../melon/widgets/preferencerow.py:170 +#: ../melon/widgets/preferencerow.py:175 msgid "Value" msgstr "مقدار" -#: ../melon/widgets/preferencerow.py:210 +#: ../melon/widgets/preferencerow.py:217 msgid "Do you really want to delete this item?" msgstr "آیا از حذف این مورد اطمینان دارید؟" -#: ../melon/widgets/preferencerow.py:211 +#: ../melon/widgets/preferencerow.py:218 msgid "You won't be able to restore it afterwards" msgstr "" -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/widgets/preferencerow.py:226 msgid "Do not remove item" msgstr "این مورد را حذف نکن." -#: ../melon/widgets/preferencerow.py:219 +#: ../melon/widgets/preferencerow.py:229 msgid "Remove item from list" msgstr "حذف این مورد از لیست" -#: ../melon/window.py:84 +#: ../melon/window.py:94 msgid "Stream videos on the go" msgstr "ویدیو‌هارا استریم کن" diff --git a/po/melon.pot b/po/melon.pot index d42cd47..534ae13 100644 --- a/po/melon.pot +++ b/po/melon.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Melon 0.2.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-12 21:13+0200\n" +"POT-Creation-Date: 2024-07-15 07:54+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -79,737 +79,737 @@ msgid "" "button" 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:29 -#: ../melon/home/playlists.py:21 ../melon/home/subs.py:20 -#: ../melon/importer.py:61 ../melon/player/__init__.py:102 -#: ../melon/player/playlist.py:158 ../melon/player/playlist.py:165 -#: ../melon/playlist/__init__.py:61 +#: ../melon/browse/__init__.py:20 ../melon/browse/search.py:66 +#: ../melon/browse/server.py:115 ../melon/browse/server.py:143 +#: ../melon/home/history.py:25 ../melon/home/new.py:36 +#: ../melon/home/playlists.py:23 ../melon/home/subs.py:22 +#: ../melon/importer.py:63 ../melon/player/__init__.py:112 +#: ../melon/player/playlist.py:164 ../melon/player/playlist.py:171 +#: ../melon/playlist/__init__.py:77 msgid "*crickets chirping*" msgstr "" -#: ../melon/browse/__init__.py:19 +#: ../melon/browse/__init__.py:21 msgid "There are no available servers" msgstr "" -#: ../melon/browse/__init__.py:21 ../melon/browse/search.py:61 -#: ../melon/importer.py:64 +#: ../melon/browse/__init__.py:24 ../melon/browse/search.py:72 +#: ../melon/importer.py:67 msgid "Enable servers in the settings menu" msgstr "" -#: ../melon/browse/__init__.py:29 +#: ../melon/browse/__init__.py:33 msgid "Available Servers" msgstr "" -#: ../melon/browse/__init__.py:30 +#: ../melon/browse/__init__.py:35 msgid "You can enable/disable and filter servers in the settings menu" msgstr "" -#: ../melon/browse/__init__.py:48 +#: ../melon/browse/__init__.py:56 msgid "Servers" msgstr "" -#: ../melon/browse/__init__.py:56 ../melon/browse/search.py:106 +#: ../melon/browse/__init__.py:64 ../melon/browse/search.py:126 msgid "Global Search" msgstr "" -#: ../melon/browse/channel.py:93 +#: ../melon/browse/channel.py:104 msgid "Subscribe to channel" msgstr "" -#: ../melon/browse/channel.py:94 +#: ../melon/browse/channel.py:105 msgid "Add latest uploads to home feed" msgstr "" -#: ../melon/browse/channel.py:106 +#: ../melon/browse/channel.py:117 msgid "Channel feed" msgstr "" -#: ../melon/browse/channel.py:107 +#: ../melon/browse/channel.py:118 msgid "This channel provides multiple feeds, choose which one to view" msgstr "" -#: ../melon/browse/channel.py:159 +#: ../melon/browse/channel.py:170 msgid "Channel" msgstr "" -#: ../melon/browse/playlist.py:37 ../melon/playlist/__init__.py:53 +#: ../melon/browse/playlist.py:48 ../melon/playlist/__init__.py:69 msgid "Start playing" msgstr "" -#: ../melon/browse/playlist.py:56 ../melon/player/__init__.py:44 +#: ../melon/browse/playlist.py:69 ../melon/player/__init__.py:53 msgid "Bookmark" msgstr "" -#: ../melon/browse/playlist.py:57 +#: ../melon/browse/playlist.py:70 msgid "Add Playlist to your local playlist collection" msgstr "" -#: ../melon/browse/playlist.py:111 +#: ../melon/browse/playlist.py:124 msgid "Playlist" msgstr "" -#: ../melon/browse/search.py:44 +#: ../melon/browse/search.py:49 msgid "No results" msgstr "" -#: ../melon/browse/search.py:46 +#: ../melon/browse/search.py:52 #, python-brace-format msgid "{count} result" msgid_plural "{count} results" msgstr[0] "" msgstr[1] "" -#: ../melon/browse/search.py:59 +#: ../melon/browse/search.py:68 msgid "There are no available servers, a search would yield no results" msgstr "" -#: ../melon/browse/search.py:83 ../melon/browse/server.py:43 +#: ../melon/browse/search.py:96 ../melon/browse/server.py:47 msgid "Any" msgstr "" -#: ../melon/browse/search.py:85 ../melon/browse/server.py:45 +#: ../melon/browse/search.py:100 ../melon/browse/server.py:51 msgid "Channels" msgstr "" -#: ../melon/browse/search.py:88 ../melon/browse/server.py:48 -#: ../melon/home/playlists.py:32 ../melon/home/playlists.py:64 -#: ../melon/servers/nebula/__init__.py:194 -#: ../melon/servers/peertube/__init__.py:205 +#: ../melon/browse/search.py:105 ../melon/browse/server.py:56 +#: ../melon/home/playlists.py:34 ../melon/home/playlists.py:78 +#: ../melon/servers/nebula/__init__.py:206 +#: ../melon/servers/peertube/__init__.py:213 msgid "Playlists" msgstr "" -#: ../melon/browse/search.py:91 ../melon/browse/server.py:51 -#: ../melon/servers/nebula/__init__.py:188 -#: ../melon/servers/peertube/__init__.py:204 -#: ../melon/servers/peertube/__init__.py:208 +#: ../melon/browse/search.py:110 ../melon/browse/server.py:61 +#: ../melon/servers/nebula/__init__.py:200 +#: ../melon/servers/peertube/__init__.py:213 +#: ../melon/servers/peertube/__init__.py:214 msgid "Videos" msgstr "" -#: ../melon/browse/server.py:23 +#: ../melon/browse/server.py:26 msgid "Search" msgstr "" -#: ../melon/browse/server.py:102 +#: ../melon/browse/server.py:116 msgid "Try searching for a term" msgstr "" -#: ../melon/browse/server.py:104 +#: ../melon/browse/server.py:118 msgid "Try using a different query" msgstr "" -#: ../melon/browse/server.py:128 +#: ../melon/browse/server.py:144 msgid "This feed is empty" msgstr "" -#: ../melon/home/__init__.py:18 +#: ../melon/home/__init__.py:20 msgid "Home" msgstr "" -#: ../melon/home/__init__.py:39 ../melon/home/history.py:26 -#: ../melon/home/new.py:36 ../melon/home/subs.py:23 +#: ../melon/home/__init__.py:41 ../melon/home/history.py:28 +#: ../melon/home/new.py:49 ../melon/home/subs.py:25 msgid "Browse Servers" msgstr "" -#: ../melon/home/__init__.py:46 +#: ../melon/home/__init__.py:48 msgid "Preferences" msgstr "" -#: ../melon/home/__init__.py:47 +#: ../melon/home/__init__.py:49 msgid "Import Data" msgstr "" -#: ../melon/home/__init__.py:48 +#: ../melon/home/__init__.py:50 msgid "About Melon" msgstr "" -#: ../melon/home/history.py:24 +#: ../melon/home/history.py:26 msgid "You haven't watched any videos yet" msgstr "" -#: ../melon/home/history.py:36 ../melon/home/history.py:117 +#: ../melon/home/history.py:38 ../melon/home/history.py:125 msgid "History" msgstr "" -#: ../melon/home/history.py:37 +#: ../melon/home/history.py:39 msgid "These are the videos you opened in the past" msgstr "" -#: ../melon/home/history.py:76 +#: ../melon/home/history.py:83 msgid "Show more" msgstr "" -#: ../melon/home/history.py:77 +#: ../melon/home/history.py:84 msgid "Load older videos" msgstr "" -#: ../melon/home/new.py:23 +#: ../melon/home/new.py:30 msgid "Refresh" msgstr "" -#: ../melon/home/new.py:32 +#: ../melon/home/new.py:40 msgid "Subscribe to a channel first, to view new uploads" msgstr "" -#: ../melon/home/new.py:34 +#: ../melon/home/new.py:45 msgid "The channels you are subscribed to haven't uploaded anything yet" msgstr "" -#: ../melon/home/new.py:55 +#: ../melon/home/new.py:68 #, python-brace-format msgid "(Last refresh: {last_refresh})" msgstr "" -#: ../melon/home/new.py:57 ../melon/home/new.py:146 +#: ../melon/home/new.py:72 ../melon/home/new.py:172 msgid "What's new" msgstr "" -#: ../melon/home/new.py:58 +#: ../melon/home/new.py:74 msgid "These are the latest videos of channels you follow" msgstr "" -#: ../melon/home/new.py:137 +#: ../melon/home/new.py:162 msgid "Pick up where you left off" msgstr "" -#: ../melon/home/new.py:138 +#: ../melon/home/new.py:163 msgid "Watch" msgstr "" -#: ../melon/home/playlists.py:22 +#: ../melon/home/playlists.py:24 msgid "You don't have any playlists yet" msgstr "" -#: ../melon/home/playlists.py:24 ../melon/home/playlists.py:34 +#: ../melon/home/playlists.py:26 ../melon/home/playlists.py:39 msgid "Create a new playlist" msgstr "" -#: ../melon/home/playlists.py:33 +#: ../melon/home/playlists.py:36 msgid "Here are playlists you've bookmarked or created yourself" msgstr "" -#: ../melon/home/playlists.py:34 +#: ../melon/home/playlists.py:39 msgid "New" msgstr "" -#: ../melon/home/subs.py:21 +#: ../melon/home/subs.py:23 msgid "You aren't yet subscribed to channels" msgstr "" -#: ../melon/home/subs.py:31 ../melon/home/subs.py:63 +#: ../melon/home/subs.py:33 ../melon/home/subs.py:68 msgid "Subscriptions" msgstr "" -#: ../melon/home/subs.py:32 +#: ../melon/home/subs.py:34 msgid "You are subscribed to the following channels" msgstr "" -#: ../melon/import_providers/newpipe.py:28 +#: ../melon/import_providers/newpipe.py:42 msgid "Newpipe Database importer" msgstr "" -#: ../melon/import_providers/newpipe.py:29 +#: ../melon/import_providers/newpipe.py:44 msgid "" "Import the .db file from inside the newpipe .zip export (as invidious " "content)" msgstr "" -#: ../melon/import_providers/newpipe.py:30 +#: ../melon/import_providers/newpipe.py:46 msgid "Select .db file" msgstr "" -#: ../melon/import_providers/newpipe.py:45 +#: ../melon/import_providers/newpipe.py:60 msgid "Newpipe Database" msgstr "" -#: ../melon/importer.py:17 ../melon/importer.py:35 +#: ../melon/importer.py:19 ../melon/importer.py:37 msgid "Import" msgstr "" -#: ../melon/importer.py:36 +#: ../melon/importer.py:38 msgid "The following import methods have been found" msgstr "" -#: ../melon/importer.py:62 +#: ../melon/importer.py:64 msgid "There are no available importer methods" msgstr "" -#: ../melon/player/__init__.py:34 +#: ../melon/player/__init__.py:41 msgid "Description" msgstr "" -#: ../melon/player/__init__.py:45 +#: ../melon/player/__init__.py:54 msgid "Add this video to a playlist" msgstr "" -#: ../melon/player/__init__.py:103 +#: ../melon/player/__init__.py:113 msgid "Video could not be loaded" msgstr "" -#: ../melon/player/__init__.py:140 ../melon/player/__init__.py:174 -#: ../melon/player/playlist.py:51 +#: ../melon/player/__init__.py:151 ../melon/player/__init__.py:185 +#: ../melon/player/playlist.py:57 msgid "Loading..." msgstr "" -#: ../melon/player/playlist.py:159 +#: ../melon/player/playlist.py:165 msgid "This playlist is empty" msgstr "" -#: ../melon/player/playlist.py:166 +#: ../melon/player/playlist.py:172 msgid "There was an error loading the playlist" msgstr "" -#: ../melon/player/playlist.py:218 +#: ../melon/player/playlist.py:225 msgid "Previous" msgstr "" -#: ../melon/player/playlist.py:219 +#: ../melon/player/playlist.py:227 msgid "Play video that comes before this one in the playlist" msgstr "" -#: ../melon/player/playlist.py:230 +#: ../melon/player/playlist.py:244 msgid "Next" msgstr "" -#: ../melon/player/playlist.py:231 +#: ../melon/player/playlist.py:246 msgid "Play video that comes after this one in the playlist" msgstr "" -#: ../melon/player/playlist.py:240 +#: ../melon/player/playlist.py:258 msgid "Skip" msgstr "" -#: ../melon/player/playlist.py:241 +#: ../melon/player/playlist.py:259 msgid "Skip this video and pick a new one at random" msgstr "" -#: ../melon/player/playlist.py:250 +#: ../melon/player/playlist.py:270 msgid "Shuffle" msgstr "" -#: ../melon/player/playlist.py:251 +#: ../melon/player/playlist.py:271 msgid "Chooses the next video at random" msgstr "" -#: ../melon/player/playlist.py:261 +#: ../melon/player/playlist.py:282 msgid "Repeat current video" msgstr "" -#: ../melon/player/playlist.py:262 +#: ../melon/player/playlist.py:283 msgid "Puts this video on loop" msgstr "" -#: ../melon/player/playlist.py:276 +#: ../melon/player/playlist.py:298 msgid "Repeat playlist" msgstr "" -#: ../melon/player/playlist.py:277 +#: ../melon/player/playlist.py:300 msgid "Start playling the playlist from the beginning after reaching the end" msgstr "" -#: ../melon/player/playlist.py:287 +#: ../melon/player/playlist.py:312 msgid "Playlist Content" msgstr "" -#: ../melon/player/playlist.py:288 +#: ../melon/player/playlist.py:314 msgid "Click on videos to continue playing the playlist from there" msgstr "" -#: ../melon/playlist/__init__.py:50 +#: ../melon/playlist/__init__.py:65 msgid "Edit" msgstr "" -#: ../melon/playlist/__init__.py:62 +#: ../melon/playlist/__init__.py:79 msgid "You haven't added any videos to this playlist yet" msgstr "" -#: ../melon/playlist/__init__.py:64 +#: ../melon/playlist/__init__.py:82 msgid "Start watching" msgstr "" -#: ../melon/playlist/__init__.py:87 +#: ../melon/playlist/__init__.py:106 msgid "Set as playlist thumbnail" msgstr "" -#: ../melon/playlist/__init__.py:88 +#: ../melon/playlist/__init__.py:114 msgid "Remove from playlist" msgstr "" -#: ../melon/playlist/__init__.py:99 +#: ../melon/playlist/__init__.py:131 msgid "Edit Playlist" msgstr "" -#: ../melon/playlist/__init__.py:105 +#: ../melon/playlist/__init__.py:137 msgid "Playlist details" msgstr "" -#: ../melon/playlist/__init__.py:106 +#: ../melon/playlist/__init__.py:138 msgid "Change playlist information" msgstr "" -#: ../melon/playlist/__init__.py:108 ../melon/playlist/create.py:37 +#: ../melon/playlist/__init__.py:140 ../melon/playlist/create.py:43 msgid "Playlist name" msgstr "" -#: ../melon/playlist/__init__.py:111 ../melon/playlist/create.py:40 +#: ../melon/playlist/__init__.py:143 ../melon/playlist/create.py:46 msgid "Playlist description" msgstr "" -#: ../melon/playlist/__init__.py:116 +#: ../melon/playlist/__init__.py:148 msgid "Save details" msgstr "" -#: ../melon/playlist/__init__.py:117 +#: ../melon/playlist/__init__.py:150 msgid "Change playlist title and description. (Closes the dialog)" msgstr "" -#: ../melon/playlist/__init__.py:127 ../melon/playlist/__init__.py:169 +#: ../melon/playlist/__init__.py:161 ../melon/playlist/__init__.py:207 msgid "Delete Playlist" msgstr "" -#: ../melon/playlist/__init__.py:128 +#: ../melon/playlist/__init__.py:163 msgid "Delete this playlist and it's content. This can NOT be undone." msgstr "" -#: ../melon/playlist/__init__.py:139 +#: ../melon/playlist/__init__.py:176 msgid "Close" msgstr "" -#: ../melon/playlist/__init__.py:139 +#: ../melon/playlist/__init__.py:178 msgid "Close without changing anything" msgstr "" -#: ../melon/playlist/__init__.py:172 +#: ../melon/playlist/__init__.py:210 msgid "Do you really want to delete this playlist?" msgstr "" -#: ../melon/playlist/__init__.py:173 +#: ../melon/playlist/__init__.py:211 msgid "This cannot be undone" msgstr "" -#: ../melon/playlist/__init__.py:177 ../melon/playlist/create.py:47 -#: ../melon/playlist/pick.py:70 ../melon/widgets/preferencerow.py:175 -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/playlist/__init__.py:216 ../melon/playlist/create.py:54 +#: ../melon/playlist/pick.py:88 ../melon/widgets/preferencerow.py:181 +#: ../melon/widgets/preferencerow.py:226 msgid "Cancel" msgstr "" -#: ../melon/playlist/__init__.py:177 +#: ../melon/playlist/__init__.py:218 msgid "Do not delete the playlist" msgstr "" -#: ../melon/playlist/__init__.py:178 ../melon/widgets/preferencerow.py:207 -#: ../melon/widgets/preferencerow.py:219 +#: ../melon/playlist/__init__.py:221 ../melon/widgets/preferencerow.py:214 +#: ../melon/widgets/preferencerow.py:229 msgid "Delete" msgstr "" -#: ../melon/playlist/__init__.py:178 +#: ../melon/playlist/__init__.py:221 msgid "Delete this playlist" msgstr "" -#: ../melon/playlist/create.py:23 ../melon/playlist/pick.py:28 +#: ../melon/playlist/create.py:25 ../melon/playlist/pick.py:35 msgid "Video" msgstr "" -#: ../melon/playlist/create.py:24 +#: ../melon/playlist/create.py:27 msgid "The following video will be added to the new playlist" msgstr "" -#: ../melon/playlist/create.py:33 ../melon/playlist/create.py:91 +#: ../melon/playlist/create.py:39 ../melon/playlist/create.py:98 msgid "New Playlist" msgstr "" -#: ../melon/playlist/create.py:34 +#: ../melon/playlist/create.py:40 msgid "Enter more playlist information" msgstr "" -#: ../melon/playlist/create.py:38 +#: ../melon/playlist/create.py:44 msgid "Unnamed Playlist" msgstr "" -#: ../melon/playlist/create.py:47 ../melon/playlist/pick.py:70 +#: ../melon/playlist/create.py:54 ../melon/playlist/pick.py:88 msgid "Do not create playlist" msgstr "" -#: ../melon/playlist/create.py:48 ../melon/widgets/preferencerow.py:176 +#: ../melon/playlist/create.py:57 ../melon/widgets/preferencerow.py:184 msgid "Create" msgstr "" -#: ../melon/playlist/create.py:48 +#: ../melon/playlist/create.py:57 msgid "Create playlist" msgstr "" -#: ../melon/playlist/pick.py:29 +#: ../melon/playlist/pick.py:37 msgid "The following video will be added to the playlist" msgstr "" -#: ../melon/playlist/pick.py:39 ../melon/widgets/feeditem.py:122 +#: ../melon/playlist/pick.py:50 ../melon/widgets/feeditem.py:145 msgid "Add to playlist" msgstr "" -#: ../melon/playlist/pick.py:40 +#: ../melon/playlist/pick.py:53 msgid "" "Choose a playlist to add the video to. Note that you can only add videos to " "local playlists, not external bookmarked ones" msgstr "" -#: ../melon/playlist/pick.py:42 +#: ../melon/playlist/pick.py:57 msgid "Create new playlist" msgstr "" -#: ../melon/playlist/pick.py:43 +#: ../melon/playlist/pick.py:58 msgid "Create a new playlist and add the video to it" msgstr "" -#: ../melon/playlist/pick.py:102 +#: ../melon/playlist/pick.py:119 msgid "Add to Playlist" msgstr "" -#: ../melon/servers/invidious/__init__.py:31 +#: ../melon/servers/invidious/__init__.py:35 msgid "Open source alternative front-end to YouTube" msgstr "" -#: ../melon/servers/invidious/__init__.py:36 +#: ../melon/servers/invidious/__init__.py:40 msgid "Instance" msgstr "" -#: ../melon/servers/invidious/__init__.py:37 +#: ../melon/servers/invidious/__init__.py:42 msgid "" "See https://docs.invidious.io/instances/ for a list of available instances" msgstr "" -#: ../melon/servers/invidious/__init__.py:48 +#: ../melon/servers/invidious/__init__.py:55 msgid "Trending" msgstr "" -#: ../melon/servers/invidious/__init__.py:49 +#: ../melon/servers/invidious/__init__.py:56 msgid "Popular" msgstr "" -#: ../melon/servers/nebula/__init__.py:19 +#: ../melon/servers/nebula/__init__.py:21 msgid "" "Home of smart, thoughtful videos, podcasts, and classes from your favorite " "creators" msgstr "" -#: ../melon/servers/nebula/__init__.py:24 +#: ../melon/servers/nebula/__init__.py:27 msgid "Email Address" msgstr "" -#: ../melon/servers/nebula/__init__.py:25 +#: ../melon/servers/nebula/__init__.py:28 msgid "Email Address to login to your account" msgstr "" -#: ../melon/servers/nebula/__init__.py:31 +#: ../melon/servers/nebula/__init__.py:35 msgid "Password" msgstr "" -#: ../melon/servers/nebula/__init__.py:32 +#: ../melon/servers/nebula/__init__.py:36 msgid "Password associated with your account" msgstr "" -#: ../melon/servers/peertube/__init__.py:15 +#: ../melon/servers/peertube/__init__.py:16 msgid "Decentralized video hosting network, based on free/libre software" msgstr "" -#: ../melon/servers/peertube/__init__.py:20 +#: ../melon/servers/peertube/__init__.py:21 msgid "Instances" msgstr "" -#: ../melon/servers/peertube/__init__.py:21 +#: ../melon/servers/peertube/__init__.py:23 msgid "" "List of peertube instances, from which to fetch content. See https://" "joinpeertube.org/instances" msgstr "" -#: ../melon/servers/peertube/__init__.py:27 +#: ../melon/servers/peertube/__init__.py:31 msgid "Show NSFW content" msgstr "" -#: ../melon/servers/peertube/__init__.py:28 +#: ../melon/servers/peertube/__init__.py:32 msgid "Passes the nsfw filter to the peertube search API" msgstr "" -#: ../melon/servers/peertube/__init__.py:33 +#: ../melon/servers/peertube/__init__.py:39 msgid "Enable Federation" msgstr "" -#: ../melon/servers/peertube/__init__.py:34 +#: ../melon/servers/peertube/__init__.py:40 msgid "Returns content from federated instances instead of only local content" msgstr "" -#: ../melon/servers/peertube/__init__.py:58 +#: ../melon/servers/peertube/__init__.py:65 msgid "Latest" msgstr "" -#: ../melon/settings/__init__.py:18 +#: ../melon/settings/__init__.py:23 msgid "Show Previews when browsing public feeds (incl. search)" msgstr "" -#: ../melon/settings/__init__.py:19 +#: ../melon/settings/__init__.py:25 msgid "" "Set to true to show previews when viewing channel contents, public feeds and " "searching" msgstr "" -#: ../melon/settings/__init__.py:25 +#: ../melon/settings/__init__.py:34 msgid "Show Previews in local feeds" msgstr "" -#: ../melon/settings/__init__.py:26 +#: ../melon/settings/__init__.py:36 msgid "" "Set to true to show previews in the new feed, for subscribed channels, and " "saved playlists" msgstr "" -#: ../melon/settings/__init__.py:32 +#: ../melon/settings/__init__.py:44 msgid "Show servers that may contain nsfw content" msgstr "" -#: ../melon/settings/__init__.py:33 +#: ../melon/settings/__init__.py:46 msgid "" "Lists/Delists servers in the browse servers list, if they contain some nsfw " "content" msgstr "" -#: ../melon/settings/__init__.py:38 +#: ../melon/settings/__init__.py:54 msgid "Show servers that only contain nsfw content" msgstr "" -#: ../melon/settings/__init__.py:39 +#: ../melon/settings/__init__.py:56 msgid "" "Lists/Delists servers in the browse servers list, if they contain only/" "mostly nsfw content" msgstr "" -#: ../melon/settings/__init__.py:45 +#: ../melon/settings/__init__.py:64 msgid "Show servers that require login" msgstr "" -#: ../melon/settings/__init__.py:46 +#: ../melon/settings/__init__.py:66 msgid "" "Lists/Delists servers in the browse servers list, if they require login to " "function" msgstr "" -#: ../melon/settings/__init__.py:55 +#: ../melon/settings/__init__.py:78 msgid "Settings" msgstr "" -#: ../melon/settings/__init__.py:68 +#: ../melon/settings/__init__.py:91 msgid "General" msgstr "" -#: ../melon/settings/__init__.py:69 +#: ../melon/settings/__init__.py:92 msgid "Global app settings" msgstr "" -#: ../melon/settings/__init__.py:93 +#: ../melon/settings/__init__.py:116 msgid "Enable Server" msgstr "" -#: ../melon/settings/__init__.py:94 +#: ../melon/settings/__init__.py:118 msgid "" "Disabled servers won't show up in the browser or on the local/home screen" msgstr "" -#: ../melon/widgets/feeditem.py:121 +#: ../melon/widgets/feeditem.py:139 msgid "Watch now" msgstr "" -#: ../melon/widgets/feeditem.py:123 +#: ../melon/widgets/feeditem.py:152 msgid "Open in browser" msgstr "" -#: ../melon/widgets/feeditem.py:124 +#: ../melon/widgets/feeditem.py:156 msgid "View channel" msgstr "" -#: ../melon/widgets/player.py:119 +#: ../melon/widgets/player.py:136 msgid "No streams available" msgstr "" -#: ../melon/widgets/player.py:180 +#: ../melon/widgets/player.py:198 msgid "Toggle floating window" msgstr "" -#: ../melon/widgets/player.py:184 +#: ../melon/widgets/player.py:204 msgid "Toggle fullscreen" msgstr "" -#: ../melon/widgets/player.py:204 ../melon/widgets/player.py:565 -#: ../melon/widgets/player.py:571 +#: ../melon/widgets/player.py:226 ../melon/widgets/player.py:613 +#: ../melon/widgets/player.py:622 msgid "Play" msgstr "" -#: ../melon/widgets/player.py:230 +#: ../melon/widgets/player.py:253 msgid "Stream options" msgstr "" -#: ../melon/widgets/player.py:442 +#: ../melon/widgets/player.py:483 msgid "Resolution" msgstr "" -#: ../melon/widgets/player.py:559 +#: ../melon/widgets/player.py:604 msgid "Pause" msgstr "" -#: ../melon/widgets/player.py:589 +#: ../melon/widgets/player.py:642 msgid "The video is playing in separate window" msgstr "" -#: ../melon/widgets/player.py:710 +#: ../melon/widgets/player.py:779 msgid "Player" msgstr "" -#: ../melon/widgets/preferencerow.py:116 +#: ../melon/widgets/preferencerow.py:125 msgid "Add" msgstr "" -#: ../melon/widgets/preferencerow.py:130 +#: ../melon/widgets/preferencerow.py:139 msgid "Move up" msgstr "" -#: ../melon/widgets/preferencerow.py:141 +#: ../melon/widgets/preferencerow.py:147 msgid "Move down" msgstr "" -#: ../melon/widgets/preferencerow.py:150 +#: ../melon/widgets/preferencerow.py:153 msgid "Remove from list" msgstr "" -#: ../melon/widgets/preferencerow.py:163 +#: ../melon/widgets/preferencerow.py:168 msgid "Add Item" msgstr "" -#: ../melon/widgets/preferencerow.py:166 +#: ../melon/widgets/preferencerow.py:171 msgid "Create a new list entry" msgstr "" -#: ../melon/widgets/preferencerow.py:167 +#: ../melon/widgets/preferencerow.py:172 msgid "Enter the new value here" msgstr "" -#: ../melon/widgets/preferencerow.py:170 +#: ../melon/widgets/preferencerow.py:175 msgid "Value" msgstr "" -#: ../melon/widgets/preferencerow.py:210 +#: ../melon/widgets/preferencerow.py:217 msgid "Do you really want to delete this item?" msgstr "" -#: ../melon/widgets/preferencerow.py:211 +#: ../melon/widgets/preferencerow.py:218 msgid "You won't be able to restore it afterwards" msgstr "" -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/widgets/preferencerow.py:226 msgid "Do not remove item" msgstr "" -#: ../melon/widgets/preferencerow.py:219 +#: ../melon/widgets/preferencerow.py:229 msgid "Remove item from list" msgstr "" -#: ../melon/window.py:84 +#: ../melon/window.py:94 msgid "Stream videos on the go" msgstr "" diff --git a/po/nl.po b/po/nl.po index 1f1e3f6..0547674 100644 --- a/po/nl.po +++ b/po/nl.po @@ -7,11 +7,11 @@ msgid "" msgstr "" "Project-Id-Version: Melon 0.2.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-12 21:13+0200\n" -"PO-Revision-Date: 2024-06-14 10:18+0000\n" +"POT-Creation-Date: 2024-07-15 07:54+0200\n" +"PO-Revision-Date: 2024-04-23 12:18+0000\n" "Last-Translator: Vistaus \n" -"Language-Team: Dutch \n" +"Language-Team: Dutch \n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -92,239 +92,239 @@ msgid "" msgstr "" "Het videoscherm met de videospeler, metagegevens en favorietenknop erboven" -#: ../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:29 -#: ../melon/home/playlists.py:21 ../melon/home/subs.py:20 -#: ../melon/importer.py:61 ../melon/player/__init__.py:102 -#: ../melon/player/playlist.py:158 ../melon/player/playlist.py:165 -#: ../melon/playlist/__init__.py:61 +#: ../melon/browse/__init__.py:20 ../melon/browse/search.py:66 +#: ../melon/browse/server.py:115 ../melon/browse/server.py:143 +#: ../melon/home/history.py:25 ../melon/home/new.py:36 +#: ../melon/home/playlists.py:23 ../melon/home/subs.py:22 +#: ../melon/importer.py:63 ../melon/player/__init__.py:112 +#: ../melon/player/playlist.py:164 ../melon/player/playlist.py:171 +#: ../melon/playlist/__init__.py:77 msgid "*crickets chirping*" msgstr "*tsjilpende krekels*" -#: ../melon/browse/__init__.py:19 +#: ../melon/browse/__init__.py:21 msgid "There are no available servers" msgstr "Er zijn geen servers beschikbaar" -#: ../melon/browse/__init__.py:21 ../melon/browse/search.py:61 -#: ../melon/importer.py:64 +#: ../melon/browse/__init__.py:24 ../melon/browse/search.py:72 +#: ../melon/importer.py:67 msgid "Enable servers in the settings menu" msgstr "Schakel servers in in het voorkeurenmenu" -#: ../melon/browse/__init__.py:29 +#: ../melon/browse/__init__.py:33 msgid "Available Servers" msgstr "Beschikbare servers" -#: ../melon/browse/__init__.py:30 +#: ../melon/browse/__init__.py:35 msgid "You can enable/disable and filter servers in the settings menu" msgstr "U kunt servers in- en uitschakelen en filteren in het voorkeurenmenu" -#: ../melon/browse/__init__.py:48 +#: ../melon/browse/__init__.py:56 msgid "Servers" msgstr "Servers" -#: ../melon/browse/__init__.py:56 ../melon/browse/search.py:106 +#: ../melon/browse/__init__.py:64 ../melon/browse/search.py:126 msgid "Global Search" msgstr "Globaal zoeken" -#: ../melon/browse/channel.py:93 +#: ../melon/browse/channel.py:104 msgid "Subscribe to channel" msgstr "Abonneren op kanaal" -#: ../melon/browse/channel.py:94 +#: ../melon/browse/channel.py:105 msgid "Add latest uploads to home feed" msgstr "Recentste uploads toevoegen aan tijdlijn" -#: ../melon/browse/channel.py:106 +#: ../melon/browse/channel.py:117 msgid "Channel feed" msgstr "Kanaaltijdlijn" -#: ../melon/browse/channel.py:107 +#: ../melon/browse/channel.py:118 msgid "This channel provides multiple feeds, choose which one to view" msgstr "Dit kanaal beschikt over meerdere tijdlijnen - kies er een" -#: ../melon/browse/channel.py:159 +#: ../melon/browse/channel.py:170 msgid "Channel" msgstr "Kanaal" -#: ../melon/browse/playlist.py:37 ../melon/playlist/__init__.py:53 +#: ../melon/browse/playlist.py:48 ../melon/playlist/__init__.py:69 msgid "Start playing" msgstr "Afspelen starten" -#: ../melon/browse/playlist.py:56 ../melon/player/__init__.py:44 +#: ../melon/browse/playlist.py:69 ../melon/player/__init__.py:53 msgid "Bookmark" msgstr "Toev. aan favorieten" -#: ../melon/browse/playlist.py:57 +#: ../melon/browse/playlist.py:70 msgid "Add Playlist to your local playlist collection" msgstr "Afspeellijst toevoegen aan lokale verzameling" -#: ../melon/browse/playlist.py:111 +#: ../melon/browse/playlist.py:124 msgid "Playlist" msgstr "Afspeellijst" -#: ../melon/browse/search.py:44 +#: ../melon/browse/search.py:49 msgid "No results" msgstr "Er zijn geen zoekresultaten" -#: ../melon/browse/search.py:46 +#: ../melon/browse/search.py:52 #, python-brace-format msgid "{count} result" msgid_plural "{count} results" msgstr[0] "{count} resultaat" msgstr[1] "{count} resultaten" -#: ../melon/browse/search.py:59 +#: ../melon/browse/search.py:68 msgid "There are no available servers, a search would yield no results" msgstr "" "Er zijn geen servers beschikbaar, dus een zoekopdracht zou geen resultaten " "geven" -#: ../melon/browse/search.py:83 ../melon/browse/server.py:43 +#: ../melon/browse/search.py:96 ../melon/browse/server.py:47 msgid "Any" msgstr "Iedere" -#: ../melon/browse/search.py:85 ../melon/browse/server.py:45 +#: ../melon/browse/search.py:100 ../melon/browse/server.py:51 msgid "Channels" msgstr "Kanalen" -#: ../melon/browse/search.py:88 ../melon/browse/server.py:48 -#: ../melon/home/playlists.py:32 ../melon/home/playlists.py:64 -#: ../melon/servers/nebula/__init__.py:194 -#: ../melon/servers/peertube/__init__.py:205 +#: ../melon/browse/search.py:105 ../melon/browse/server.py:56 +#: ../melon/home/playlists.py:34 ../melon/home/playlists.py:78 +#: ../melon/servers/nebula/__init__.py:206 +#: ../melon/servers/peertube/__init__.py:213 msgid "Playlists" msgstr "Afspeellijsten" -#: ../melon/browse/search.py:91 ../melon/browse/server.py:51 -#: ../melon/servers/nebula/__init__.py:188 -#: ../melon/servers/peertube/__init__.py:204 -#: ../melon/servers/peertube/__init__.py:208 +#: ../melon/browse/search.py:110 ../melon/browse/server.py:61 +#: ../melon/servers/nebula/__init__.py:200 +#: ../melon/servers/peertube/__init__.py:213 +#: ../melon/servers/peertube/__init__.py:214 msgid "Videos" msgstr "Video's" -#: ../melon/browse/server.py:23 +#: ../melon/browse/server.py:26 msgid "Search" msgstr "Zoeken" -#: ../melon/browse/server.py:102 +#: ../melon/browse/server.py:116 msgid "Try searching for a term" msgstr "Voer een zoekopdracht in" -#: ../melon/browse/server.py:104 +#: ../melon/browse/server.py:118 msgid "Try using a different query" msgstr "Probeer een andere zoekopdracht" -#: ../melon/browse/server.py:128 +#: ../melon/browse/server.py:144 msgid "This feed is empty" msgstr "Deze tijdlijn is blanco" -#: ../melon/home/__init__.py:18 +#: ../melon/home/__init__.py:20 msgid "Home" msgstr "Tijdlijn" -#: ../melon/home/__init__.py:39 ../melon/home/history.py:26 -#: ../melon/home/new.py:36 ../melon/home/subs.py:23 +#: ../melon/home/__init__.py:41 ../melon/home/history.py:28 +#: ../melon/home/new.py:49 ../melon/home/subs.py:25 msgid "Browse Servers" msgstr "Servers verkennen" -#: ../melon/home/__init__.py:46 +#: ../melon/home/__init__.py:48 msgid "Preferences" msgstr "Voorkeuren" -#: ../melon/home/__init__.py:47 +#: ../melon/home/__init__.py:49 msgid "Import Data" msgstr "Gegevens importeren" -#: ../melon/home/__init__.py:48 +#: ../melon/home/__init__.py:50 msgid "About Melon" msgstr "Over Melon" -#: ../melon/home/history.py:24 +#: ../melon/home/history.py:26 msgid "You haven't watched any videos yet" msgstr "U heeft nog geen video's bekeken" -#: ../melon/home/history.py:36 ../melon/home/history.py:117 +#: ../melon/home/history.py:38 ../melon/home/history.py:125 msgid "History" msgstr "Geschiedenis" -#: ../melon/home/history.py:37 +#: ../melon/home/history.py:39 msgid "These are the videos you opened in the past" msgstr "Dit zijn video's die u in het verleden bekeken hebt" -#: ../melon/home/history.py:76 +#: ../melon/home/history.py:83 msgid "Show more" msgstr "Meer tonen" -#: ../melon/home/history.py:77 +#: ../melon/home/history.py:84 msgid "Load older videos" msgstr "Oudere video's laden" -#: ../melon/home/new.py:23 +#: ../melon/home/new.py:30 msgid "Refresh" msgstr "Herladen" -#: ../melon/home/new.py:32 +#: ../melon/home/new.py:40 msgid "Subscribe to a channel first, to view new uploads" msgstr "Abonneer op een kanaal om uploads te bekijken" -#: ../melon/home/new.py:34 +#: ../melon/home/new.py:45 msgid "The channels you are subscribed to haven't uploaded anything yet" msgstr "De kanalen waarop u geabonneerd bent hebben nog niks geüpload" -#: ../melon/home/new.py:55 +#: ../melon/home/new.py:68 #, python-brace-format msgid "(Last refresh: {last_refresh})" msgstr "(Bijgewerkt op {last_refresh})" -#: ../melon/home/new.py:57 ../melon/home/new.py:146 +#: ../melon/home/new.py:72 ../melon/home/new.py:172 msgid "What's new" msgstr "Nieuw" -#: ../melon/home/new.py:58 +#: ../melon/home/new.py:74 msgid "These are the latest videos of channels you follow" msgstr "Dit zijn de nieuwste video's van de kanalen die u volgt" -#: ../melon/home/new.py:137 +#: ../melon/home/new.py:162 msgid "Pick up where you left off" msgstr "Hervatten vanaf pauzepunt" -#: ../melon/home/new.py:138 +#: ../melon/home/new.py:163 msgid "Watch" msgstr "Bekijken" -#: ../melon/home/playlists.py:22 +#: ../melon/home/playlists.py:24 msgid "You don't have any playlists yet" msgstr "Er zijn nog geen afspeellijsten" -#: ../melon/home/playlists.py:24 ../melon/home/playlists.py:34 +#: ../melon/home/playlists.py:26 ../melon/home/playlists.py:39 msgid "Create a new playlist" msgstr "Nieuwe afspeellijst" -#: ../melon/home/playlists.py:33 +#: ../melon/home/playlists.py:36 msgid "Here are playlists you've bookmarked or created yourself" msgstr "Hier vindt u de door u gemaakte en toegevoegde afspeellijsten" -#: ../melon/home/playlists.py:34 +#: ../melon/home/playlists.py:39 msgid "New" msgstr "Nieuw" -#: ../melon/home/subs.py:21 +#: ../melon/home/subs.py:23 msgid "You aren't yet subscribed to channels" msgstr "U bent nog niet geabonneerd op kanalen" -#: ../melon/home/subs.py:31 ../melon/home/subs.py:63 +#: ../melon/home/subs.py:33 ../melon/home/subs.py:68 msgid "Subscriptions" msgstr "Abonnementen" -#: ../melon/home/subs.py:32 +#: ../melon/home/subs.py:34 msgid "You are subscribed to the following channels" msgstr "U bent geabonneerd op de volgende kanalen" -#: ../melon/import_providers/newpipe.py:28 +#: ../melon/import_providers/newpipe.py:42 msgid "Newpipe Database importer" msgstr "NewPipe-databankimport" -#: ../melon/import_providers/newpipe.py:29 +#: ../melon/import_providers/newpipe.py:44 msgid "" "Import the .db file from inside the newpipe .zip export (as invidious " "content)" @@ -332,242 +332,242 @@ msgstr "" "Importeer het .db-bestand uit de .zip-export van NewPipe (als Invidious-" "inhoud)" -#: ../melon/import_providers/newpipe.py:30 +#: ../melon/import_providers/newpipe.py:46 msgid "Select .db file" msgstr "Kies een .db-bestand" -#: ../melon/import_providers/newpipe.py:45 +#: ../melon/import_providers/newpipe.py:60 msgid "Newpipe Database" msgstr "NewPipe-databank" -#: ../melon/importer.py:17 ../melon/importer.py:35 +#: ../melon/importer.py:19 ../melon/importer.py:37 msgid "Import" msgstr "Importeren" -#: ../melon/importer.py:36 +#: ../melon/importer.py:38 msgid "The following import methods have been found" msgstr "De volgende importmogelijkheden zijn beschikbaar" -#: ../melon/importer.py:62 +#: ../melon/importer.py:64 msgid "There are no available importer methods" msgstr "Er zijn beschikbare importmogelijkheden" -#: ../melon/player/__init__.py:34 +#: ../melon/player/__init__.py:41 msgid "Description" msgstr "Beschrijving" -#: ../melon/player/__init__.py:45 +#: ../melon/player/__init__.py:54 msgid "Add this video to a playlist" msgstr "Video toevoegen aan afspeellijst" -#: ../melon/player/__init__.py:103 +#: ../melon/player/__init__.py:113 msgid "Video could not be loaded" msgstr "De video kan niet worden geladen" -#: ../melon/player/__init__.py:140 ../melon/player/__init__.py:174 -#: ../melon/player/playlist.py:51 +#: ../melon/player/__init__.py:151 ../melon/player/__init__.py:185 +#: ../melon/player/playlist.py:57 msgid "Loading..." msgstr "Bezig met laden…" -#: ../melon/player/playlist.py:159 +#: ../melon/player/playlist.py:165 msgid "This playlist is empty" msgstr "Deze afspeellijst is blanco" -#: ../melon/player/playlist.py:166 +#: ../melon/player/playlist.py:172 msgid "There was an error loading the playlist" msgstr "Er is een fout opgetreden tijdens het laden" -#: ../melon/player/playlist.py:218 +#: ../melon/player/playlist.py:225 msgid "Previous" msgstr "Vorige" -#: ../melon/player/playlist.py:219 +#: ../melon/player/playlist.py:227 msgid "Play video that comes before this one in the playlist" msgstr "Video hiervoor afspelen" -#: ../melon/player/playlist.py:230 +#: ../melon/player/playlist.py:244 msgid "Next" msgstr "Volgende" -#: ../melon/player/playlist.py:231 +#: ../melon/player/playlist.py:246 msgid "Play video that comes after this one in the playlist" msgstr "Video hierna afspelen" -#: ../melon/player/playlist.py:240 +#: ../melon/player/playlist.py:258 msgid "Skip" msgstr "Overslaan" -#: ../melon/player/playlist.py:241 +#: ../melon/player/playlist.py:259 msgid "Skip this video and pick a new one at random" msgstr "Sla deze video over en kies een willekeurige andere" -#: ../melon/player/playlist.py:250 +#: ../melon/player/playlist.py:270 msgid "Shuffle" msgstr "Willekeurig" -#: ../melon/player/playlist.py:251 +#: ../melon/player/playlist.py:271 msgid "Chooses the next video at random" msgstr "Kies een willekeurige video" -#: ../melon/player/playlist.py:261 +#: ../melon/player/playlist.py:282 msgid "Repeat current video" msgstr "Huidige video herhalen" -#: ../melon/player/playlist.py:262 +#: ../melon/player/playlist.py:283 msgid "Puts this video on loop" msgstr "Herhaalt de video" -#: ../melon/player/playlist.py:276 +#: ../melon/player/playlist.py:298 msgid "Repeat playlist" msgstr "Afspeellijst herhalen" -#: ../melon/player/playlist.py:277 +#: ../melon/player/playlist.py:300 msgid "Start playling the playlist from the beginning after reaching the end" msgstr "Starten met afspelen vanaf het begin na de laatste video" -#: ../melon/player/playlist.py:287 +#: ../melon/player/playlist.py:312 msgid "Playlist Content" msgstr "Afspeellijstinhoud" -#: ../melon/player/playlist.py:288 +#: ../melon/player/playlist.py:314 msgid "Click on videos to continue playing the playlist from there" msgstr "Klik op video's om het afspelen vanaf daar te hervatten" -#: ../melon/playlist/__init__.py:50 +#: ../melon/playlist/__init__.py:65 msgid "Edit" msgstr "Bewerken" -#: ../melon/playlist/__init__.py:62 +#: ../melon/playlist/__init__.py:79 msgid "You haven't added any videos to this playlist yet" msgstr "Deze afspeellijst bevat nog geen video's" -#: ../melon/playlist/__init__.py:64 +#: ../melon/playlist/__init__.py:82 msgid "Start watching" msgstr "Beginnen met kijken" -#: ../melon/playlist/__init__.py:87 +#: ../melon/playlist/__init__.py:106 msgid "Set as playlist thumbnail" msgstr "Instellen als afspeellijstminiatuur" -#: ../melon/playlist/__init__.py:88 +#: ../melon/playlist/__init__.py:114 msgid "Remove from playlist" msgstr "Verwijderen van afspeellijst" -#: ../melon/playlist/__init__.py:99 +#: ../melon/playlist/__init__.py:131 msgid "Edit Playlist" msgstr "Afspeellijst bewerken" -#: ../melon/playlist/__init__.py:105 +#: ../melon/playlist/__init__.py:137 msgid "Playlist details" msgstr "Afspeellijstinformatie" -#: ../melon/playlist/__init__.py:106 +#: ../melon/playlist/__init__.py:138 msgid "Change playlist information" msgstr "Afspeellijstinformatie wijzigen" -#: ../melon/playlist/__init__.py:108 ../melon/playlist/create.py:37 +#: ../melon/playlist/__init__.py:140 ../melon/playlist/create.py:43 msgid "Playlist name" msgstr "Afspeellijstnaam" -#: ../melon/playlist/__init__.py:111 ../melon/playlist/create.py:40 +#: ../melon/playlist/__init__.py:143 ../melon/playlist/create.py:46 msgid "Playlist description" msgstr "Afspeellijstbeschrijving" -#: ../melon/playlist/__init__.py:116 +#: ../melon/playlist/__init__.py:148 msgid "Save details" msgstr "Informatie opslaan" -#: ../melon/playlist/__init__.py:117 +#: ../melon/playlist/__init__.py:150 msgid "Change playlist title and description. (Closes the dialog)" msgstr "" "Wijzig de afspeellijsttitel en -beschrijving (het venster wordt gesloten)" -#: ../melon/playlist/__init__.py:127 ../melon/playlist/__init__.py:169 +#: ../melon/playlist/__init__.py:161 ../melon/playlist/__init__.py:207 msgid "Delete Playlist" msgstr "Afspeellijst verwijderen" -#: ../melon/playlist/__init__.py:128 +#: ../melon/playlist/__init__.py:163 msgid "Delete this playlist and it's content. This can NOT be undone." msgstr "" "Verwijder deze afspeellijst en alle bijbehorende inhoud. Let op: dit is " "onomkeerbaar." -#: ../melon/playlist/__init__.py:139 +#: ../melon/playlist/__init__.py:176 msgid "Close" msgstr "Sluiten" -#: ../melon/playlist/__init__.py:139 +#: ../melon/playlist/__init__.py:178 msgid "Close without changing anything" msgstr "Sluiten zonder opslaan" -#: ../melon/playlist/__init__.py:172 +#: ../melon/playlist/__init__.py:210 msgid "Do you really want to delete this playlist?" msgstr "Weet u zeker dat u deze afspeellijst wilt verwijderen?" -#: ../melon/playlist/__init__.py:173 +#: ../melon/playlist/__init__.py:211 msgid "This cannot be undone" msgstr "Dit is onomkeerbaar" -#: ../melon/playlist/__init__.py:177 ../melon/playlist/create.py:47 -#: ../melon/playlist/pick.py:70 ../melon/widgets/preferencerow.py:175 -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/playlist/__init__.py:216 ../melon/playlist/create.py:54 +#: ../melon/playlist/pick.py:88 ../melon/widgets/preferencerow.py:181 +#: ../melon/widgets/preferencerow.py:226 msgid "Cancel" msgstr "Annuleren" -#: ../melon/playlist/__init__.py:177 +#: ../melon/playlist/__init__.py:218 msgid "Do not delete the playlist" msgstr "Afspeellijst niet verwijderen" -#: ../melon/playlist/__init__.py:178 ../melon/widgets/preferencerow.py:207 -#: ../melon/widgets/preferencerow.py:219 +#: ../melon/playlist/__init__.py:221 ../melon/widgets/preferencerow.py:214 +#: ../melon/widgets/preferencerow.py:229 msgid "Delete" msgstr "Verwijderen" -#: ../melon/playlist/__init__.py:178 +#: ../melon/playlist/__init__.py:221 msgid "Delete this playlist" msgstr "Afspeellijst verwijderen" -#: ../melon/playlist/create.py:23 ../melon/playlist/pick.py:28 +#: ../melon/playlist/create.py:25 ../melon/playlist/pick.py:35 msgid "Video" msgstr "Video" -#: ../melon/playlist/create.py:24 +#: ../melon/playlist/create.py:27 msgid "The following video will be added to the new playlist" msgstr "De volgende video zal worden toegevoegd aan de nieuwe afspeellijst" -#: ../melon/playlist/create.py:33 ../melon/playlist/create.py:91 +#: ../melon/playlist/create.py:39 ../melon/playlist/create.py:98 msgid "New Playlist" msgstr "Nieuwe afspeellijst" -#: ../melon/playlist/create.py:34 +#: ../melon/playlist/create.py:40 msgid "Enter more playlist information" msgstr "Voer meer afspeellijstinformatie in" -#: ../melon/playlist/create.py:38 +#: ../melon/playlist/create.py:44 msgid "Unnamed Playlist" msgstr "Naamloze afspeellijst" -#: ../melon/playlist/create.py:47 ../melon/playlist/pick.py:70 +#: ../melon/playlist/create.py:54 ../melon/playlist/pick.py:88 msgid "Do not create playlist" msgstr "Geen afspeellijst maken" -#: ../melon/playlist/create.py:48 ../melon/widgets/preferencerow.py:176 +#: ../melon/playlist/create.py:57 ../melon/widgets/preferencerow.py:184 msgid "Create" msgstr "Maken" -#: ../melon/playlist/create.py:48 +#: ../melon/playlist/create.py:57 msgid "Create playlist" msgstr "Afspeellijst maken" -#: ../melon/playlist/pick.py:29 +#: ../melon/playlist/pick.py:37 msgid "The following video will be added to the playlist" msgstr "De volgende video zal worden toegevoegd aan de afspeellijst" -#: ../melon/playlist/pick.py:39 ../melon/widgets/feeditem.py:122 +#: ../melon/playlist/pick.py:50 ../melon/widgets/feeditem.py:145 msgid "Add to playlist" msgstr "Toevoegen aan afspeellijst" -#: ../melon/playlist/pick.py:40 +#: ../melon/playlist/pick.py:53 msgid "" "Choose a playlist to add the video to. Note that you can only add videos to " "local playlists, not external bookmarked ones" @@ -575,42 +575,42 @@ msgstr "" "Kies een afspeellijst om de video aan toe te voegen. Let op: u kunt alleen " "video's toevoegen aan lokale afspeellijsten." -#: ../melon/playlist/pick.py:42 +#: ../melon/playlist/pick.py:57 msgid "Create new playlist" msgstr "Nieuwe afspeellijst maken" -#: ../melon/playlist/pick.py:43 +#: ../melon/playlist/pick.py:58 msgid "Create a new playlist and add the video to it" msgstr "Nieuwe afspeellijst aanmaken en video toevoegen" -#: ../melon/playlist/pick.py:102 +#: ../melon/playlist/pick.py:119 msgid "Add to Playlist" msgstr "Toevoegen aan afspeellijst" -#: ../melon/servers/invidious/__init__.py:31 +#: ../melon/servers/invidious/__init__.py:35 msgid "Open source alternative front-end to YouTube" msgstr "Een alternatieve, opensourceclient voor YouTube" -#: ../melon/servers/invidious/__init__.py:36 +#: ../melon/servers/invidious/__init__.py:40 msgid "Instance" msgstr "Instantie" -#: ../melon/servers/invidious/__init__.py:37 +#: ../melon/servers/invidious/__init__.py:42 msgid "" "See https://docs.invidious.io/instances/ for a list of available instances" msgstr "" "Zie https://docs.invidious.io/instances/ voor een lijst met beschikbare " "instanties" -#: ../melon/servers/invidious/__init__.py:48 +#: ../melon/servers/invidious/__init__.py:55 msgid "Trending" msgstr "Trending" -#: ../melon/servers/invidious/__init__.py:49 +#: ../melon/servers/invidious/__init__.py:56 msgid "Popular" msgstr "Populair" -#: ../melon/servers/nebula/__init__.py:19 +#: ../melon/servers/nebula/__init__.py:21 msgid "" "Home of smart, thoughtful videos, podcasts, and classes from your favorite " "creators" @@ -618,65 +618,65 @@ msgstr "" "Een tijdlijn met slimme, doordachte video's, podcasts en cursussen van uw " "favoriete makers" -#: ../melon/servers/nebula/__init__.py:24 +#: ../melon/servers/nebula/__init__.py:27 msgid "Email Address" msgstr "E-mailadres" -#: ../melon/servers/nebula/__init__.py:25 +#: ../melon/servers/nebula/__init__.py:28 msgid "Email Address to login to your account" msgstr "Het e-mailadres waarmee u inlogt op uw account" -#: ../melon/servers/nebula/__init__.py:31 +#: ../melon/servers/nebula/__init__.py:35 msgid "Password" msgstr "Wachtwoord" -#: ../melon/servers/nebula/__init__.py:32 +#: ../melon/servers/nebula/__init__.py:36 msgid "Password associated with your account" msgstr "Het wachtwoord van uw account" -#: ../melon/servers/peertube/__init__.py:15 +#: ../melon/servers/peertube/__init__.py:16 msgid "Decentralized video hosting network, based on free/libre software" msgstr "" "Een gedecentraliseerd videonetwerk, gebaseerd op gratis, vrije software" -#: ../melon/servers/peertube/__init__.py:20 +#: ../melon/servers/peertube/__init__.py:21 msgid "Instances" msgstr "Instanties" -#: ../melon/servers/peertube/__init__.py:21 +#: ../melon/servers/peertube/__init__.py:23 msgid "" "List of peertube instances, from which to fetch content. See https://" "joinpeertube.org/instances" msgstr "Lijst met PeerTube-instanties om inhoud van op te halen. Zie" -#: ../melon/servers/peertube/__init__.py:27 +#: ../melon/servers/peertube/__init__.py:31 msgid "Show NSFW content" msgstr "18+-inhoud tonen" -#: ../melon/servers/peertube/__init__.py:28 +#: ../melon/servers/peertube/__init__.py:32 msgid "Passes the nsfw filter to the peertube search API" msgstr "Geeft het 18+-filter door aan de PeerTube-zoek-api" -#: ../melon/servers/peertube/__init__.py:33 +#: ../melon/servers/peertube/__init__.py:39 msgid "Enable Federation" msgstr "Federatie inschakelen" -#: ../melon/servers/peertube/__init__.py:34 +#: ../melon/servers/peertube/__init__.py:40 msgid "Returns content from federated instances instead of only local content" msgstr "" "Haalt inhoud van gefedereerde instanties op in plaats van alleen lokale " "inhoud" -#: ../melon/servers/peertube/__init__.py:58 +#: ../melon/servers/peertube/__init__.py:65 msgid "Latest" msgstr "Nieuwste" -#: ../melon/settings/__init__.py:18 +#: ../melon/settings/__init__.py:23 msgid "Show Previews when browsing public feeds (incl. search)" msgstr "" "Voorvertoningen inschakelen op openbare tijdlijnen (inclusief zoekpagina)" -#: ../melon/settings/__init__.py:19 +#: ../melon/settings/__init__.py:25 msgid "" "Set to true to show previews when viewing channel contents, public feeds and " "searching" @@ -684,11 +684,11 @@ msgstr "" "Schakel in om voorvertoningen in te schakelen op kanaalinhoud, openbare " "tijdlijnen en de zoekpagina" -#: ../melon/settings/__init__.py:25 +#: ../melon/settings/__init__.py:34 msgid "Show Previews in local feeds" msgstr "Voorvertoningen inschakelen op lokale tijdlijnen" -#: ../melon/settings/__init__.py:26 +#: ../melon/settings/__init__.py:36 msgid "" "Set to true to show previews in the new feed, for subscribed channels, and " "saved playlists" @@ -696,22 +696,22 @@ msgstr "" "Schakel in om voorvertoningen in te schakelen op de tijdlijn, geabonneerde " "kanalen en opgeslagen afspeellijsten" -#: ../melon/settings/__init__.py:32 +#: ../melon/settings/__init__.py:44 msgid "Show servers that may contain nsfw content" msgstr "Servers tonen die 18+-inhoud kunnen bevatten" -#: ../melon/settings/__init__.py:33 +#: ../melon/settings/__init__.py:46 msgid "" "Lists/Delists servers in the browse servers list, if they contain some nsfw " "content" msgstr "" "Toon/Verberg servers op de verkenlijst als ze enige 18+-inhoud bevatten" -#: ../melon/settings/__init__.py:38 +#: ../melon/settings/__init__.py:54 msgid "Show servers that only contain nsfw content" msgstr "Servers tonen die alleen 18+-inhoud bevatten" -#: ../melon/settings/__init__.py:39 +#: ../melon/settings/__init__.py:56 msgid "" "Lists/Delists servers in the browse servers list, if they contain only/" "mostly nsfw content" @@ -719,136 +719,136 @@ msgstr "" "Toon/Verberg servers op de verkenlijst als ze alleen of veelal 18+-inhoud " "bevatten" -#: ../melon/settings/__init__.py:45 +#: ../melon/settings/__init__.py:64 msgid "Show servers that require login" msgstr "Servers tonen die een account vereisen" -#: ../melon/settings/__init__.py:46 +#: ../melon/settings/__init__.py:66 msgid "" "Lists/Delists servers in the browse servers list, if they require login to " "function" msgstr "Toon/Verberg servers op de verkenlijst als ze een account vereisen" -#: ../melon/settings/__init__.py:55 +#: ../melon/settings/__init__.py:78 msgid "Settings" msgstr "Voorkeuren" -#: ../melon/settings/__init__.py:68 +#: ../melon/settings/__init__.py:91 msgid "General" msgstr "Algemeen" -#: ../melon/settings/__init__.py:69 +#: ../melon/settings/__init__.py:92 msgid "Global app settings" msgstr "Algemene voorkeuren" -#: ../melon/settings/__init__.py:93 +#: ../melon/settings/__init__.py:116 msgid "Enable Server" msgstr "Server inschakelen" -#: ../melon/settings/__init__.py:94 +#: ../melon/settings/__init__.py:118 msgid "" "Disabled servers won't show up in the browser or on the local/home screen" msgstr "" "Uitgeschakelde servers worden niet getoond op het verken- en tijdlijnscherm" -#: ../melon/widgets/feeditem.py:121 +#: ../melon/widgets/feeditem.py:139 msgid "Watch now" msgstr "Nu bekijken" -#: ../melon/widgets/feeditem.py:123 +#: ../melon/widgets/feeditem.py:152 msgid "Open in browser" msgstr "Openen in webbrowser" -#: ../melon/widgets/feeditem.py:124 +#: ../melon/widgets/feeditem.py:156 msgid "View channel" msgstr "Kanaal bekijken" -#: ../melon/widgets/player.py:119 +#: ../melon/widgets/player.py:136 msgid "No streams available" msgstr "Er zijn geen streams beschikbaar" -#: ../melon/widgets/player.py:180 +#: ../melon/widgets/player.py:198 msgid "Toggle floating window" msgstr "Zwevend venster aan/uit" -#: ../melon/widgets/player.py:184 +#: ../melon/widgets/player.py:204 msgid "Toggle fullscreen" msgstr "Schermvullende weergave aan/uit" -#: ../melon/widgets/player.py:204 ../melon/widgets/player.py:565 -#: ../melon/widgets/player.py:571 +#: ../melon/widgets/player.py:226 ../melon/widgets/player.py:613 +#: ../melon/widgets/player.py:622 msgid "Play" msgstr "Afspelen" -#: ../melon/widgets/player.py:230 +#: ../melon/widgets/player.py:253 msgid "Stream options" msgstr "Streamopties" -#: ../melon/widgets/player.py:442 +#: ../melon/widgets/player.py:483 msgid "Resolution" msgstr "Resolutie" -#: ../melon/widgets/player.py:559 +#: ../melon/widgets/player.py:604 msgid "Pause" msgstr "Pauzeren" -#: ../melon/widgets/player.py:589 +#: ../melon/widgets/player.py:642 msgid "The video is playing in separate window" msgstr "De video wordt in een apart venster afgespeeld" -#: ../melon/widgets/player.py:710 +#: ../melon/widgets/player.py:779 msgid "Player" msgstr "Speler" -#: ../melon/widgets/preferencerow.py:116 +#: ../melon/widgets/preferencerow.py:125 msgid "Add" msgstr "Toevoegen" -#: ../melon/widgets/preferencerow.py:130 +#: ../melon/widgets/preferencerow.py:139 msgid "Move up" msgstr "Omhoog verplaatsen" -#: ../melon/widgets/preferencerow.py:141 +#: ../melon/widgets/preferencerow.py:147 msgid "Move down" msgstr "Omlaag verplaatsen" -#: ../melon/widgets/preferencerow.py:150 +#: ../melon/widgets/preferencerow.py:153 msgid "Remove from list" msgstr "Verwijderen van lijst" -#: ../melon/widgets/preferencerow.py:163 +#: ../melon/widgets/preferencerow.py:168 msgid "Add Item" msgstr "Item toevoegen" -#: ../melon/widgets/preferencerow.py:166 +#: ../melon/widgets/preferencerow.py:171 msgid "Create a new list entry" msgstr "Nieuw lijstitem" -#: ../melon/widgets/preferencerow.py:167 +#: ../melon/widgets/preferencerow.py:172 msgid "Enter the new value here" msgstr "Voer hier de nieuwe waarde in" -#: ../melon/widgets/preferencerow.py:170 +#: ../melon/widgets/preferencerow.py:175 msgid "Value" msgstr "Waarde" -#: ../melon/widgets/preferencerow.py:210 +#: ../melon/widgets/preferencerow.py:217 msgid "Do you really want to delete this item?" msgstr "Weet u zeker dat u dit item wilt verwijderen?" -#: ../melon/widgets/preferencerow.py:211 +#: ../melon/widgets/preferencerow.py:218 msgid "You won't be able to restore it afterwards" msgstr "U kunt nadien geen herstel uitvoeren" -#: ../melon/widgets/preferencerow.py:218 +#: ../melon/widgets/preferencerow.py:226 msgid "Do not remove item" msgstr "Item niet verwijderen" -#: ../melon/widgets/preferencerow.py:219 +#: ../melon/widgets/preferencerow.py:229 msgid "Remove item from list" msgstr "Item verwijderen van lijst" -#: ../melon/window.py:84 +#: ../melon/window.py:94 msgid "Stream videos on the go" msgstr "Video's onderweg streamen" -- 2.38.5