M melon/application.py => melon/application.py +6 -4
@@ 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):
M melon/background.py => melon/background.py +6 -1
@@ 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)
M melon/browse/__init__.py => melon/browse/__init__.py +13 -5
@@ 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"))
M melon/browse/channel.py => melon/browse/channel.py +19 -8
@@ 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)
M melon/browse/playlist.py => melon/browse/playlist.py +22 -9
@@ 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)
M melon/browse/search.py => melon/browse/search.py +29 -9
@@ 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)
M melon/browse/server.py => melon/browse/server.py +35 -13
@@ 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)
M melon/home/__init__.py => melon/home/__init__.py +4 -2
@@ 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)
M melon/home/history.py => melon/home/history.py +16 -8
@@ 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()
M melon/home/new.py => melon/home/new.py +37 -11
@@ 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)
M melon/home/playlists.py => melon/home/playlists.py +31 -17
@@ 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()
M melon/home/subs.py => melon/home/subs.py +9 -4
@@ 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()
M melon/import_providers/__init__.py => melon/import_providers/__init__.py +6 -2
@@ 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
M melon/import_providers/newpipe.py => melon/import_providers/newpipe.py +70 -28
@@ 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
M melon/import_providers/utils.py => melon/import_providers/utils.py +11 -3
@@ 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
M melon/importer.py => melon/importer.py +9 -4
@@ 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)
M melon/models/__init__.py => melon/models/__init__.py +496 -160
@@ 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
M melon/models/callbacks.py => melon/models/callbacks.py +13 -3
@@ 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:
M melon/player/__init__.py => melon/player/__init__.py +22 -11
@@ 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
M melon/player/playlist.py => melon/player/playlist.py +49 -20
@@ 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 <clicked.index>
- 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)
M melon/playlist/__init__.py => melon/playlist/__init__.py +66 -27
@@ 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)
M melon/playlist/create.py => melon/playlist/create.py +28 -22
@@ 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()
M melon/playlist/pick.py => melon/playlist/pick.py +38 -20
@@ 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)
M melon/servers/__init__.py => melon/servers/__init__.py +56 -19
@@ 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,
M melon/servers/invidious/__init__.py => melon/servers/invidious/__init__.py +85 -64
@@ 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())]
M melon/servers/loader.py => melon/servers/loader.py +5 -5
@@ 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"))
M melon/servers/nebula/__init__.py => melon/servers/nebula/__init__.py +62 -55
@@ 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"]
M melon/servers/peertube/__init__.py => melon/servers/peertube/__init__.py +53 -43
@@ 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)
M melon/servers/utils.py => melon/servers/utils.py +66 -31
@@ 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 <vfebvre@easter-eggs.com>
+
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,
M melon/settings/__init__.py => melon/settings/__init__.py +79 -50
@@ 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)
M melon/utils.py => melon/utils.py +10 -4
@@ 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
M melon/widgets/feeditem.py => melon/widgets/feeditem.py +74 -31
@@ 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
M melon/widgets/filterbutton.py => melon/widgets/filterbutton.py +3 -1
@@ 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)
M melon/widgets/iconbutton.py => melon/widgets/iconbutton.py +8 -2
@@ 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)
M melon/widgets/player.py => melon/widgets/player.py +119 -49
@@ 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
M melon/widgets/preferencerow.py => melon/widgets/preferencerow.py +57 -49
@@ 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()
M melon/widgets/simpledialog.py => melon/widgets/simpledialog.py +10 -4
@@ 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)
M melon/widgets/viewstackpage.py => melon/widgets/viewstackpage.py +4 -7
@@ 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)
M melon/window.py => melon/window.py +26 -11
@@ 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
M po/de.po => po/de.po +193 -193
@@ 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 <hurzelchen@users.noreply.translate.codeberg."
"org>\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"
M po/fa.po => po/fa.po +193 -193
@@ 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 <sohrabbehdani@users.noreply.translate."
"codeberg.org>\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 "ویدیوهارا استریم کن"
M po/melon.pot => po/melon.pot +193 -193
@@ 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""
M po/nl.po => po/nl.po +196 -196
@@ 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 <Vistaus@users.noreply.translate.codeberg.org>\n"
-"Language-Team: Dutch <https://translate.codeberg.org/projects/melon/"
-"melon-app/nl/>\n"
+"Language-Team: Dutch <https://translate.codeberg.org/projects/melon/melon-"
+"app/nl/>\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"