~comcloudway/melon

7137c78c39ea3412f6ad8ac6b49b6070839ac655 — Jakob Meier 6 months ago 6d74d63
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)
6 files changed, 363 insertions(+), 33 deletions(-)

M melon/player/__init__.py
M melon/player/playlist.py
M melon/widgets/feeditem.py
M po/de.po
M po/fa.po
M po/melon.pot
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("&","&amp;"))
            self.set_subtitle(unidecode(resource.channel[0]).replace("&","&amp;"))
            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("&","&amp;"))


@@ 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("&","&amp;"))
            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("&","&amp;"))


@@ 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("&","&amp;"))
            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 ""