mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-02-02 12:19:16 +00:00
media browser: expose remote playlists together with local playlists
This is similar to how they are shown in the app UI.
This commit is contained in:
parent
f53ee4b65e
commit
0c6387a92f
@ -12,6 +12,7 @@ import android.support.v4.media.MediaDescriptionCompat;
|
|||||||
import android.support.v4.media.session.MediaSessionCompat;
|
import android.support.v4.media.session.MediaSessionCompat;
|
||||||
import android.support.v4.media.session.PlaybackStateCompat;
|
import android.support.v4.media.session.PlaybackStateCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@ -28,23 +29,30 @@ import org.schabi.newpipe.R;
|
|||||||
import org.schabi.newpipe.database.AppDatabase;
|
import org.schabi.newpipe.database.AppDatabase;
|
||||||
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
|
||||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
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.PlaylistStreamEntry;
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||||
import org.schabi.newpipe.error.ErrorInfo;
|
import org.schabi.newpipe.error.ErrorInfo;
|
||||||
import org.schabi.newpipe.error.UserAction;
|
import org.schabi.newpipe.error.UserAction;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
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.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.local.bookmark.MergedPlaylistManager;
|
||||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
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.PlayerService;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
@ -61,6 +69,7 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
|
|||||||
|
|
||||||
private AppDatabase database;
|
private AppDatabase database;
|
||||||
private LocalPlaylistManager localPlaylistManager;
|
private LocalPlaylistManager localPlaylistManager;
|
||||||
|
private RemotePlaylistManager remotePlaylistManager;
|
||||||
private Disposable prepareOrPlayDisposable;
|
private Disposable prepareOrPlayDisposable;
|
||||||
|
|
||||||
public MediaBrowserConnector(@NonNull final PlayerService playerService) {
|
public MediaBrowserConnector(@NonNull final PlayerService playerService) {
|
||||||
@ -94,6 +103,11 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
|
|||||||
@NonNull
|
@NonNull
|
||||||
private static final String ID_STREAM = ID_ROOT + "/stream";
|
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
|
@NonNull
|
||||||
private MediaItem createRootMediaItem(@Nullable final String mediaId,
|
private MediaItem createRootMediaItem(@Nullable final String mediaId,
|
||||||
final String folderName,
|
final String folderName,
|
||||||
@ -117,11 +131,12 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private MediaItem createPlaylistMediaItem(@NonNull final PlaylistMetadataEntry playlist) {
|
private MediaItem createPlaylistMediaItem(@NonNull final PlaylistLocalItem playlist) {
|
||||||
final var builder = new MediaDescriptionCompat.Builder();
|
final var builder = new MediaDescriptionCompat.Builder();
|
||||||
builder.setMediaId(createMediaIdForPlaylist(playlist.getUid()))
|
final boolean remote = playlist instanceof PlaylistRemoteEntity;
|
||||||
.setTitle(playlist.name)
|
builder.setMediaId(createMediaIdForPlaylist(remote, playlist.getUid()))
|
||||||
.setIconUri(Uri.parse(playlist.thumbnailUrl));
|
.setTitle(playlist.getOrderingName())
|
||||||
|
.setIconUri(Uri.parse(playlist.getThumbnailUrl()));
|
||||||
|
|
||||||
final Bundle extras = new Bundle();
|
final Bundle extras = new Bundle();
|
||||||
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||||
@ -131,16 +146,16 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private String createMediaIdForPlaylist(final long playlistId) {
|
private String createMediaIdForPlaylist(final boolean remote, final long playlistId) {
|
||||||
return ID_BOOKMARKS + '/' + playlistId;
|
return ID_BOOKMARKS + '/' + (remote ? ID_REMOTE : ID_LOCAL) + '/' + playlistId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private MediaItem createPlaylistStreamMediaItem(final long playlistId,
|
private MediaItem createLocalPlaylistStreamMediaItem(final long playlistId,
|
||||||
@NonNull final PlaylistStreamEntry item,
|
@NonNull final PlaylistStreamEntry item,
|
||||||
final int index) {
|
final int index) {
|
||||||
final var builder = new MediaDescriptionCompat.Builder();
|
final var builder = new MediaDescriptionCompat.Builder();
|
||||||
builder.setMediaId(createMediaIdForPlaylistIndex(playlistId, index))
|
builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
|
||||||
.setTitle(item.getStreamEntity().getTitle())
|
.setTitle(item.getStreamEntity().getTitle())
|
||||||
.setSubtitle(item.getStreamEntity().getUploader())
|
.setSubtitle(item.getStreamEntity().getUploader())
|
||||||
.setIconUri(Uri.parse(item.getStreamEntity().getThumbnailUrl()));
|
.setIconUri(Uri.parse(item.getStreamEntity().getThumbnailUrl()));
|
||||||
@ -149,8 +164,25 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private String createMediaIdForPlaylistIndex(final long playlistId, final int index) {
|
private MediaItem createRemotePlaylistStreamMediaItem(final long playlistId,
|
||||||
return createMediaIdForPlaylist(playlistId) + '/' + index;
|
@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
|
@Nullable
|
||||||
@ -186,12 +218,16 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
|
|||||||
final List<String> path = parentIdUri.getPathSegments();
|
final List<String> path = parentIdUri.getPathSegments();
|
||||||
if (path.size() == 2) {
|
if (path.size() == 2) {
|
||||||
return populateBookmarks();
|
return populateBookmarks();
|
||||||
} else if (path.size() == 3) {
|
} else if (path.size() == 4) {
|
||||||
final long playlistId = Long.parseLong(path.get(2));
|
final String localOrRemote = path.get(2);
|
||||||
return populatePlaylist(playlistId);
|
final long playlistId = Long.parseLong(path.get(3));
|
||||||
} else {
|
if (localOrRemote.equals(ID_LOCAL)) {
|
||||||
Log.w(TAG, "Unknown playlist URI: " + parentId);
|
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)) {
|
} else if (parentId.equals(ID_HISTORY)) {
|
||||||
return populateHistory();
|
return populateHistory();
|
||||||
}
|
}
|
||||||
@ -224,17 +260,21 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
|
|||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocalPlaylistManager getPlaylistManager() {
|
private Flowable<List<PlaylistLocalItem>> getPlaylists() {
|
||||||
if (localPlaylistManager == null) {
|
if (localPlaylistManager == null) {
|
||||||
localPlaylistManager = new LocalPlaylistManager(getDatabase());
|
localPlaylistManager = new LocalPlaylistManager(getDatabase());
|
||||||
}
|
}
|
||||||
return localPlaylistManager;
|
if (remotePlaylistManager == null) {
|
||||||
|
remotePlaylistManager = new RemotePlaylistManager(getDatabase());
|
||||||
|
}
|
||||||
|
return MergedPlaylistManager.getMergedOrderedPlaylists(localPlaylistManager,
|
||||||
|
remotePlaylistManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable Disposable bookmarksNotificationsDisposable;
|
@Nullable Disposable bookmarksNotificationsDisposable;
|
||||||
|
|
||||||
private void setupBookmarksNotifications() {
|
private void setupBookmarksNotifications() {
|
||||||
bookmarksNotificationsDisposable = getPlaylistManager().getPlaylists().subscribe(
|
bookmarksNotificationsDisposable = getPlaylists().subscribe(
|
||||||
playlistMetadataEntries -> playerService.notifyChildrenChanged(ID_BOOKMARKS));
|
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
|
// available in Android API 34 and not currently available with desugaring
|
||||||
@SuppressWarnings("squid:S6204")
|
@SuppressWarnings("squid:S6204")
|
||||||
private Single<List<MediaItem>> populateBookmarks() {
|
private Single<List<MediaItem>> populateBookmarks() {
|
||||||
final var playlists = getPlaylistManager().getPlaylists().firstOrError();
|
final var playlists = getPlaylists().firstOrError();
|
||||||
return playlists.map(playlist -> playlist.stream()
|
return playlists.map(playlist -> playlist.stream()
|
||||||
.map(this::createPlaylistMediaItem)
|
.map(this::createPlaylistMediaItem)
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Single<List<MediaItem>> populatePlaylist(final long playlistId) {
|
private Single<List<MediaItem>> populateLocalPlaylist(final long playlistId) {
|
||||||
final var playlist = getPlaylistManager().getPlaylistStreams(playlistId).firstOrError();
|
final var playlist = localPlaylistManager.getPlaylistStreams(playlistId).firstOrError();
|
||||||
return playlist.map(items -> {
|
return playlist.map(items -> {
|
||||||
final List<MediaItem> results = new ArrayList<>();
|
final List<MediaItem> results = new ArrayList<>();
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (final PlaylistStreamEntry item : items) {
|
for (final PlaylistStreamEntry item : items) {
|
||||||
results.add(createPlaylistStreamMediaItem(playlistId, item, index));
|
results.add(createLocalPlaylistStreamMediaItem(playlistId, item, index));
|
||||||
++index;
|
++index;
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Single<List<Pair<StreamInfoItem, Integer>>> 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<Throwable> 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<List<MediaItem>> 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) {
|
private void playbackError(@StringRes final int resId, final int code) {
|
||||||
playerService.stopForImmediateReusing();
|
playerService.stopForImmediateReusing();
|
||||||
sessionConnector.setCustomErrorMessage(playerService.getString(resId), code);
|
sessionConnector.setCustomErrorMessage(playerService.getString(resId), code);
|
||||||
@ -277,6 +351,24 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
|
|||||||
playbackError(errorInfo.getMessageStringId(), PlaybackStateCompat.ERROR_CODE_APP_ERROR);
|
playbackError(errorInfo.getMessageStringId(), PlaybackStateCompat.ERROR_CODE_APP_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Single<PlayQueue> extractLocalPlayQueue(final long playlistId, final int index) {
|
||||||
|
return localPlaylistManager.getPlaylistStreams(playlistId)
|
||||||
|
.firstOrError()
|
||||||
|
.map(items -> {
|
||||||
|
final List<StreamInfoItem> infoItems = items.stream()
|
||||||
|
.map(PlaylistStreamEntry::toStreamInfoItem)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return new SinglePlayQueue(infoItems, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Single<PlayQueue> 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<PlayQueue> extractPlayQueueFromMediaId(final String mediaId) {
|
private Single<PlayQueue> extractPlayQueueFromMediaId(final String mediaId) {
|
||||||
final Uri mediaIdUri = Uri.parse(mediaId);
|
final Uri mediaIdUri = Uri.parse(mediaId);
|
||||||
if (mediaIdUri == null) {
|
if (mediaIdUri == null) {
|
||||||
@ -285,19 +377,16 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
|
|||||||
|
|
||||||
final List<String> path = mediaIdUri.getPathSegments();
|
final List<String> path = mediaIdUri.getPathSegments();
|
||||||
|
|
||||||
if (mediaId.startsWith(ID_BOOKMARKS) && path.size() == 4) {
|
if (mediaId.startsWith(ID_BOOKMARKS) && path.size() == 5) {
|
||||||
final long playlistId = Long.parseLong(path.get(2));
|
final String localOrRemote = path.get(2);
|
||||||
final int index = Integer.parseInt(path.get(3));
|
final long playlistId = Long.parseLong(path.get(3));
|
||||||
|
final int index = Integer.parseInt(path.get(4));
|
||||||
|
|
||||||
return getPlaylistManager()
|
if (localOrRemote.equals(ID_LOCAL)) {
|
||||||
.getPlaylistStreams(playlistId)
|
return extractLocalPlayQueue(playlistId, index);
|
||||||
.firstOrError()
|
} else {
|
||||||
.map(items -> {
|
return extractRemotePlayQueue(playlistId, index);
|
||||||
final List<StreamInfoItem> infoItems = items.stream()
|
}
|
||||||
.map(PlaylistStreamEntry::toStreamInfoItem)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
return new SinglePlayQueue(infoItems, index);
|
|
||||||
});
|
|
||||||
} else if (mediaId.startsWith(ID_STREAM) && path.size() == 3) {
|
} else if (mediaId.startsWith(ID_STREAM) && path.size() == 3) {
|
||||||
final long streamId = Long.parseLong(path.get(2));
|
final long streamId = Long.parseLong(path.get(2));
|
||||||
return getDatabase().streamHistoryDAO().getHistory()
|
return getDatabase().streamHistoryDAO().getHistory()
|
||||||
|
Loading…
Reference in New Issue
Block a user