From 7137c78c39ea3412f6ad8ac6b49b6070839ac655 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Wed, 13 Mar 2024 07:43:16 +0100 Subject: [PATCH] add playlist control options - easily switch songs by selecting one from the playlist content - enable shuffle mode, single-song-repeat or repeat-all (if shuffle is disabled) - navigate forwards/backwards (if shuffle is disabled) - skip songs (if shuffle is enabled) --- melon/player/__init__.py | 1 + melon/player/playlist.py | 170 ++++++++++++++++++++++++++++++++++++-- melon/widgets/feeditem.py | 21 +++-- po/de.po | 68 +++++++++++++-- po/fa.po | 68 +++++++++++++-- po/melon.pot | 68 +++++++++++++-- 6 files changed, 363 insertions(+), 33 deletions(-) diff --git a/melon/player/__init__.py b/melon/player/__init__.py index 12077ed..b029a33 100644 --- a/melon/player/__init__.py +++ b/melon/player/__init__.py @@ -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) diff --git a/melon/player/playlist.py b/melon/player/playlist.py index 229a5d9..d81995f 100644 --- a/melon/player/playlist.py +++ b/melon/player/playlist.py @@ -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 + 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) diff --git a/melon/widgets/feeditem.py b/melon/widgets/feeditem.py index 819f126..589bad3 100644 --- a/melon/widgets/feeditem.py +++ b/melon/widgets/feeditem.py @@ -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) diff --git a/po/de.po b/po/de.po index b1dbf8a..4211fc4 100644 --- a/po/de.po +++ b/po/de.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Melon 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-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 \n" "Language-Team: German \n" "Language-Team: Persian \n" "Language-Team: LANGUAGE \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 "" -- 2.38.5