M melon/player/__init__.py => melon/player/__init__.py +1 -0
@@ 124,6 124,7 @@ class PlayerScreen(Adw.NavigationPage):
self.channel = self.instance.get_channel_info(self.video.channel[1])
GLib.idle_add(self.display_channel)
+ view = None
def __init__(self, server_id, video_id, *args, **kwargs):
super().__init__(*args, **kwargs)
M melon/player/playlist.py => melon/player/playlist.py +161 -9
@@ 6,6 6,7 @@ from gi.repository import Gtk, Adw, WebKit, GLib
from unidecode import unidecode
from gettext import gettext as _
import threading
+import random
from melon.servers.utils import get_server_instance, get_servers_list
from melon.servers import SearchMode
@@ 26,6 27,10 @@ class PlaylistPlayerScreen(PlayerScreen):
playlist_instance = None
playlist_content = []
+ playlist_shuffle = False
+ playlist_repeat = False
+ playlist_repeat_single = False
+
def __init__(self, ref: (tuple[str, str] | int), index=0, *args, **kwargs):
# make sure to initalize widget
# PlayerScreen.__init__ will break after initializing parents
@@ 37,6 42,9 @@ class PlaylistPlayerScreen(PlayerScreen):
self.video_duration = None
self.video_position = None
self.playlist_instance = None
+ self.playlist_shuffle = False
+ self.playlist_repeat = False
+ self.playlist_repeat_single = False
# prepare base layout
self.header_bar = Adw.HeaderBar()
@@ 100,9 108,37 @@ class PlaylistPlayerScreen(PlayerScreen):
if self.playlist_index > len(self.playlist_content):
GLib.idle_add(self.playlist_error)
return
- self.prepare(0)
+
+ ind = self.get_next_index(0)
+ if not ind is None:
+ self.prepare(ind)
+
+ def get_next_index(self,fallback=None):
+ index = self.playlist_index + 1
+ if not fallback is None:
+ index = fallback
+
+ if self.playlist_repeat_single:
+ # repeat single song is enabled
+ return self.playlist_index
+ 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)
+ return index
+
+ 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
+ return self.get_next_index(0)
+ else:
+ # no more songs to play
+ return None
+ return index
def prepare(self, index):
+ self.playlist_index = index
dt = self.playlist_content[index]
server_id = dt.server
video_id = dt.id
@@ 111,19 147,15 @@ class PlaylistPlayerScreen(PlayerScreen):
def on_player_end(self):
super().on_player_end()
- # if there are more videos in the playlist
- # load the next one
- # do nothing if there are no more videos
- if self.playlist_index+1 < len(self.playlist_content):
- self.playlist_index += 1
- self.prepare(self.playlist_index)
+ ind = self.get_next_index()
+ if not ind is None:
+ self.prepare(ind)
def empty_playlist(self):
# display playlist is empty banner
# acts as a drop in replacement for the player screen
status = Adw.StatusPage()
status.set_title(_("*crickets chirping*"))
- subs = get_subscribed_channels()
status.set_description(_("This playlist is empty"))
status.set_icon_name("weather-few-clouds-night-symbolic")
self.scrollview.set_child(status)
@@ 131,7 163,6 @@ class PlaylistPlayerScreen(PlayerScreen):
def playlist_error(self):
status = Adw.StatusPage()
status.set_title(_("*crickets chirping*"))
- subs = get_subscribed_channels()
status.set_description(_("There was an error loading the playlist"))
status.set_icon_name("weather-few-clouds-night-symbolic")
self.scrollview.set_child(status)
@@ 142,3 173,124 @@ class PlaylistPlayerScreen(PlayerScreen):
# however in playlist mode it makes sense to always autoplay
# TODO: consider not autoplaying the first title?
self.player.play()
+
+ def background(self, video_id):
+ super().background(video_id)
+ GLib.idle_add(self.display_control)
+
+ def ctr_previous_video(self):
+ index = self.playlist_index - 1
+ if index >= 0:
+ self.prepare(index)
+
+ def ctr_next_video(self):
+ index = self.playlist_index + 1
+ if index < len(self.playlist_content):
+ self.prepare(index)
+
+ def ctr_skip_video(self):
+ ind = self.get_next_index()
+ if not ind is None:
+ self.prepare(ind)
+
+ def ctr_toggle_shuffle(self, state):
+ self.playlist_shuffle = state
+ # force rerender the playlist control box
+ # updates the prev/next and repeat_all button
+ self.about.remove(self.ctr_group)
+ self.display_control()
+
+ def ctr_toggle_playlist_repeat(self, state):
+ self.playlist_repeat = state
+ def ctr_toggle_playlist_repeat_single(self, state):
+ self.playlist_repeat_single = state
+
+ def display_control(self):
+ self.ctr_group = Adw.PreferencesGroup()
+ self.ctr_group.set_title(self.playlist.inner.title)
+ self.ctr_group.set_description(self.playlist.inner.description)
+
+ # if the video isn't the first
+ # show a previous button
+ # not shown when shuffle is enbaled
+ 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.connect("activated", lambda _: self.ctr_previous_video())
+ prev_btn.set_activatable(True)
+ self.ctr_group.add(prev_btn)
+
+ # 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:
+ 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.connect("activated", lambda _: self.ctr_next_video())
+ next_btn.set_activatable(True)
+ self.ctr_group.add(next_btn)
+ elif self.playlist_shuffle:
+ # show alternative next button
+ # which picks a new song
+ 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.connect("activated", lambda _: self.ctr_skip_video())
+ skip_btn.set_activatable(True)
+ self.ctr_group.add(skip_btn)
+
+ # control the playlist shuffle mode
+ pref_shuffle = Preference(
+ "playlist-shuffle",
+ _("Shuffle"),
+ _("Chooses the next video at random"),
+ PreferenceType.TOGGLE,
+ 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())
+ # control single video repeat mode
+ pref_repeat_single = Preference(
+ "playlist-repeat-single",
+ _("Repeat current video"),
+ _("Puts this video on loop"),
+ PreferenceType.TOGGLE,
+ 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())
+
+ # control playlist repeat mode
+ # cannot be changed if shuffle mode is enabled
+ # because the playlist never ends
+ if not self.playlist_shuffle:
+ pref_repeat = Preference(
+ "playlist-repeat",
+ _("Repeat playlist"),
+ _("Start playling the playlist from the beginning after reaching the end"),
+ PreferenceType.TOGGLE,
+ 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())
+
+ # 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"))
+ 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))
+ expander.add_row(row)
+ self.ctr_group.add(expander)
+
+ self.about.add(self.ctr_group)
M melon/widgets/feeditem.py => melon/widgets/feeditem.py +15 -6
@@ 15,11 15,12 @@ class AdaptiveFeedItem(Adw.ActionRow):
"""
FeedItem with automatic adjustment to resource type
"""
- def __init__(self, resource:Resource, show_preview=True, clickable=True, *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
self.clickable = clickable
+ self.onClick = onClick
self.update()
def set_resource(self, resource: Resource):
self.res = resource
@@ 31,14 32,16 @@ class AdaptiveFeedItem(Adw.ActionRow):
show_preview = self.show_preview
clickable = self.clickable
self.set_activatable(clickable)
- self.set_action_target_value(
- GLib.Variant("as", [resource.server, resource.id]))
+ if self.onClick is None:
+ self.set_action_target_value(
+ 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_action_name("win.player")
+ if self.onClick is None:
+ self.set_action_name("win.player")
elif isinstance(resource, Playlist):
thumb_url = resource.thumbnail
self.set_title(unidecode(resource.title).replace("&","&"))
@@ 49,7 52,8 @@ class AdaptiveFeedItem(Adw.ActionRow):
# because it could possibly cut the unicode in half? I think?
sub = unidecode(resource.description)[:80] + pad
self.set_subtitle(sub.replace("&","&"))
- self.set_action_name("win.browse_playlist")
+ 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("&","&"))
@@ 60,7 64,12 @@ class AdaptiveFeedItem(Adw.ActionRow):
# because it could possibly cut the unicode in half? I think?
sub = unidecode(resource.bio)[:80] + pad
self.set_subtitle(sub.replace("&","&"))
- self.set_action_name("win.browse_channel")
+ if self.onClick is None:
+ self.set_action_name("win.browse_channel")
+
+ if not self.onClick is None:
+ self.connect("activated", self.onClick)
+
self.preview = Adw.Avatar()
self.preview.set_size(48)
self.preview.set_show_initials(True)
M po/de.po => po/de.po +62 -6
@@ 7,7 7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Melon 0.1.2\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-03-15 08:56+0100\n"
+"POT-Creation-Date: 2024-03-15 09:36+0100\n"
"PO-Revision-Date: 2024-03-01 11:23+0000\n"
"Last-Translator: Anonymous <noreply@weblate.org>\n"
"Language-Team: German <https://translate.codeberg.org/projects/melon/melon-"
@@ 83,7 83,7 @@ msgstr ""
#: ../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:101
-#: ../melon/player/playlist.py:125 ../melon/player/playlist.py:133
+#: ../melon/player/playlist.py:158 ../melon/player/playlist.py:165
#: ../melon/playlist/__init__.py:50
msgid "*crickets chirping*"
msgstr "*Grillen zirpen*"
@@ 353,19 353,75 @@ msgstr "Füge dieses Video einer Wiedergabeliste hinzu"
msgid "Video could not be loaded"
msgstr ""
-#: ../melon/player/__init__.py:139 ../melon/player/__init__.py:173
-#: ../melon/player/playlist.py:43
+#: ../melon/player/__init__.py:140 ../melon/player/__init__.py:174
+#: ../melon/player/playlist.py:51
msgid "Loading..."
msgstr ""
-#: ../melon/player/playlist.py:127
+#: ../melon/player/playlist.py:159
msgid "This playlist is empty"
msgstr ""
-#: ../melon/player/playlist.py:135
+#: ../melon/player/playlist.py:166
msgid "There was an error loading the playlist"
msgstr ""
+#: ../melon/player/playlist.py:218
+msgid "Previous"
+msgstr ""
+
+#: ../melon/player/playlist.py:219
+msgid "Play video that comes before this one in the playlist"
+msgstr ""
+
+#: ../melon/player/playlist.py:230
+msgid "Next"
+msgstr ""
+
+#: ../melon/player/playlist.py:231
+msgid "Play video that comes after this one in the playlist"
+msgstr ""
+
+#: ../melon/player/playlist.py:240
+msgid "Skip"
+msgstr ""
+
+#: ../melon/player/playlist.py:241
+msgid "Skip this video and pick a new one at random"
+msgstr ""
+
+#: ../melon/player/playlist.py:250
+msgid "Shuffle"
+msgstr ""
+
+#: ../melon/player/playlist.py:251
+msgid "Chooses the next video at random"
+msgstr ""
+
+#: ../melon/player/playlist.py:261
+msgid "Repeat current video"
+msgstr ""
+
+#: ../melon/player/playlist.py:262
+msgid "Puts this video on loop"
+msgstr ""
+
+#: ../melon/player/playlist.py:276
+msgid "Repeat playlist"
+msgstr ""
+
+#: ../melon/player/playlist.py:277
+msgid "Start playling the playlist from the beginning after reaching the end"
+msgstr ""
+
+#: ../melon/player/playlist.py:287
+msgid "Playlist Content"
+msgstr ""
+
+#: ../melon/player/playlist.py:288
+msgid "Click on videos to continue playing the playlist from there"
+msgstr ""
+
#: ../melon/playlist/__init__.py:39
msgid "Edit"
msgstr "Bearbeiten"
M po/fa.po => po/fa.po +62 -6
@@ 7,7 7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Melon 0.1.2\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-03-15 08:56+0100\n"
+"POT-Creation-Date: 2024-03-15 09:36+0100\n"
"PO-Revision-Date: 2024-03-05 05:13+0000\n"
"Last-Translator: sohrabbehdani <behdanisohrab@gmail.com>\n"
"Language-Team: Persian <https://translate.codeberg.org/projects/melon/melon-"
@@ 78,7 78,7 @@ msgstr ""
#: ../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:101
-#: ../melon/player/playlist.py:125 ../melon/player/playlist.py:133
+#: ../melon/player/playlist.py:158 ../melon/player/playlist.py:165
#: ../melon/playlist/__init__.py:50
msgid "*crickets chirping*"
msgstr ""
@@ 341,19 341,75 @@ msgstr "افزودن این ویدئو به لیستپخش"
msgid "Video could not be loaded"
msgstr ""
-#: ../melon/player/__init__.py:139 ../melon/player/__init__.py:173
-#: ../melon/player/playlist.py:43
+#: ../melon/player/__init__.py:140 ../melon/player/__init__.py:174
+#: ../melon/player/playlist.py:51
msgid "Loading..."
msgstr ""
-#: ../melon/player/playlist.py:127
+#: ../melon/player/playlist.py:159
msgid "This playlist is empty"
msgstr ""
-#: ../melon/player/playlist.py:135
+#: ../melon/player/playlist.py:166
msgid "There was an error loading the playlist"
msgstr ""
+#: ../melon/player/playlist.py:218
+msgid "Previous"
+msgstr ""
+
+#: ../melon/player/playlist.py:219
+msgid "Play video that comes before this one in the playlist"
+msgstr ""
+
+#: ../melon/player/playlist.py:230
+msgid "Next"
+msgstr ""
+
+#: ../melon/player/playlist.py:231
+msgid "Play video that comes after this one in the playlist"
+msgstr ""
+
+#: ../melon/player/playlist.py:240
+msgid "Skip"
+msgstr ""
+
+#: ../melon/player/playlist.py:241
+msgid "Skip this video and pick a new one at random"
+msgstr ""
+
+#: ../melon/player/playlist.py:250
+msgid "Shuffle"
+msgstr ""
+
+#: ../melon/player/playlist.py:251
+msgid "Chooses the next video at random"
+msgstr ""
+
+#: ../melon/player/playlist.py:261
+msgid "Repeat current video"
+msgstr ""
+
+#: ../melon/player/playlist.py:262
+msgid "Puts this video on loop"
+msgstr ""
+
+#: ../melon/player/playlist.py:276
+msgid "Repeat playlist"
+msgstr ""
+
+#: ../melon/player/playlist.py:277
+msgid "Start playling the playlist from the beginning after reaching the end"
+msgstr ""
+
+#: ../melon/player/playlist.py:287
+msgid "Playlist Content"
+msgstr ""
+
+#: ../melon/player/playlist.py:288
+msgid "Click on videos to continue playing the playlist from there"
+msgstr ""
+
#: ../melon/playlist/__init__.py:39
msgid "Edit"
msgstr ""
M po/melon.pot => po/melon.pot +62 -6
@@ 8,7 8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Melon 0.1.3\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-03-15 08:56+0100\n"
+"POT-Creation-Date: 2024-03-15 09:36+0100\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"
@@ 77,7 77,7 @@ msgstr ""
#: ../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:101
-#: ../melon/player/playlist.py:125 ../melon/player/playlist.py:133
+#: ../melon/player/playlist.py:158 ../melon/player/playlist.py:165
#: ../melon/playlist/__init__.py:50
msgid "*crickets chirping*"
msgstr ""
@@ 340,19 340,75 @@ msgstr ""
msgid "Video could not be loaded"
msgstr ""
-#: ../melon/player/__init__.py:139 ../melon/player/__init__.py:173
-#: ../melon/player/playlist.py:43
+#: ../melon/player/__init__.py:140 ../melon/player/__init__.py:174
+#: ../melon/player/playlist.py:51
msgid "Loading..."
msgstr ""
-#: ../melon/player/playlist.py:127
+#: ../melon/player/playlist.py:159
msgid "This playlist is empty"
msgstr ""
-#: ../melon/player/playlist.py:135
+#: ../melon/player/playlist.py:166
msgid "There was an error loading the playlist"
msgstr ""
+#: ../melon/player/playlist.py:218
+msgid "Previous"
+msgstr ""
+
+#: ../melon/player/playlist.py:219
+msgid "Play video that comes before this one in the playlist"
+msgstr ""
+
+#: ../melon/player/playlist.py:230
+msgid "Next"
+msgstr ""
+
+#: ../melon/player/playlist.py:231
+msgid "Play video that comes after this one in the playlist"
+msgstr ""
+
+#: ../melon/player/playlist.py:240
+msgid "Skip"
+msgstr ""
+
+#: ../melon/player/playlist.py:241
+msgid "Skip this video and pick a new one at random"
+msgstr ""
+
+#: ../melon/player/playlist.py:250
+msgid "Shuffle"
+msgstr ""
+
+#: ../melon/player/playlist.py:251
+msgid "Chooses the next video at random"
+msgstr ""
+
+#: ../melon/player/playlist.py:261
+msgid "Repeat current video"
+msgstr ""
+
+#: ../melon/player/playlist.py:262
+msgid "Puts this video on loop"
+msgstr ""
+
+#: ../melon/player/playlist.py:276
+msgid "Repeat playlist"
+msgstr ""
+
+#: ../melon/player/playlist.py:277
+msgid "Start playling the playlist from the beginning after reaching the end"
+msgstr ""
+
+#: ../melon/player/playlist.py:287
+msgid "Playlist Content"
+msgstr ""
+
+#: ../melon/player/playlist.py:288
+msgid "Click on videos to continue playing the playlist from there"
+msgstr ""
+
#: ../melon/playlist/__init__.py:39
msgid "Edit"
msgstr ""