From 0c6387a92fe7661a0bb0e1ff17f4160bee30db0e Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Fri, 2 Aug 2024 13:44:27 +0300 Subject: [PATCH] media browser: expose remote playlists together with local playlists This is similar to how they are shown in the app UI. --- .../mediabrowser/MediaBrowserConnector.java | 163 ++++++++++++++---- 1 file changed, 126 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 7d67e3865..99099e308 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -12,6 +12,7 @@ import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; +import android.util.Pair; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; @@ -28,23 +29,30 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; -import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; +import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.local.bookmark.MergedPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; +import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.IntStream; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.Disposable; @@ -61,6 +69,7 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep private AppDatabase database; private LocalPlaylistManager localPlaylistManager; + private RemotePlaylistManager remotePlaylistManager; private Disposable prepareOrPlayDisposable; public MediaBrowserConnector(@NonNull final PlayerService playerService) { @@ -94,6 +103,11 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep @NonNull private static final String ID_STREAM = ID_ROOT + "/stream"; + @NonNull + private static final String ID_LOCAL = "local"; + @NonNull + private static final String ID_REMOTE = "remote"; + @NonNull private MediaItem createRootMediaItem(@Nullable final String mediaId, final String folderName, @@ -117,11 +131,12 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep } @NonNull - private MediaItem createPlaylistMediaItem(@NonNull final PlaylistMetadataEntry playlist) { + private MediaItem createPlaylistMediaItem(@NonNull final PlaylistLocalItem playlist) { final var builder = new MediaDescriptionCompat.Builder(); - builder.setMediaId(createMediaIdForPlaylist(playlist.getUid())) - .setTitle(playlist.name) - .setIconUri(Uri.parse(playlist.thumbnailUrl)); + final boolean remote = playlist instanceof PlaylistRemoteEntity; + builder.setMediaId(createMediaIdForPlaylist(remote, playlist.getUid())) + .setTitle(playlist.getOrderingName()) + .setIconUri(Uri.parse(playlist.getThumbnailUrl())); final Bundle extras = new Bundle(); extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, @@ -131,16 +146,16 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep } @NonNull - private String createMediaIdForPlaylist(final long playlistId) { - return ID_BOOKMARKS + '/' + playlistId; + private String createMediaIdForPlaylist(final boolean remote, final long playlistId) { + return ID_BOOKMARKS + '/' + (remote ? ID_REMOTE : ID_LOCAL) + '/' + playlistId; } @NonNull - private MediaItem createPlaylistStreamMediaItem(final long playlistId, - @NonNull final PlaylistStreamEntry item, - final int index) { + private MediaItem createLocalPlaylistStreamMediaItem(final long playlistId, + @NonNull final PlaylistStreamEntry item, + final int index) { final var builder = new MediaDescriptionCompat.Builder(); - builder.setMediaId(createMediaIdForPlaylistIndex(playlistId, index)) + builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index)) .setTitle(item.getStreamEntity().getTitle()) .setSubtitle(item.getStreamEntity().getUploader()) .setIconUri(Uri.parse(item.getStreamEntity().getThumbnailUrl())); @@ -149,8 +164,25 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep } @NonNull - private String createMediaIdForPlaylistIndex(final long playlistId, final int index) { - return createMediaIdForPlaylist(playlistId) + '/' + index; + private MediaItem createRemotePlaylistStreamMediaItem(final long playlistId, + @NonNull final StreamInfoItem item, + final int index) { + final var builder = new MediaDescriptionCompat.Builder(); + builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index)) + .setTitle(item.getName()) + .setSubtitle(item.getUploaderName()); + final var thumbnails = item.getThumbnails(); + if (!thumbnails.isEmpty()) { + builder.setIconUri(Uri.parse(thumbnails.get(0).getUrl())); + } + + return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); + } + + @NonNull + private String createMediaIdForPlaylistIndex(final boolean remote, final long playlistId, + final int index) { + return createMediaIdForPlaylist(remote, playlistId) + '/' + index; } @Nullable @@ -186,12 +218,16 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep final List path = parentIdUri.getPathSegments(); if (path.size() == 2) { return populateBookmarks(); - } else if (path.size() == 3) { - final long playlistId = Long.parseLong(path.get(2)); - return populatePlaylist(playlistId); - } else { - Log.w(TAG, "Unknown playlist URI: " + parentId); + } else if (path.size() == 4) { + final String localOrRemote = path.get(2); + final long playlistId = Long.parseLong(path.get(3)); + if (localOrRemote.equals(ID_LOCAL)) { + return populateLocalPlaylist(playlistId); + } else if (localOrRemote.equals(ID_REMOTE)) { + return populateRemotePlaylist(playlistId); + } } + Log.w(TAG, "Unknown playlist URI: " + parentId); } else if (parentId.equals(ID_HISTORY)) { return populateHistory(); } @@ -224,17 +260,21 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep return database; } - private LocalPlaylistManager getPlaylistManager() { + private Flowable> getPlaylists() { if (localPlaylistManager == null) { localPlaylistManager = new LocalPlaylistManager(getDatabase()); } - return localPlaylistManager; + if (remotePlaylistManager == null) { + remotePlaylistManager = new RemotePlaylistManager(getDatabase()); + } + return MergedPlaylistManager.getMergedOrderedPlaylists(localPlaylistManager, + remotePlaylistManager); } @Nullable Disposable bookmarksNotificationsDisposable; private void setupBookmarksNotifications() { - bookmarksNotificationsDisposable = getPlaylistManager().getPlaylists().subscribe( + bookmarksNotificationsDisposable = getPlaylists().subscribe( playlistMetadataEntries -> playerService.notifyChildrenChanged(ID_BOOKMARKS)); } @@ -249,25 +289,59 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep // available in Android API 34 and not currently available with desugaring @SuppressWarnings("squid:S6204") private Single> populateBookmarks() { - final var playlists = getPlaylistManager().getPlaylists().firstOrError(); + final var playlists = getPlaylists().firstOrError(); return playlists.map(playlist -> playlist.stream() .map(this::createPlaylistMediaItem) .collect(Collectors.toList())); } - private Single> populatePlaylist(final long playlistId) { - final var playlist = getPlaylistManager().getPlaylistStreams(playlistId).firstOrError(); + private Single> populateLocalPlaylist(final long playlistId) { + final var playlist = localPlaylistManager.getPlaylistStreams(playlistId).firstOrError(); return playlist.map(items -> { final List results = new ArrayList<>(); int index = 0; for (final PlaylistStreamEntry item : items) { - results.add(createPlaylistStreamMediaItem(playlistId, item, index)); + results.add(createLocalPlaylistStreamMediaItem(playlistId, item, index)); ++index; } return results; }); } + private Single>> getRemotePlaylist(final long playlistId) { + final var playlistFlow = remotePlaylistManager.getPlaylist(playlistId).firstOrError(); + return playlistFlow.flatMap(item -> { + final var playlist = item.get(0); + final var playlistInfo = ExtractorHelper.getPlaylistInfo(playlist.getServiceId(), + playlist.getUrl(), false); + return playlistInfo.flatMap(info -> { + final var infoItemsPage = info.getRelatedItems(); + + if (!info.getErrors().isEmpty()) { + final List errors = new ArrayList<>(info.getErrors()); + + errors.removeIf(ContentNotSupportedException.class::isInstance); + + if (!errors.isEmpty()) { + return Single.error(errors.get(0)); + } + } + + return Single.just(IntStream.range(0, infoItemsPage.size()) + .mapToObj(i -> Pair.create(infoItemsPage.get(i), i)) + .toList()); + }); + }); + } + + private Single> populateRemotePlaylist(final long playlistId) { + return getRemotePlaylist(playlistId).map(items -> + items.stream().map(pair -> + createRemotePlaylistStreamMediaItem(playlistId, pair.first, pair.second) + ).toList() + ); + } + private void playbackError(@StringRes final int resId, final int code) { playerService.stopForImmediateReusing(); sessionConnector.setCustomErrorMessage(playerService.getString(resId), code); @@ -277,6 +351,24 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep playbackError(errorInfo.getMessageStringId(), PlaybackStateCompat.ERROR_CODE_APP_ERROR); } + private Single extractLocalPlayQueue(final long playlistId, final int index) { + return localPlaylistManager.getPlaylistStreams(playlistId) + .firstOrError() + .map(items -> { + final List infoItems = items.stream() + .map(PlaylistStreamEntry::toStreamInfoItem) + .collect(Collectors.toList()); + return new SinglePlayQueue(infoItems, index); + }); + } + + private Single extractRemotePlayQueue(final long playlistId, final int index) { + return getRemotePlaylist(playlistId).map(items -> { + final var infoItems = items.stream().map(pair -> pair.first).toList(); + return new SinglePlayQueue(infoItems, index); + }); + } + private Single extractPlayQueueFromMediaId(final String mediaId) { final Uri mediaIdUri = Uri.parse(mediaId); if (mediaIdUri == null) { @@ -285,19 +377,16 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep final List path = mediaIdUri.getPathSegments(); - if (mediaId.startsWith(ID_BOOKMARKS) && path.size() == 4) { - final long playlistId = Long.parseLong(path.get(2)); - final int index = Integer.parseInt(path.get(3)); + if (mediaId.startsWith(ID_BOOKMARKS) && path.size() == 5) { + final String localOrRemote = path.get(2); + final long playlistId = Long.parseLong(path.get(3)); + final int index = Integer.parseInt(path.get(4)); - return getPlaylistManager() - .getPlaylistStreams(playlistId) - .firstOrError() - .map(items -> { - final List infoItems = items.stream() - .map(PlaylistStreamEntry::toStreamInfoItem) - .collect(Collectors.toList()); - return new SinglePlayQueue(infoItems, index); - }); + if (localOrRemote.equals(ID_LOCAL)) { + return extractLocalPlayQueue(playlistId, index); + } else { + return extractRemotePlayQueue(playlistId, index); + } } else if (mediaId.startsWith(ID_STREAM) && path.size() == 3) { final long streamId = Long.parseLong(path.get(2)); return getDatabase().streamHistoryDAO().getHistory()