1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-01-08 16:30:34 +00:00

media browser: support searching

Also improve parser code to simplify passing URLs within a media ID.
This commit is contained in:
Haggai Eran 2024-08-04 09:01:46 +03:00 committed by Siddhesh Naik
parent 0c6387a92f
commit 189c70f9d3
2 changed files with 339 additions and 64 deletions

View File

@ -229,6 +229,13 @@ public final class PlayerService extends MediaBrowserServiceCompat {
compositeDisposableLoadChildren.add(disposable); compositeDisposableLoadChildren.add(disposable);
} }
@Override
public void onSearch(@NonNull final String query,
final Bundle extras,
@NonNull final Result<List<MediaItem>> result) {
mediaBrowserConnector.onSearch(query, result);
}
public static final class LocalBinder extends Binder { public static final class LocalBinder extends Binder {
private final WeakReference<PlayerService> playerService; private final WeakReference<PlayerService> playerService;

View File

@ -24,6 +24,7 @@ import androidx.media.utils.MediaConstants;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.AppDatabase;
@ -34,20 +35,32 @@ import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; 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.InfoItem;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
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.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
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.bookmark.MergedPlaylistManager;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.SearchInfo;
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.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@ -56,6 +69,9 @@ 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;
import io.reactivex.rxjava3.core.SingleSource;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPreparer { public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPreparer {
private static final String TAG = MediaBrowserConnector.class.getSimpleName(); private static final String TAG = MediaBrowserConnector.class.getSimpleName();
@ -71,6 +87,7 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
private LocalPlaylistManager localPlaylistManager; private LocalPlaylistManager localPlaylistManager;
private RemotePlaylistManager remotePlaylistManager; private RemotePlaylistManager remotePlaylistManager;
private Disposable prepareOrPlayDisposable; private Disposable prepareOrPlayDisposable;
private Disposable searchDisposable;
public MediaBrowserConnector(@NonNull final PlayerService playerService) { public MediaBrowserConnector(@NonNull final PlayerService playerService) {
this.playerService = playerService; this.playerService = playerService;
@ -95,18 +112,28 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
} }
@NonNull @NonNull
private static final String ID_ROOT = "//${BuildConfig.APPLICATION_ID}/r"; private static final String ID_AUTHORITY = BuildConfig.APPLICATION_ID;
@NonNull @NonNull
private static final String ID_BOOKMARKS = ID_ROOT + "/playlists"; private static final String ID_ROOT = "//" + ID_AUTHORITY;
@NonNull @NonNull
private static final String ID_HISTORY = ID_ROOT + "/history"; private static final String ID_BOOKMARKS = "playlists";
@NonNull @NonNull
private static final String ID_STREAM = ID_ROOT + "/stream"; private static final String ID_HISTORY = "history";
@NonNull
private static final String ID_INFO_ITEM = "item";
@NonNull @NonNull
private static final String ID_LOCAL = "local"; private static final String ID_LOCAL = "local";
@NonNull @NonNull
private static final String ID_REMOTE = "remote"; private static final String ID_REMOTE = "remote";
@NonNull
private static final String ID_URL = "url";
@NonNull
private static final String ID_STREAM = "stream";
@NonNull
private static final String ID_PLAYLIST = "playlist";
@NonNull
private static final String ID_CHANNEL = "channel";
@NonNull @NonNull
private MediaItem createRootMediaItem(@Nullable final String mediaId, private MediaItem createRootMediaItem(@Nullable final String mediaId,
@ -134,7 +161,7 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
private MediaItem createPlaylistMediaItem(@NonNull final PlaylistLocalItem playlist) { private MediaItem createPlaylistMediaItem(@NonNull final PlaylistLocalItem playlist) {
final var builder = new MediaDescriptionCompat.Builder(); final var builder = new MediaDescriptionCompat.Builder();
final boolean remote = playlist instanceof PlaylistRemoteEntity; final boolean remote = playlist instanceof PlaylistRemoteEntity;
builder.setMediaId(createMediaIdForPlaylist(remote, playlist.getUid())) builder.setMediaId(createMediaIdForInfoItem(remote, playlist.getUid()))
.setTitle(playlist.getOrderingName()) .setTitle(playlist.getOrderingName())
.setIconUri(Uri.parse(playlist.getThumbnailUrl())); .setIconUri(Uri.parse(playlist.getThumbnailUrl()));
@ -145,9 +172,82 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE); return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE);
} }
private MediaItem createInfoItemMediaItem(@NonNull final InfoItem item) {
final var builder = new MediaDescriptionCompat.Builder();
builder.setMediaId(createMediaIdForInfoItem(item))
.setTitle(item.getName());
switch (item.getInfoType()) {
case STREAM:
builder.setSubtitle(((StreamInfoItem) item).getUploaderName());
break;
case PLAYLIST:
builder.setSubtitle(((PlaylistInfoItem) item).getUploaderName());
break;
case CHANNEL:
builder.setSubtitle(((ChannelInfoItem) item).getDescription());
break;
default:
break;
}
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 @NonNull
private String createMediaIdForPlaylist(final boolean remote, final long playlistId) { private Uri.Builder buildMediaId() {
return ID_BOOKMARKS + '/' + (remote ? ID_REMOTE : ID_LOCAL) + '/' + playlistId; return new Uri.Builder().authority(ID_AUTHORITY);
}
@NonNull
private Uri.Builder buildPlaylistMediaId(final String playlistType) {
return buildMediaId()
.appendPath(ID_BOOKMARKS)
.appendPath(playlistType);
}
@NonNull
private Uri.Builder buildLocalPlaylistItemMediaId(final boolean remote, final long playlistId) {
return buildPlaylistMediaId(remote ? ID_REMOTE : ID_LOCAL)
.appendPath(Long.toString(playlistId));
}
private static String infoItemTypeToString(final InfoItem.InfoType type) {
return switch (type) {
case STREAM -> ID_STREAM;
case PLAYLIST -> ID_PLAYLIST;
case CHANNEL -> ID_CHANNEL;
default ->
throw new IllegalStateException("Unexpected value: " + type);
};
}
private static InfoItem.InfoType infoItemTypeFromString(final String type) {
return switch (type) {
case ID_STREAM -> InfoItem.InfoType.STREAM;
case ID_PLAYLIST -> InfoItem.InfoType.PLAYLIST;
case ID_CHANNEL -> InfoItem.InfoType.CHANNEL;
default ->
throw new IllegalStateException("Unexpected value: " + type);
};
}
@NonNull
private Uri.Builder buildInfoItemMediaId(@NonNull final InfoItem item) {
return buildMediaId()
.appendPath(ID_INFO_ITEM)
.appendPath(infoItemTypeToString(item.getInfoType()))
.appendPath(Integer.toString(item.getServiceId()))
.appendQueryParameter(ID_URL, item.getUrl());
}
@NonNull
private String createMediaIdForInfoItem(final boolean remote, final long playlistId) {
return buildLocalPlaylistItemMediaId(remote, playlistId)
.build().toString();
} }
@NonNull @NonNull
@ -182,7 +282,14 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
@NonNull @NonNull
private String createMediaIdForPlaylistIndex(final boolean remote, final long playlistId, private String createMediaIdForPlaylistIndex(final boolean remote, final long playlistId,
final int index) { final int index) {
return createMediaIdForPlaylist(remote, playlistId) + '/' + index; return buildLocalPlaylistItemMediaId(remote, playlistId)
.appendPath(Integer.toString(index))
.build().toString();
}
@NonNull
private String createMediaIdForInfoItem(@NonNull final InfoItem item) {
return buildInfoItemMediaId(item).build().toString();
} }
@Nullable @Nullable
@ -194,7 +301,10 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
clientPackageName, clientUid, rootHints)); clientPackageName, clientUid, rootHints));
} }
return new MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, null); final Bundle extras = new Bundle();
extras.putBoolean(
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true);
return new MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras);
} }
public Single<List<MediaItem>> onLoadChildren(@NonNull final String parentId) { public Single<List<MediaItem>> onLoadChildren(@NonNull final String parentId) {
@ -202,36 +312,56 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId)); Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId));
} }
final List<MediaItem> mediaItems = new ArrayList<>();
if (parentId.equals(ID_ROOT)) { try {
mediaItems.add(
createRootMediaItem(ID_BOOKMARKS,
playerService.getResources().getString(R.string.tab_bookmarks_short),
R.drawable.ic_bookmark_white));
mediaItems.add(
createRootMediaItem(ID_HISTORY,
playerService.getResources().getString(R.string.action_history),
R.drawable.ic_history_white));
} else if (parentId.startsWith(ID_BOOKMARKS)) {
final Uri parentIdUri = Uri.parse(parentId); final Uri parentIdUri = Uri.parse(parentId);
final List<String> path = parentIdUri.getPathSegments(); if (parentIdUri == null) {
if (path.size() == 2) { throw parseError();
return populateBookmarks();
} 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)) { final List<String> path = new ArrayList<>(parentIdUri.getPathSegments());
return populateHistory();
if (path.isEmpty()) {
final List<MediaItem> mediaItems = new ArrayList<>();
mediaItems.add(
createRootMediaItem(ID_BOOKMARKS,
playerService.getResources().getString(
R.string.tab_bookmarks_short),
R.drawable.ic_bookmark_white));
mediaItems.add(
createRootMediaItem(ID_HISTORY,
playerService.getResources().getString(R.string.action_history),
R.drawable.ic_history_white));
return Single.just(mediaItems);
}
final String uriType = path.get(0);
path.remove(0);
switch (uriType) {
case ID_BOOKMARKS:
if (path.isEmpty()) {
return populateBookmarks();
}
if (path.size() == 2) {
final String localOrRemote = path.get(0);
final long playlistId = Long.parseLong(path.get(1));
if (localOrRemote.equals(ID_LOCAL)) {
return populateLocalPlaylist(playlistId);
} else if (localOrRemote.equals(ID_REMOTE)) {
return populateRemotePlaylist(playlistId);
}
}
Log.w(TAG, "Unknown playlist URI: " + parentId);
throw parseError();
case ID_HISTORY:
return populateHistory();
default:
throw parseError();
}
} catch (final ContentNotAvailableException e) {
return Single.error(e);
} }
return Single.just(mediaItems);
} }
private Single<List<MediaItem>> populateHistory() { private Single<List<MediaItem>> populateHistory() {
@ -245,7 +375,11 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
@NonNull @NonNull
private MediaItem createHistoryMediaItem(@NonNull final StreamHistoryEntry streamHistoryEntry) { private MediaItem createHistoryMediaItem(@NonNull final StreamHistoryEntry streamHistoryEntry) {
final var builder = new MediaDescriptionCompat.Builder(); final var builder = new MediaDescriptionCompat.Builder();
builder.setMediaId(ID_STREAM + '/' + streamHistoryEntry.getStreamId()) final var mediaId = buildMediaId()
.appendPath(ID_HISTORY)
.appendPath(Long.toString(streamHistoryEntry.getStreamId()))
.build().toString();
builder.setMediaId(mediaId)
.setTitle(streamHistoryEntry.getStreamEntity().getTitle()) .setTitle(streamHistoryEntry.getStreamEntity().getTitle())
.setSubtitle(streamHistoryEntry.getStreamEntity().getUploader()) .setSubtitle(streamHistoryEntry.getStreamEntity().getUploader())
.setIconUri(Uri.parse(streamHistoryEntry.getStreamEntity().getThumbnailUrl())); .setIconUri(Uri.parse(streamHistoryEntry.getStreamEntity().getThumbnailUrl()));
@ -369,38 +503,119 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
}); });
} }
private static ContentNotAvailableException parseError() {
return new ContentNotAvailableException("Failed to parse media ID");
}
private Single<PlayQueue> extractPlayQueueFromMediaId(final String mediaId) { private Single<PlayQueue> extractPlayQueueFromMediaId(final String mediaId) {
final Uri mediaIdUri = Uri.parse(mediaId); try {
if (mediaIdUri == null) { final Uri mediaIdUri = Uri.parse(mediaId);
return Single.error(new ContentNotAvailableException("Media ID cannot be parsed")); if (mediaIdUri == null) {
} throw parseError();
final List<String> path = mediaIdUri.getPathSegments();
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));
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)); final List<String> path = new ArrayList<>(mediaIdUri.getPathSegments());
return getDatabase().streamHistoryDAO().getHistory()
.firstOrError() if (path.isEmpty()) {
.map(items -> { throw parseError();
final List<StreamInfoItem> infoItems = items.stream() }
.filter(it -> it.getStreamId() == streamId)
.map(StreamHistoryEntry::toStreamInfoItem) final String uriType = path.get(0);
.collect(Collectors.toList()); path.remove(0);
return new SinglePlayQueue(infoItems, 0);
}); return switch (uriType) {
case ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId(path,
mediaIdUri.getQueryParameter(ID_URL));
case ID_HISTORY -> extractPlayQueueFromHistoryMediaId(path);
case ID_INFO_ITEM -> extractPlayQueueFromInfoItemMediaId(path,
mediaIdUri.getQueryParameter(ID_URL));
default -> throw parseError();
};
} catch (final ContentNotAvailableException e) {
return Single.error(e);
}
}
private Single<PlayQueue>
extractPlayQueueFromPlaylistMediaId(
@NonNull final List<String> path,
@Nullable final String url) throws ContentNotAvailableException {
if (path.isEmpty()) {
throw parseError();
} }
return Single.error(new ContentNotAvailableException("Media ID cannot be parsed")); final String playlistType = path.get(0);
path.remove(0);
switch (playlistType) {
case ID_LOCAL, ID_REMOTE:
if (path.size() != 2) {
throw parseError();
}
final long playlistId = Long.parseLong(path.get(0));
final int index = Integer.parseInt(path.get(1));
return playlistType.equals(ID_LOCAL)
? extractLocalPlayQueue(playlistId, index)
: extractRemotePlayQueue(playlistId, index);
case ID_URL:
if (path.size() != 1) {
throw parseError();
}
final int serviceId = Integer.parseInt(path.get(0));
return ExtractorHelper.getPlaylistInfo(serviceId, url, false)
.map(PlaylistPlayQueue::new);
default:
throw parseError();
}
}
private Single<PlayQueue> extractPlayQueueFromHistoryMediaId(
final List<String> path) throws ContentNotAvailableException {
if (path.size() != 1) {
throw parseError();
}
final long streamId = Long.parseLong(path.get(0));
return getDatabase().streamHistoryDAO().getHistory()
.firstOrError()
.map(items -> {
final List<StreamInfoItem> infoItems = items.stream()
.filter(it -> it.getStreamId() == streamId)
.map(StreamHistoryEntry::toStreamInfoItem)
.collect(Collectors.toList());
return new SinglePlayQueue(infoItems, 0);
});
}
private static Single<PlayQueue> extractPlayQueueFromInfoItemMediaId(
final List<String> path, final String url) throws ContentNotAvailableException {
if (path.size() != 2) {
throw parseError();
}
final var infoItemType = infoItemTypeFromString(path.get(0));
final int serviceId = Integer.parseInt(path.get(1));
return switch (infoItemType) {
case STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false)
.map(SinglePlayQueue::new);
case PLAYLIST -> ExtractorHelper.getPlaylistInfo(serviceId, url, false)
.map(PlaylistPlayQueue::new);
case CHANNEL -> ExtractorHelper.getChannelInfo(serviceId, url, false)
.map(info -> {
final Optional<ListLinkHandler> playableTab = info.getTabs()
.stream()
.filter(ChannelTabHelper::isStreamsTab)
.findFirst();
if (playableTab.isPresent()) {
return new ChannelTabPlayQueue(serviceId,
new ListLinkHandler(playableTab.get()));
} else {
throw new ContentNotAvailableException("No streams tab found");
}
});
default -> throw parseError();
};
} }
@Override @Override
@ -432,6 +647,7 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
disposePrepareOrPlayCommands(); disposePrepareOrPlayCommands();
prepareOrPlayDisposable = extractPlayQueueFromMediaId(mediaId) prepareOrPlayDisposable = extractPlayQueueFromMediaId(mediaId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
playQueue -> { playQueue -> {
@ -448,6 +664,45 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
public void onPrepareFromSearch(@NonNull final String query, public void onPrepareFromSearch(@NonNull final String query,
final boolean playWhenReady, final boolean playWhenReady,
@Nullable final Bundle extras) { @Nullable final Bundle extras) {
disposePrepareOrPlayCommands();
playbackError(R.string.content_not_supported,
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED);
}
private @NonNull Single<SearchInfo> searchMusicBySongTitle(final String query) {
final var serviceId = ServiceHelper.getSelectedServiceId(playerService);
return ExtractorHelper.searchFor(serviceId, query,
new ArrayList<>(), "");
}
private @NonNull SingleSource<List<MediaItem>>
mediaItemsFromInfoItemList(final ListInfo<InfoItem> result) {
final List<Throwable> exceptions = result.getErrors();
if (!exceptions.isEmpty()
&& !(exceptions.size() == 1
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) {
return Single.error(exceptions.get(0));
}
final List<InfoItem> items = result.getRelatedItems();
if (items.isEmpty()) {
return Single.error(new NullPointerException("Got no search results."));
}
try {
final List<MediaItem> results = items.stream()
.filter(item ->
item.getInfoType() == InfoItem.InfoType.STREAM
|| item.getInfoType() == InfoItem.InfoType.PLAYLIST
|| item.getInfoType() == InfoItem.InfoType.CHANNEL)
.map(this::createInfoItemMediaItem).toList();
return Single.just(results);
} catch (final Exception e) {
return Single.error(e);
}
}
private void handleSearchError(final Throwable throwable) {
Log.e(TAG, "Search error: " + throwable);
disposePrepareOrPlayCommands(); disposePrepareOrPlayCommands();
playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED);
} }
@ -467,4 +722,17 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep
@Nullable final ResultReceiver cb) { @Nullable final ResultReceiver cb) {
return false; return false;
} }
public void onSearch(@NonNull final String query,
@NonNull final MediaBrowserServiceCompat.Result<List<MediaItem>> result) {
result.detach();
if (searchDisposable != null) {
searchDisposable.dispose();
}
searchDisposable = searchMusicBySongTitle(query)
.flatMap(this::mediaItemsFromInfoItemList)
.subscribeOn(Schedulers.io())
.subscribe(result::sendResult,
this::handleSearchError);
}
} }