mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-02-02 12:19:16 +00:00
Convert new and important files to Kotlin and optimize
This commit is contained in:
parent
89bdfef4b9
commit
bf59f1e09d
@ -1,13 +0,0 @@
|
|||||||
package org.schabi.newpipe.database;
|
|
||||||
|
|
||||||
public interface LocalItem {
|
|
||||||
LocalItemType getLocalItemType();
|
|
||||||
|
|
||||||
enum LocalItemType {
|
|
||||||
PLAYLIST_LOCAL_ITEM,
|
|
||||||
PLAYLIST_REMOTE_ITEM,
|
|
||||||
|
|
||||||
PLAYLIST_STREAM_ITEM,
|
|
||||||
STATISTIC_STREAM_ITEM,
|
|
||||||
}
|
|
||||||
}
|
|
18
app/src/main/java/org/schabi/newpipe/database/LocalItem.kt
Normal file
18
app/src/main/java/org/schabi/newpipe/database/LocalItem.kt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a generic item that can be stored locally. This can be a playlist, a stream, etc.
|
||||||
|
*/
|
||||||
|
interface LocalItem {
|
||||||
|
/**
|
||||||
|
* The type of local item. Can be null if the type is unknown or not applicable.
|
||||||
|
*/
|
||||||
|
val localItemType: LocalItemType?
|
||||||
|
|
||||||
|
enum class LocalItemType {
|
||||||
|
PLAYLIST_LOCAL_ITEM,
|
||||||
|
PLAYLIST_REMOTE_ITEM,
|
||||||
|
PLAYLIST_STREAM_ITEM,
|
||||||
|
STATISTIC_STREAM_ITEM,
|
||||||
|
}
|
||||||
|
}
|
@ -30,18 +30,16 @@ data class StreamHistoryEntry(
|
|||||||
accessDate.isEqual(other.accessDate)
|
accessDate.isEqual(other.accessDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toStreamInfoItem(): StreamInfoItem {
|
fun toStreamInfoItem(): StreamInfoItem =
|
||||||
val item = StreamInfoItem(
|
StreamInfoItem(
|
||||||
streamEntity.serviceId,
|
streamEntity.serviceId,
|
||||||
streamEntity.url,
|
streamEntity.url,
|
||||||
streamEntity.title,
|
streamEntity.title,
|
||||||
streamEntity.streamType
|
streamEntity.streamType,
|
||||||
)
|
).apply {
|
||||||
item.duration = streamEntity.duration
|
duration = streamEntity.duration
|
||||||
item.uploaderName = streamEntity.uploader
|
uploaderName = streamEntity.uploader
|
||||||
item.uploaderUrl = streamEntity.uploaderUrl
|
uploaderUrl = streamEntity.uploaderUrl
|
||||||
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
|
|
||||||
return item
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
package org.schabi.newpipe.database.playlist;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.database.LocalItem;
|
|
||||||
|
|
||||||
public interface PlaylistLocalItem extends LocalItem {
|
|
||||||
String getOrderingName();
|
|
||||||
|
|
||||||
long getDisplayIndex();
|
|
||||||
|
|
||||||
long getUid();
|
|
||||||
|
|
||||||
void setDisplayIndex(long displayIndex);
|
|
||||||
|
|
||||||
String getThumbnailUrl();
|
|
||||||
}
|
|
@ -0,0 +1,28 @@
|
|||||||
|
package org.schabi.newpipe.database.playlist
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a playlist item stored locally.
|
||||||
|
*/
|
||||||
|
interface PlaylistLocalItem : LocalItem {
|
||||||
|
/**
|
||||||
|
* The name used for ordering this item within the playlist. Can be null.
|
||||||
|
*/
|
||||||
|
val orderingName: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The index used to display this item within the playlist.
|
||||||
|
*/
|
||||||
|
var displayIndex: Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique identifier for this playlist item.
|
||||||
|
*/
|
||||||
|
val uid: Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL of the thumbnail image for this playlist item. Can be null.
|
||||||
|
*/
|
||||||
|
val thumbnailUrl: String?
|
||||||
|
}
|
@ -22,19 +22,20 @@ data class PlaylistStreamEntry(
|
|||||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
|
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
|
||||||
val joinIndex: Int
|
val joinIndex: Int
|
||||||
) : LocalItem {
|
) : LocalItem {
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
fun toStreamInfoItem(): StreamInfoItem {
|
fun toStreamInfoItem() =
|
||||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
StreamInfoItem(
|
||||||
item.duration = streamEntity.duration
|
streamEntity.serviceId,
|
||||||
item.uploaderName = streamEntity.uploader
|
streamEntity.url,
|
||||||
item.uploaderUrl = streamEntity.uploaderUrl
|
streamEntity.title,
|
||||||
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
streamEntity.streamType,
|
||||||
|
).apply {
|
||||||
return item
|
duration = streamEntity.duration
|
||||||
|
uploaderName = streamEntity.uploader
|
||||||
|
uploaderUrl = streamEntity.uploaderUrl
|
||||||
|
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLocalItemType(): LocalItem.LocalItemType {
|
override val localItemType: LocalItem.LocalItemType
|
||||||
return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
|
get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -26,19 +26,21 @@ class StreamStatisticsEntry(
|
|||||||
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||||
val watchCount: Long
|
val watchCount: Long
|
||||||
) : LocalItem {
|
) : LocalItem {
|
||||||
fun toStreamInfoItem(): StreamInfoItem {
|
fun toStreamInfoItem() =
|
||||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
StreamInfoItem(
|
||||||
item.duration = streamEntity.duration
|
streamEntity.serviceId,
|
||||||
item.uploaderName = streamEntity.uploader
|
streamEntity.url,
|
||||||
item.uploaderUrl = streamEntity.uploaderUrl
|
streamEntity.title,
|
||||||
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
streamEntity.streamType,
|
||||||
|
).apply {
|
||||||
return item
|
duration = streamEntity.duration
|
||||||
|
uploaderName = streamEntity.uploader
|
||||||
|
uploaderUrl = streamEntity.uploaderUrl
|
||||||
|
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLocalItemType(): LocalItem.LocalItemType {
|
override val localItemType: LocalItem.LocalItemType
|
||||||
return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM
|
get() = LocalItem.LocalItemType.STATISTIC_STREAM_ITEM
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val STREAM_LATEST_DATE = "latestAccess"
|
const val STREAM_LATEST_DATE = "latestAccess"
|
||||||
|
@ -45,6 +45,7 @@ import static org.schabi.newpipe.player.notification.NotificationConstants.ACTIO
|
|||||||
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
|
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
|
||||||
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
@ -417,7 +418,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||||||
}
|
}
|
||||||
if (playQueue.getIndex() != newQueue.getIndex()) {
|
if (playQueue.getIndex() != newQueue.getIndex()) {
|
||||||
simpleExoPlayer.seekTo(newQueue.getIndex(),
|
simpleExoPlayer.seekTo(newQueue.getIndex(),
|
||||||
newQueue.getItem().getRecoveryPosition());
|
requireNonNull(newQueue.getItem()).getRecoveryPosition());
|
||||||
}
|
}
|
||||||
simpleExoPlayer.setPlayWhenReady(playWhenReady);
|
simpleExoPlayer.setPlayWhenReady(playWhenReady);
|
||||||
|
|
||||||
|
@ -1,256 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
|
||||||
* Part of NewPipe
|
|
||||||
*
|
|
||||||
* License: GPL-3.0+
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.player;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.support.v4.media.MediaBrowserCompat.MediaItem;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.media.MediaBrowserServiceCompat;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.player.mediabrowser.MediaBrowserConnector;
|
|
||||||
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
|
|
||||||
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One service for all players.
|
|
||||||
*/
|
|
||||||
public final class PlayerService extends MediaBrowserServiceCompat {
|
|
||||||
private static final String TAG = PlayerService.class.getSimpleName();
|
|
||||||
private static final boolean DEBUG = Player.DEBUG;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Player player;
|
|
||||||
|
|
||||||
private final IBinder mBinder = new PlayerService.LocalBinder(this);
|
|
||||||
|
|
||||||
|
|
||||||
private MediaBrowserConnector mediaBrowserConnector;
|
|
||||||
private final CompositeDisposable compositeDisposableLoadChildren = new CompositeDisposable();
|
|
||||||
|
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
|
||||||
// Service's LifeCycle
|
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onCreate() called");
|
|
||||||
}
|
|
||||||
assureCorrectAppLanguage(this);
|
|
||||||
ThemeHelper.setTheme(this);
|
|
||||||
|
|
||||||
mediaBrowserConnector = new MediaBrowserConnector(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializePlayerIfNeeded() {
|
|
||||||
if (player == null) {
|
|
||||||
player = new Player(this);
|
|
||||||
/*
|
|
||||||
Create the player notification and start immediately the service in foreground,
|
|
||||||
otherwise if nothing is played or initializing the player and its components (especially
|
|
||||||
loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the
|
|
||||||
service would never be put in the foreground while we said to the system we would do so
|
|
||||||
*/
|
|
||||||
player.UIs().get(NotificationPlayerUi.class)
|
|
||||||
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress Sonar warning to not always return the same value, as we need to do some actions
|
|
||||||
// before returning
|
|
||||||
@SuppressWarnings("squid:S3516")
|
|
||||||
@Override
|
|
||||||
public int onStartCommand(final Intent intent, final int flags, final int startId) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
|
|
||||||
+ "], flags = [" + flags + "], startId = [" + startId + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Be sure that the player notification is set and the service is started in foreground,
|
|
||||||
otherwise, the app may crash on Android 8+ as the service would never be put in the
|
|
||||||
foreground while we said to the system we would do so
|
|
||||||
The service is always requested to be started in foreground, so always creating a
|
|
||||||
notification if there is no one already and starting the service in foreground should
|
|
||||||
not create any issues
|
|
||||||
If the service is already started in foreground, requesting it to be started shouldn't
|
|
||||||
do anything
|
|
||||||
*/
|
|
||||||
if (player != null) {
|
|
||||||
player.UIs().get(NotificationPlayerUi.class)
|
|
||||||
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
|
||||||
&& (player == null || player.getPlayQueue() == null)) {
|
|
||||||
/*
|
|
||||||
No need to process media button's actions if the player is not working, otherwise
|
|
||||||
the player service would strangely start with nothing to play
|
|
||||||
Stop the service in this case, which will be removed from the foreground and its
|
|
||||||
notification cancelled in its destruction
|
|
||||||
*/
|
|
||||||
stopSelf();
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
initializePlayerIfNeeded();
|
|
||||||
Objects.requireNonNull(player).handleIntent(intent);
|
|
||||||
player.UIs().get(MediaSessionPlayerUi.class)
|
|
||||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
|
||||||
|
|
||||||
return START_NOT_STICKY;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopForImmediateReusing() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "stopForImmediateReusing() called");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (player != null && !player.exoPlayerIsNull()) {
|
|
||||||
// Releases wifi & cpu, disables keepScreenOn, etc.
|
|
||||||
// We can't just pause the player here because it will make transition
|
|
||||||
// from one stream to a new stream not smooth
|
|
||||||
player.smoothStopForImmediateReusing();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTaskRemoved(final Intent rootIntent) {
|
|
||||||
super.onTaskRemoved(rootIntent);
|
|
||||||
if (player != null && !player.videoPlayerSelected()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onDestroy();
|
|
||||||
// Unload from memory completely
|
|
||||||
Runtime.getRuntime().halt(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, "destroy() called");
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup();
|
|
||||||
|
|
||||||
if (mediaBrowserConnector != null) {
|
|
||||||
mediaBrowserConnector.release();
|
|
||||||
mediaBrowserConnector = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
compositeDisposableLoadChildren.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanup() {
|
|
||||||
if (player != null) {
|
|
||||||
player.destroy();
|
|
||||||
player = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopService() {
|
|
||||||
cleanup();
|
|
||||||
stopSelf();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(final Context base) {
|
|
||||||
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(@NonNull final Intent intent) {
|
|
||||||
if (SERVICE_INTERFACE.equals(intent.getAction())) {
|
|
||||||
return super.onBind(intent);
|
|
||||||
}
|
|
||||||
return mBinder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public MediaSessionConnector getSessionConnector() {
|
|
||||||
return mediaBrowserConnector.getSessionConnector();
|
|
||||||
}
|
|
||||||
|
|
||||||
// MediaBrowserServiceCompat methods
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public BrowserRoot onGetRoot(@NonNull final String clientPackageName,
|
|
||||||
final int clientUid,
|
|
||||||
@Nullable final Bundle rootHints) {
|
|
||||||
return mediaBrowserConnector.onGetRoot(clientPackageName, clientUid, rootHints);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadChildren(@NonNull final String parentId,
|
|
||||||
@NonNull final Result<List<MediaItem>> result) {
|
|
||||||
result.detach();
|
|
||||||
final Disposable disposable = mediaBrowserConnector.onLoadChildren(parentId)
|
|
||||||
.subscribe(result::sendResult);
|
|
||||||
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 {
|
|
||||||
private final WeakReference<PlayerService> playerService;
|
|
||||||
|
|
||||||
LocalBinder(final PlayerService playerService) {
|
|
||||||
this.playerService = new WeakReference<>(playerService);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public PlayerService getService() {
|
|
||||||
return playerService.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Player getPlayer() {
|
|
||||||
return playerService.get().player;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
236
app/src/main/java/org/schabi/newpipe/player/PlayerService.kt
Normal file
236
app/src/main/java/org/schabi/newpipe/player/PlayerService.kt
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
|
||||||
|
* Part of NewPipe
|
||||||
|
*
|
||||||
|
* License: GPL-3.0+
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.schabi.newpipe.player
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import org.schabi.newpipe.player.mediabrowser.MediaBrowserConnector
|
||||||
|
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi
|
||||||
|
import org.schabi.newpipe.player.notification.NotificationPlayerUi
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One service for all players.
|
||||||
|
*/
|
||||||
|
class PlayerService : MediaBrowserServiceCompat() {
|
||||||
|
private val player: Player by lazy {
|
||||||
|
Player(this).apply {
|
||||||
|
/*
|
||||||
|
Create the player notification and start immediately the service in foreground,
|
||||||
|
otherwise if nothing is played or initializing the player and its components (especially
|
||||||
|
loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the
|
||||||
|
service would never be put in the foreground while we said to the system we would do so
|
||||||
|
*/
|
||||||
|
UIs()[NotificationPlayerUi::class.java].ifPresent {
|
||||||
|
it.createNotificationAndStartForeground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mBinder: IBinder = LocalBinder(this)
|
||||||
|
private val compositeDisposableLoadChildren = CompositeDisposable()
|
||||||
|
private var mediaBrowserConnector: MediaBrowserConnector? = null
|
||||||
|
get() {
|
||||||
|
if (field == null) {
|
||||||
|
return MediaBrowserConnector(this)
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
val sessionConnector: MediaSessionConnector?
|
||||||
|
get() = mediaBrowserConnector?.sessionConnector
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Service's LifeCycle
|
||||||
|
////////////////////////////////////////////////////////////////////////// */
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreate() called")
|
||||||
|
}
|
||||||
|
Localization.assureCorrectAppLanguage(this)
|
||||||
|
ThemeHelper.setTheme(this)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create the player notification and start immediately the service in foreground,
|
||||||
|
otherwise if nothing is played or initializing the player and its components (especially
|
||||||
|
loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the
|
||||||
|
service would never be put in the foreground while we said to the system we would do so
|
||||||
|
*/
|
||||||
|
player.UIs()[NotificationPlayerUi::class.java].ifPresent {
|
||||||
|
it.createNotificationAndStartForeground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppress Sonar warning to not always return the same value, as we need to do some actions
|
||||||
|
// before returning
|
||||||
|
override fun onStartCommand(
|
||||||
|
intent: Intent,
|
||||||
|
flags: Int,
|
||||||
|
startId: Int,
|
||||||
|
): Int {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"onStartCommand() called with: intent = [" + intent + "], flags = [" + flags +
|
||||||
|
"], startId = [" + startId + "]",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Be sure that the player notification is set and the service is started in foreground,
|
||||||
|
otherwise, the app may crash on Android 8+ as the service would never be put in the
|
||||||
|
foreground while we said to the system we would do so
|
||||||
|
The service is always requested to be started in foreground, so always creating a
|
||||||
|
notification if there is no one already and starting the service in foreground should
|
||||||
|
not create any issues
|
||||||
|
If the service is already started in foreground, requesting it to be started shouldn't
|
||||||
|
do anything
|
||||||
|
*/
|
||||||
|
player.UIs()[NotificationPlayerUi::class.java].ifPresent {
|
||||||
|
it.createNotificationAndStartForeground()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Intent.ACTION_MEDIA_BUTTON == intent.action && (player.playQueue == null)) {
|
||||||
|
/*
|
||||||
|
No need to process media button's actions if the player is not working, otherwise
|
||||||
|
the player service would strangely start with nothing to play
|
||||||
|
Stop the service in this case, which will be removed from the foreground and its
|
||||||
|
notification cancelled in its destruction
|
||||||
|
*/
|
||||||
|
stopSelf()
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
player.handleIntent(intent)
|
||||||
|
player.UIs()[MediaSessionPlayerUi::class.java].ifPresent {
|
||||||
|
it.handleMediaButtonIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopForImmediateReusing() {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "stopForImmediateReusing() called")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player.exoPlayerIsNull()) {
|
||||||
|
// Releases wifi & cpu, disables keepScreenOn, etc.
|
||||||
|
// We can't just pause the player here because it will make transition
|
||||||
|
// from one stream to a new stream not smooth
|
||||||
|
player.smoothStopForImmediateReusing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent) {
|
||||||
|
super.onTaskRemoved(rootIntent)
|
||||||
|
if (!player.videoPlayerSelected()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onDestroy()
|
||||||
|
// Unload from memory completely
|
||||||
|
Runtime.getRuntime().halt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "destroy() called")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
mediaBrowserConnector?.release()
|
||||||
|
mediaBrowserConnector = null
|
||||||
|
|
||||||
|
compositeDisposableLoadChildren.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanup() {
|
||||||
|
player.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopService() {
|
||||||
|
cleanup()
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder = mBinder
|
||||||
|
|
||||||
|
// MediaBrowserServiceCompat methods
|
||||||
|
override fun onGetRoot(
|
||||||
|
clientPackageName: String,
|
||||||
|
clientUid: Int,
|
||||||
|
rootHints: Bundle?,
|
||||||
|
): BrowserRoot? = mediaBrowserConnector?.onGetRoot(clientPackageName, clientUid, rootHints)
|
||||||
|
|
||||||
|
override fun onLoadChildren(
|
||||||
|
parentId: String,
|
||||||
|
result: Result<List<MediaBrowserCompat.MediaItem>>,
|
||||||
|
) {
|
||||||
|
result.detach()
|
||||||
|
mediaBrowserConnector?.let {
|
||||||
|
val disposable =
|
||||||
|
it.onLoadChildren(parentId).subscribe { mediaItems ->
|
||||||
|
result.sendResult(mediaItems)
|
||||||
|
}
|
||||||
|
compositeDisposableLoadChildren.add(disposable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSearch(
|
||||||
|
query: String,
|
||||||
|
extras: Bundle,
|
||||||
|
result: Result<List<MediaBrowserCompat.MediaItem>>,
|
||||||
|
) {
|
||||||
|
mediaBrowserConnector?.onSearch(query, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalBinder internal constructor(
|
||||||
|
playerService: PlayerService,
|
||||||
|
) : Binder() {
|
||||||
|
private val playerService = WeakReference(playerService)
|
||||||
|
|
||||||
|
val service: PlayerService?
|
||||||
|
get() = playerService.get()
|
||||||
|
|
||||||
|
fun getPlayer(): Player = service?.player ?: throw Error("Player service is null")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = PlayerService::class.java.simpleName
|
||||||
|
private val DEBUG = Player.DEBUG
|
||||||
|
}
|
||||||
|
}
|
@ -1,730 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.mediabrowser;
|
|
||||||
|
|
||||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.ResultReceiver;
|
|
||||||
import android.support.v4.media.MediaBrowserCompat.MediaItem;
|
|
||||||
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;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.StringRes;
|
|
||||||
import androidx.media.MediaBrowserServiceCompat;
|
|
||||||
import androidx.media.utils.MediaConstants;
|
|
||||||
|
|
||||||
import com.google.android.exoplayer2.Player;
|
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
|
||||||
import org.schabi.newpipe.NewPipeDatabase;
|
|
||||||
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.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.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.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.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.RemotePlaylistManager;
|
|
||||||
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.PlaylistPlayQueue;
|
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
|
||||||
import org.schabi.newpipe.util.ChannelTabHelper;
|
|
||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
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;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.SingleSource;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPreparer {
|
|
||||||
|
|
||||||
private static final String TAG = MediaBrowserConnector.class.getSimpleName();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private final PlayerService playerService;
|
|
||||||
@NonNull
|
|
||||||
private final MediaSessionConnector sessionConnector;
|
|
||||||
@NonNull
|
|
||||||
private final MediaSessionCompat mediaSession;
|
|
||||||
|
|
||||||
private AppDatabase database;
|
|
||||||
private LocalPlaylistManager localPlaylistManager;
|
|
||||||
private RemotePlaylistManager remotePlaylistManager;
|
|
||||||
private Disposable prepareOrPlayDisposable;
|
|
||||||
private Disposable searchDisposable;
|
|
||||||
|
|
||||||
public MediaBrowserConnector(@NonNull final PlayerService playerService) {
|
|
||||||
this.playerService = playerService;
|
|
||||||
mediaSession = new MediaSessionCompat(playerService, TAG);
|
|
||||||
sessionConnector = new MediaSessionConnector(mediaSession);
|
|
||||||
sessionConnector.setMetadataDeduplicationEnabled(true);
|
|
||||||
sessionConnector.setPlaybackPreparer(this);
|
|
||||||
playerService.setSessionToken(mediaSession.getSessionToken());
|
|
||||||
|
|
||||||
setupBookmarksNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public MediaSessionConnector getSessionConnector() {
|
|
||||||
return sessionConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void release() {
|
|
||||||
disposePrepareOrPlayCommands();
|
|
||||||
disposeBookmarksNotifications();
|
|
||||||
mediaSession.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static final String ID_AUTHORITY = BuildConfig.APPLICATION_ID;
|
|
||||||
@NonNull
|
|
||||||
private static final String ID_ROOT = "//" + ID_AUTHORITY;
|
|
||||||
@NonNull
|
|
||||||
private static final String ID_BOOKMARKS = "playlists";
|
|
||||||
@NonNull
|
|
||||||
private static final String ID_HISTORY = "history";
|
|
||||||
@NonNull
|
|
||||||
private static final String ID_INFO_ITEM = "item";
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static final String ID_LOCAL = "local";
|
|
||||||
@NonNull
|
|
||||||
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
|
|
||||||
private MediaItem createRootMediaItem(@Nullable final String mediaId,
|
|
||||||
final String folderName,
|
|
||||||
@DrawableRes final int iconResId) {
|
|
||||||
final var builder = new MediaDescriptionCompat.Builder();
|
|
||||||
builder.setMediaId(mediaId);
|
|
||||||
builder.setTitle(folderName);
|
|
||||||
final Resources resources = playerService.getResources();
|
|
||||||
builder.setIconUri(new Uri.Builder()
|
|
||||||
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
|
||||||
.authority(resources.getResourcePackageName(iconResId))
|
|
||||||
.appendPath(resources.getResourceTypeName(iconResId))
|
|
||||||
.appendPath(resources.getResourceEntryName(iconResId))
|
|
||||||
.build());
|
|
||||||
|
|
||||||
final Bundle extras = new Bundle();
|
|
||||||
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
|
||||||
playerService.getString(R.string.app_name));
|
|
||||||
builder.setExtras(extras);
|
|
||||||
return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private MediaItem createPlaylistMediaItem(@NonNull final PlaylistLocalItem playlist) {
|
|
||||||
final var builder = new MediaDescriptionCompat.Builder();
|
|
||||||
final boolean remote = playlist instanceof PlaylistRemoteEntity;
|
|
||||||
builder.setMediaId(createMediaIdForInfoItem(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,
|
|
||||||
playerService.getResources().getString(R.string.tab_bookmarks));
|
|
||||||
builder.setExtras(extras);
|
|
||||||
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
|
|
||||||
private Uri.Builder buildMediaId() {
|
|
||||||
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
|
|
||||||
private MediaItem createLocalPlaylistStreamMediaItem(final long playlistId,
|
|
||||||
@NonNull final PlaylistStreamEntry item,
|
|
||||||
final int index) {
|
|
||||||
final var builder = new MediaDescriptionCompat.Builder();
|
|
||||||
builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
|
|
||||||
.setTitle(item.getStreamEntity().getTitle())
|
|
||||||
.setSubtitle(item.getStreamEntity().getUploader())
|
|
||||||
.setIconUri(Uri.parse(item.getStreamEntity().getThumbnailUrl()));
|
|
||||||
|
|
||||||
return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
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 buildLocalPlaylistItemMediaId(remote, playlistId)
|
|
||||||
.appendPath(Integer.toString(index))
|
|
||||||
.build().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private String createMediaIdForInfoItem(@NonNull final InfoItem item) {
|
|
||||||
return buildInfoItemMediaId(item).build().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String clientPackageName,
|
|
||||||
final int clientUid,
|
|
||||||
@Nullable final Bundle rootHints) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, String.format("MediaBrowserService.onGetRoot(%s, %s, %s)",
|
|
||||||
clientPackageName, clientUid, rootHints));
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
final Uri parentIdUri = Uri.parse(parentId);
|
|
||||||
final List<String> path = new ArrayList<>(parentIdUri.getPathSegments());
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Single<List<MediaItem>> populateHistory() {
|
|
||||||
final StreamHistoryDAO streamHistory = getDatabase().streamHistoryDAO();
|
|
||||||
final var history = streamHistory.getHistory().firstOrError();
|
|
||||||
return history.map(items -> items.stream()
|
|
||||||
.map(this::createHistoryMediaItem)
|
|
||||||
.collect(Collectors.toList()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private MediaItem createHistoryMediaItem(@NonNull final StreamHistoryEntry streamHistoryEntry) {
|
|
||||||
final var builder = new MediaDescriptionCompat.Builder();
|
|
||||||
final var mediaId = buildMediaId()
|
|
||||||
.appendPath(ID_HISTORY)
|
|
||||||
.appendPath(Long.toString(streamHistoryEntry.getStreamId()))
|
|
||||||
.build().toString();
|
|
||||||
builder.setMediaId(mediaId)
|
|
||||||
.setTitle(streamHistoryEntry.getStreamEntity().getTitle())
|
|
||||||
.setSubtitle(streamHistoryEntry.getStreamEntity().getUploader())
|
|
||||||
.setIconUri(Uri.parse(streamHistoryEntry.getStreamEntity().getThumbnailUrl()));
|
|
||||||
|
|
||||||
return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AppDatabase getDatabase() {
|
|
||||||
if (database == null) {
|
|
||||||
database = NewPipeDatabase.getInstance(playerService);
|
|
||||||
}
|
|
||||||
return database;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Flowable<List<PlaylistLocalItem>> getPlaylists() {
|
|
||||||
if (localPlaylistManager == null) {
|
|
||||||
localPlaylistManager = new LocalPlaylistManager(getDatabase());
|
|
||||||
}
|
|
||||||
if (remotePlaylistManager == null) {
|
|
||||||
remotePlaylistManager = new RemotePlaylistManager(getDatabase());
|
|
||||||
}
|
|
||||||
return MergedPlaylistManager.getMergedOrderedPlaylists(localPlaylistManager,
|
|
||||||
remotePlaylistManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable Disposable bookmarksNotificationsDisposable;
|
|
||||||
|
|
||||||
private void setupBookmarksNotifications() {
|
|
||||||
bookmarksNotificationsDisposable = getPlaylists().subscribe(
|
|
||||||
playlistMetadataEntries -> playerService.notifyChildrenChanged(ID_BOOKMARKS));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disposeBookmarksNotifications() {
|
|
||||||
if (bookmarksNotificationsDisposable != null) {
|
|
||||||
bookmarksNotificationsDisposable.dispose();
|
|
||||||
bookmarksNotificationsDisposable = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress Sonar warning replace list collection by Stream.toList call, as this method is only
|
|
||||||
// available in Android API 34 and not currently available with desugaring
|
|
||||||
@SuppressWarnings("squid:S6204")
|
|
||||||
private Single<List<MediaItem>> populateBookmarks() {
|
|
||||||
final var playlists = getPlaylists().firstOrError();
|
|
||||||
return playlists.map(playlist -> playlist.stream()
|
|
||||||
.map(this::createPlaylistMediaItem)
|
|
||||||
.collect(Collectors.toList()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Single<List<MediaItem>> populateLocalPlaylist(final long playlistId) {
|
|
||||||
final var playlist = localPlaylistManager.getPlaylistStreams(playlistId).firstOrError();
|
|
||||||
return playlist.map(items -> {
|
|
||||||
final List<MediaItem> results = new ArrayList<>();
|
|
||||||
int index = 0;
|
|
||||||
for (final PlaylistStreamEntry item : items) {
|
|
||||||
results.add(createLocalPlaylistStreamMediaItem(playlistId, item, index));
|
|
||||||
++index;
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
playerService.stopForImmediateReusing();
|
|
||||||
sessionConnector.setCustomErrorMessage(playerService.getString(resId), code);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void playbackError(@NonNull final ErrorInfo errorInfo) {
|
|
||||||
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 static ContentNotAvailableException parseError() {
|
|
||||||
return new ContentNotAvailableException("Failed to parse media ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Single<PlayQueue> extractPlayQueueFromMediaId(final String mediaId) {
|
|
||||||
try {
|
|
||||||
final Uri mediaIdUri = Uri.parse(mediaId);
|
|
||||||
final List<String> path = new ArrayList<>(mediaIdUri.getPathSegments());
|
|
||||||
|
|
||||||
if (path.isEmpty()) {
|
|
||||||
throw parseError();
|
|
||||||
}
|
|
||||||
|
|
||||||
final String uriType = path.get(0);
|
|
||||||
path.remove(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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
public long getSupportedPrepareActions() {
|
|
||||||
return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disposePrepareOrPlayCommands() {
|
|
||||||
if (prepareOrPlayDisposable != null) {
|
|
||||||
prepareOrPlayDisposable.dispose();
|
|
||||||
prepareOrPlayDisposable = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepare(final boolean playWhenReady) {
|
|
||||||
disposePrepareOrPlayCommands();
|
|
||||||
// No need to prepare
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepareFromMediaId(@NonNull final String mediaId,
|
|
||||||
final boolean playWhenReady,
|
|
||||||
@Nullable final Bundle extras) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Log.d(TAG, String.format("MediaBrowserConnector.onPrepareFromMediaId(%s, %s, %s)",
|
|
||||||
mediaId, playWhenReady, extras));
|
|
||||||
}
|
|
||||||
|
|
||||||
disposePrepareOrPlayCommands();
|
|
||||||
prepareOrPlayDisposable = extractPlayQueueFromMediaId(mediaId)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(
|
|
||||||
playQueue -> {
|
|
||||||
sessionConnector.setCustomErrorMessage(null);
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(playerService, playQueue,
|
|
||||||
playWhenReady);
|
|
||||||
},
|
|
||||||
throwable -> playbackError(new ErrorInfo(throwable, UserAction.PLAY_STREAM,
|
|
||||||
"Failed playback of media ID [" + mediaId + "]: "))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepareFromSearch(@NonNull final String query,
|
|
||||||
final boolean playWhenReady,
|
|
||||||
@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();
|
|
||||||
playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepareFromUri(@NonNull final Uri uri,
|
|
||||||
final boolean playWhenReady,
|
|
||||||
@Nullable final Bundle extras) {
|
|
||||||
disposePrepareOrPlayCommands();
|
|
||||||
playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCommand(@NonNull final Player player,
|
|
||||||
@NonNull final String command,
|
|
||||||
@Nullable final Bundle extras,
|
|
||||||
@Nullable final ResultReceiver cb) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,735 @@
|
|||||||
|
package org.schabi.newpipe.player.mediabrowser
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ResultReceiver
|
||||||
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
|
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 androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.google.android.exoplayer2.Player
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.core.SingleSource
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.MainActivity
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.database.AppDatabase
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry
|
||||||
|
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.InfoItem
|
||||||
|
import org.schabi.newpipe.extractor.InfoItem.InfoType
|
||||||
|
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.ContentNotSupportedException
|
||||||
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.search.SearchExtractor.NothingFoundException
|
||||||
|
import org.schabi.newpipe.extractor.search.SearchInfo
|
||||||
|
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.ChannelTabPlayQueue
|
||||||
|
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.util.ChannelTabHelper
|
||||||
|
import org.schabi.newpipe.util.ExtractorHelper
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
class MediaBrowserConnector(private val playerService: PlayerService) : PlaybackPreparer {
|
||||||
|
private val mediaSession = MediaSessionCompat(playerService, TAG)
|
||||||
|
val sessionConnector = MediaSessionConnector(mediaSession).apply {
|
||||||
|
setMetadataDeduplicationEnabled(true)
|
||||||
|
setPlaybackPreparer(this@MediaBrowserConnector)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val database: AppDatabase by lazy { NewPipeDatabase.getInstance(playerService) }
|
||||||
|
private val localPlaylistManager: LocalPlaylistManager by lazy { LocalPlaylistManager(database) }
|
||||||
|
private val remotePlaylistManager: RemotePlaylistManager by lazy {
|
||||||
|
RemotePlaylistManager(database)
|
||||||
|
}
|
||||||
|
private val playlists: Flowable<List<PlaylistLocalItem?>>
|
||||||
|
get() {
|
||||||
|
return MergedPlaylistManager.getMergedOrderedPlaylists(localPlaylistManager, remotePlaylistManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var prepareOrPlayDisposable: Disposable? = null
|
||||||
|
private var searchDisposable: Disposable? = null
|
||||||
|
private var bookmarksNotificationsDisposable: Disposable? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
playerService.sessionToken = mediaSession.sessionToken
|
||||||
|
setupBookmarksNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
disposePrepareOrPlayCommands()
|
||||||
|
disposeBookmarksNotifications()
|
||||||
|
mediaSession.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRootMediaItem(
|
||||||
|
mediaId: String?,
|
||||||
|
folderName: String,
|
||||||
|
@DrawableRes iconResId: Int
|
||||||
|
): MediaBrowserCompat.MediaItem {
|
||||||
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
|
builder.setMediaId(mediaId)
|
||||||
|
builder.setTitle(folderName)
|
||||||
|
val resources = playerService.resources
|
||||||
|
builder.setIconUri(
|
||||||
|
Uri.Builder()
|
||||||
|
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||||
|
.authority(resources.getResourcePackageName(iconResId))
|
||||||
|
.appendPath(resources.getResourceTypeName(iconResId))
|
||||||
|
.appendPath(resources.getResourceEntryName(iconResId))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val extras = Bundle()
|
||||||
|
extras.putString(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||||
|
playerService.getString(R.string.app_name)
|
||||||
|
)
|
||||||
|
builder.setExtras(extras)
|
||||||
|
return MediaBrowserCompat.MediaItem(
|
||||||
|
builder.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem {
|
||||||
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
|
val remote = playlist is PlaylistRemoteEntity
|
||||||
|
builder.setMediaId(createMediaIdForInfoItem(remote, playlist.uid))
|
||||||
|
.setTitle(playlist.orderingName)
|
||||||
|
.setIconUri(Uri.parse(playlist.thumbnailUrl))
|
||||||
|
|
||||||
|
val extras = Bundle()
|
||||||
|
extras.putString(
|
||||||
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||||
|
playerService.resources.getString(R.string.tab_bookmarks)
|
||||||
|
)
|
||||||
|
builder.setExtras(extras)
|
||||||
|
return MediaBrowserCompat.MediaItem(
|
||||||
|
builder.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createInfoItemMediaItem(item: InfoItem): MediaBrowserCompat.MediaItem {
|
||||||
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
|
builder.setMediaId(createMediaIdForInfoItem(item))
|
||||||
|
.setTitle(item.name)
|
||||||
|
|
||||||
|
when (item.infoType) {
|
||||||
|
InfoType.STREAM -> builder.setSubtitle((item as StreamInfoItem).uploaderName)
|
||||||
|
InfoType.PLAYLIST -> builder.setSubtitle((item as PlaylistInfoItem).uploaderName)
|
||||||
|
InfoType.CHANNEL -> builder.setSubtitle((item as ChannelInfoItem).description)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
item.thumbnails.firstOrNull()?.let {
|
||||||
|
builder.setIconUri(Uri.parse(it.url))
|
||||||
|
}
|
||||||
|
return MediaBrowserCompat.MediaItem(
|
||||||
|
builder.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMediaId() = Uri.Builder().authority(ID_AUTHORITY)
|
||||||
|
|
||||||
|
private fun buildPlaylistMediaId(playlistType: String) =
|
||||||
|
buildMediaId()
|
||||||
|
.appendPath(ID_BOOKMARKS)
|
||||||
|
.appendPath(playlistType)
|
||||||
|
|
||||||
|
private fun buildLocalPlaylistItemMediaId(
|
||||||
|
remote: Boolean,
|
||||||
|
playlistId: Long,
|
||||||
|
) = buildPlaylistMediaId(if (remote) ID_REMOTE else ID_LOCAL)
|
||||||
|
.appendPath(playlistId.toString())
|
||||||
|
|
||||||
|
private fun buildInfoItemMediaId(item: InfoItem) =
|
||||||
|
buildMediaId()
|
||||||
|
.appendPath(ID_INFO_ITEM)
|
||||||
|
.appendPath(infoItemTypeToString(item.infoType))
|
||||||
|
.appendPath(item.serviceId.toString())
|
||||||
|
.appendQueryParameter(ID_URL, item.url)
|
||||||
|
|
||||||
|
private fun createMediaIdForInfoItem(
|
||||||
|
remote: Boolean,
|
||||||
|
playlistId: Long,
|
||||||
|
) = buildLocalPlaylistItemMediaId(remote, playlistId)
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
private fun createLocalPlaylistStreamMediaItem(
|
||||||
|
playlistId: Long,
|
||||||
|
item: PlaylistStreamEntry,
|
||||||
|
index: Int
|
||||||
|
): MediaBrowserCompat.MediaItem {
|
||||||
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
|
builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
|
||||||
|
.setTitle(item.streamEntity.title)
|
||||||
|
.setSubtitle(item.streamEntity.uploader)
|
||||||
|
.setIconUri(Uri.parse(item.streamEntity.thumbnailUrl))
|
||||||
|
|
||||||
|
return MediaBrowserCompat.MediaItem(
|
||||||
|
builder.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRemotePlaylistStreamMediaItem(
|
||||||
|
playlistId: Long,
|
||||||
|
item: StreamInfoItem,
|
||||||
|
index: Int
|
||||||
|
): MediaBrowserCompat.MediaItem {
|
||||||
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
|
builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index))
|
||||||
|
.setTitle(item.name)
|
||||||
|
.setSubtitle(item.uploaderName)
|
||||||
|
item.thumbnails.firstOrNull()?.let {
|
||||||
|
builder.setIconUri(Uri.parse(it.url))
|
||||||
|
}
|
||||||
|
|
||||||
|
return MediaBrowserCompat.MediaItem(
|
||||||
|
builder.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMediaIdForPlaylistIndex(
|
||||||
|
remote: Boolean,
|
||||||
|
playlistId: Long,
|
||||||
|
index: Int,
|
||||||
|
) = buildLocalPlaylistItemMediaId(remote, playlistId)
|
||||||
|
.appendPath(index.toString())
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
private fun createMediaIdForInfoItem(item: InfoItem) = buildInfoItemMediaId(item).build().toString()
|
||||||
|
|
||||||
|
fun onGetRoot(
|
||||||
|
clientPackageName: String,
|
||||||
|
clientUid: Int,
|
||||||
|
rootHints: Bundle?
|
||||||
|
): MediaBrowserServiceCompat.BrowserRoot {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"MediaBrowserService.onGetRoot(%s, %s, %s)",
|
||||||
|
clientPackageName, clientUid, rootHints
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val extras = Bundle()
|
||||||
|
extras.putBoolean(
|
||||||
|
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
|
||||||
|
)
|
||||||
|
return MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onLoadChildren(parentId: String): Single<List<MediaBrowserCompat.MediaItem>> {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val parentIdUri = Uri.parse(parentId)
|
||||||
|
val path = parentIdUri.pathSegments
|
||||||
|
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
mediaItems.add(
|
||||||
|
createRootMediaItem(
|
||||||
|
ID_BOOKMARKS,
|
||||||
|
playerService.resources.getString(
|
||||||
|
R.string.tab_bookmarks_short
|
||||||
|
),
|
||||||
|
R.drawable.ic_bookmark_white
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mediaItems.add(
|
||||||
|
createRootMediaItem(
|
||||||
|
ID_HISTORY,
|
||||||
|
playerService.resources.getString(R.string.action_history),
|
||||||
|
R.drawable.ic_history_white
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return Single.just(mediaItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
val uriType = path[0]
|
||||||
|
path.removeAt(0)
|
||||||
|
|
||||||
|
when (uriType) {
|
||||||
|
ID_BOOKMARKS -> {
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return populateBookmarks()
|
||||||
|
}
|
||||||
|
if (path.size == 2) {
|
||||||
|
val localOrRemote = path[0]
|
||||||
|
val playlistId = path[1].toLong()
|
||||||
|
if (localOrRemote == ID_LOCAL) {
|
||||||
|
return populateLocalPlaylist(playlistId)
|
||||||
|
} else if (localOrRemote == ID_REMOTE) {
|
||||||
|
return populateRemotePlaylist(playlistId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.w(TAG, "Unknown playlist URI: $parentId")
|
||||||
|
throw parseError()
|
||||||
|
}
|
||||||
|
|
||||||
|
ID_HISTORY -> return populateHistory()
|
||||||
|
else -> throw parseError()
|
||||||
|
}
|
||||||
|
} catch (e: ContentNotAvailableException) {
|
||||||
|
return Single.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateHistory(): Single<List<MediaBrowserCompat.MediaItem>> =
|
||||||
|
database
|
||||||
|
.streamHistoryDAO()
|
||||||
|
.history
|
||||||
|
.firstOrError()
|
||||||
|
.map { items -> items.map(::createHistoryMediaItem) }
|
||||||
|
|
||||||
|
private fun createHistoryMediaItem(streamHistoryEntry: StreamHistoryEntry): MediaBrowserCompat.MediaItem {
|
||||||
|
val builder = MediaDescriptionCompat.Builder()
|
||||||
|
val mediaId = buildMediaId()
|
||||||
|
.appendPath(ID_HISTORY)
|
||||||
|
.appendPath(streamHistoryEntry.streamId.toString())
|
||||||
|
.build().toString()
|
||||||
|
builder.setMediaId(mediaId)
|
||||||
|
.setTitle(streamHistoryEntry.streamEntity.title)
|
||||||
|
.setSubtitle(streamHistoryEntry.streamEntity.uploader)
|
||||||
|
.setIconUri(Uri.parse(streamHistoryEntry.streamEntity.thumbnailUrl))
|
||||||
|
|
||||||
|
return MediaBrowserCompat.MediaItem(
|
||||||
|
builder.build(),
|
||||||
|
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupBookmarksNotifications() {
|
||||||
|
bookmarksNotificationsDisposable =
|
||||||
|
playlists.subscribe { _ ->
|
||||||
|
playerService.notifyChildrenChanged(ID_BOOKMARKS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disposeBookmarksNotifications() {
|
||||||
|
bookmarksNotificationsDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppress Sonar warning replace list collection by Stream.toList call, as this method is only
|
||||||
|
// available in Android API 34 and not currently available with desugaring
|
||||||
|
private fun populateBookmarks() =
|
||||||
|
playlists.firstOrError().map { playlist ->
|
||||||
|
playlist.filterNotNull().map { createPlaylistMediaItem(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateLocalPlaylist(playlistId: Long): Single<List<MediaBrowserCompat.MediaItem>> =
|
||||||
|
localPlaylistManager
|
||||||
|
.getPlaylistStreams(playlistId)
|
||||||
|
.firstOrError()
|
||||||
|
.map { items: List<PlaylistStreamEntry> ->
|
||||||
|
val results: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||||
|
for ((index, item) in items.withIndex()) {
|
||||||
|
results.add(createLocalPlaylistStreamMediaItem(playlistId, item, index))
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRemotePlaylist(playlistId: Long): Single<List<Pair<StreamInfoItem, Int>>> =
|
||||||
|
remotePlaylistManager
|
||||||
|
.getPlaylist(playlistId)
|
||||||
|
.firstOrError()
|
||||||
|
.map { playlistEntities ->
|
||||||
|
val playlist = playlistEntities[0]
|
||||||
|
ExtractorHelper
|
||||||
|
.getPlaylistInfo(playlist.serviceId, playlist.url, false)
|
||||||
|
.map { info ->
|
||||||
|
handlePlaylistInfoErrors(info)
|
||||||
|
info.relatedItems.withIndex().map { (index, item) -> item to index }
|
||||||
|
}
|
||||||
|
}.flatMap { it }
|
||||||
|
|
||||||
|
private fun handlePlaylistInfoErrors(info: PlaylistInfo) {
|
||||||
|
val nonContentNotSupportedErrors = info.errors.filterNot { it is ContentNotSupportedException }
|
||||||
|
if (nonContentNotSupportedErrors.isNotEmpty()) {
|
||||||
|
throw nonContentNotSupportedErrors.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populateRemotePlaylist(playlistId: Long): Single<List<MediaBrowserCompat.MediaItem>> =
|
||||||
|
getRemotePlaylist(playlistId).map { items ->
|
||||||
|
items.map { pair ->
|
||||||
|
createRemotePlaylistStreamMediaItem(
|
||||||
|
playlistId,
|
||||||
|
pair.first,
|
||||||
|
pair.second,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playbackError(@StringRes resId: Int, code: Int) {
|
||||||
|
playerService.stopForImmediateReusing()
|
||||||
|
sessionConnector.setCustomErrorMessage(playerService.getString(resId), code)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun playbackError(errorInfo: ErrorInfo) {
|
||||||
|
playbackError(errorInfo.messageStringId, PlaybackStateCompat.ERROR_CODE_APP_ERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractLocalPlayQueue(playlistId: Long, index: Int): Single<PlayQueue> {
|
||||||
|
return localPlaylistManager.getPlaylistStreams(playlistId)
|
||||||
|
.firstOrError()
|
||||||
|
.map { items: List<PlaylistStreamEntry> ->
|
||||||
|
val infoItems = items.stream()
|
||||||
|
.map { obj: PlaylistStreamEntry -> obj.toStreamInfoItem() }
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
SinglePlayQueue(infoItems, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractRemotePlayQueue(playlistId: Long, index: Int): Single<PlayQueue> {
|
||||||
|
return getRemotePlaylist(playlistId).map { items ->
|
||||||
|
val infoItems = items.map { (item, _) -> item }
|
||||||
|
SinglePlayQueue(infoItems, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractPlayQueueFromMediaId(mediaId: String): Single<PlayQueue> {
|
||||||
|
try {
|
||||||
|
val mediaIdUri = Uri.parse(mediaId)
|
||||||
|
val path = mediaIdUri.pathSegments
|
||||||
|
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
throw parseError()
|
||||||
|
}
|
||||||
|
|
||||||
|
val uriType = path[0]
|
||||||
|
path.removeAt(0)
|
||||||
|
|
||||||
|
return when (uriType) {
|
||||||
|
ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId(
|
||||||
|
path,
|
||||||
|
mediaIdUri.getQueryParameter(ID_URL)
|
||||||
|
)
|
||||||
|
|
||||||
|
ID_HISTORY -> extractPlayQueueFromHistoryMediaId(path)
|
||||||
|
ID_INFO_ITEM -> extractPlayQueueFromInfoItemMediaId(
|
||||||
|
path,
|
||||||
|
mediaIdUri.getQueryParameter(ID_URL)
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> throw parseError()
|
||||||
|
}
|
||||||
|
} catch (error: ContentNotAvailableException) {
|
||||||
|
return Single.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(ContentNotAvailableException::class)
|
||||||
|
private fun extractPlayQueueFromPlaylistMediaId(
|
||||||
|
mediaIdSegments: List<String>,
|
||||||
|
url: String?,
|
||||||
|
): Single<PlayQueue> {
|
||||||
|
if (mediaIdSegments.isEmpty()) {
|
||||||
|
throw parseError()
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val playlistType = mediaIdSegments.first()) {
|
||||||
|
ID_LOCAL, ID_REMOTE -> {
|
||||||
|
if (mediaIdSegments.size != 2) {
|
||||||
|
throw parseError()
|
||||||
|
}
|
||||||
|
val playlistId = mediaIdSegments[0].toLong()
|
||||||
|
val index = mediaIdSegments[1].toInt()
|
||||||
|
return if (playlistType == ID_LOCAL) {
|
||||||
|
extractLocalPlayQueue(playlistId, index)
|
||||||
|
} else {
|
||||||
|
extractRemotePlayQueue(playlistId, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ID_URL -> {
|
||||||
|
if (mediaIdSegments.size != 1) {
|
||||||
|
throw parseError()
|
||||||
|
}
|
||||||
|
|
||||||
|
val serviceId = mediaIdSegments[0].toInt()
|
||||||
|
return ExtractorHelper
|
||||||
|
.getPlaylistInfo(serviceId, url, false)
|
||||||
|
.map(::PlaylistPlayQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw parseError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(ContentNotAvailableException::class)
|
||||||
|
private fun extractPlayQueueFromHistoryMediaId(
|
||||||
|
path: List<String>
|
||||||
|
): Single<PlayQueue> {
|
||||||
|
if (path.size != 1) {
|
||||||
|
throw parseError()
|
||||||
|
}
|
||||||
|
|
||||||
|
val streamId = path[0].toLong()
|
||||||
|
return database
|
||||||
|
.streamHistoryDAO()
|
||||||
|
.history
|
||||||
|
.firstOrError()
|
||||||
|
.map { items ->
|
||||||
|
val infoItems =
|
||||||
|
items
|
||||||
|
.filter { streamHistoryEntry -> streamHistoryEntry.streamId == streamId }
|
||||||
|
.map { streamHistoryEntry -> streamHistoryEntry.toStreamInfoItem() }
|
||||||
|
SinglePlayQueue(infoItems, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSupportedPrepareActions() = PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
|
||||||
|
|
||||||
|
private fun disposePrepareOrPlayCommands() {
|
||||||
|
prepareOrPlayDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepare(playWhenReady: Boolean) {
|
||||||
|
disposePrepareOrPlayCommands()
|
||||||
|
// No need to prepare
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareFromMediaId(
|
||||||
|
mediaId: String,
|
||||||
|
playWhenReady: Boolean,
|
||||||
|
extras: Bundle?
|
||||||
|
) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
String.format(
|
||||||
|
"MediaBrowserConnector.onPrepareFromMediaId(%s, %s, %s)",
|
||||||
|
mediaId, playWhenReady, extras
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
disposePrepareOrPlayCommands()
|
||||||
|
prepareOrPlayDisposable = extractPlayQueueFromMediaId(mediaId)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{ playQueue ->
|
||||||
|
sessionConnector.setCustomErrorMessage(null)
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(
|
||||||
|
playerService, playQueue,
|
||||||
|
playWhenReady
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ throwable ->
|
||||||
|
playbackError(
|
||||||
|
ErrorInfo(
|
||||||
|
throwable, UserAction.PLAY_STREAM,
|
||||||
|
"Failed playback of media ID [$mediaId]: "
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareFromSearch(
|
||||||
|
query: String,
|
||||||
|
playWhenReady: Boolean,
|
||||||
|
extras: Bundle?
|
||||||
|
) {
|
||||||
|
disposePrepareOrPlayCommands()
|
||||||
|
playbackError(
|
||||||
|
R.string.content_not_supported,
|
||||||
|
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMusicBySongTitle(query: String): Single<SearchInfo> {
|
||||||
|
val serviceId = ServiceHelper.getSelectedServiceId(playerService)
|
||||||
|
return ExtractorHelper.searchFor(
|
||||||
|
serviceId, query,
|
||||||
|
ArrayList(), ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mediaItemsFromInfoItemList(result: ListInfo<InfoItem>): SingleSource<List<MediaBrowserCompat.MediaItem>> {
|
||||||
|
result.errors
|
||||||
|
.takeIf { exceptions ->
|
||||||
|
exceptions.isNotEmpty() &&
|
||||||
|
!(
|
||||||
|
exceptions.size == 1 &&
|
||||||
|
exceptions.first() is NothingFoundException
|
||||||
|
)
|
||||||
|
}?.let { exceptions ->
|
||||||
|
return Single.error(exceptions.first())
|
||||||
|
}
|
||||||
|
|
||||||
|
val items = result.relatedItems
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
return Single.error(NullPointerException("Got no search results."))
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val results =
|
||||||
|
items
|
||||||
|
.filter { item ->
|
||||||
|
item.infoType == InfoType.STREAM ||
|
||||||
|
item.infoType == InfoType.PLAYLIST ||
|
||||||
|
item.infoType == InfoType.CHANNEL
|
||||||
|
}.map { item -> this.createInfoItemMediaItem(item) }
|
||||||
|
|
||||||
|
return Single.just(results)
|
||||||
|
} catch (error: Exception) {
|
||||||
|
return Single.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSearchError(throwable: Throwable) {
|
||||||
|
Log.e(TAG, "Search error: $throwable")
|
||||||
|
disposePrepareOrPlayCommands()
|
||||||
|
playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareFromUri(
|
||||||
|
uri: Uri,
|
||||||
|
playWhenReady: Boolean,
|
||||||
|
extras: Bundle?
|
||||||
|
) {
|
||||||
|
disposePrepareOrPlayCommands()
|
||||||
|
playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCommand(
|
||||||
|
player: Player,
|
||||||
|
command: String,
|
||||||
|
extras: Bundle?,
|
||||||
|
cb: ResultReceiver?,
|
||||||
|
) = false
|
||||||
|
|
||||||
|
fun onSearch(
|
||||||
|
query: String,
|
||||||
|
result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>,
|
||||||
|
) {
|
||||||
|
result.detach()
|
||||||
|
searchDisposable?.dispose()
|
||||||
|
searchDisposable =
|
||||||
|
searchMusicBySongTitle(query)
|
||||||
|
.flatMap(::mediaItemsFromInfoItemList)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe(
|
||||||
|
{ mediaItemsResult -> result.sendResult(mediaItemsResult) },
|
||||||
|
{ throwable -> this.handleSearchError(throwable) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = MediaBrowserConnector::class.java.simpleName
|
||||||
|
|
||||||
|
private const val ID_AUTHORITY = BuildConfig.APPLICATION_ID
|
||||||
|
private const val ID_ROOT = "//$ID_AUTHORITY"
|
||||||
|
private const val ID_BOOKMARKS = "playlists"
|
||||||
|
private const val ID_HISTORY = "history"
|
||||||
|
private const val ID_INFO_ITEM = "item"
|
||||||
|
|
||||||
|
private const val ID_LOCAL = "local"
|
||||||
|
private const val ID_REMOTE = "remote"
|
||||||
|
private const val ID_URL = "url"
|
||||||
|
private const val ID_STREAM = "stream"
|
||||||
|
private const val ID_PLAYLIST = "playlist"
|
||||||
|
private const val ID_CHANNEL = "channel"
|
||||||
|
|
||||||
|
private fun infoItemTypeToString(type: InfoType): String {
|
||||||
|
return when (type) {
|
||||||
|
InfoType.STREAM -> ID_STREAM
|
||||||
|
InfoType.PLAYLIST -> ID_PLAYLIST
|
||||||
|
InfoType.CHANNEL -> ID_CHANNEL
|
||||||
|
else -> throw IllegalStateException("Unexpected value: $type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun infoItemTypeFromString(type: String): InfoType {
|
||||||
|
return when (type) {
|
||||||
|
ID_STREAM -> InfoType.STREAM
|
||||||
|
ID_PLAYLIST -> InfoType.PLAYLIST
|
||||||
|
ID_CHANNEL -> InfoType.CHANNEL
|
||||||
|
else -> throw IllegalStateException("Unexpected value: $type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseError(): ContentNotAvailableException {
|
||||||
|
return ContentNotAvailableException("Failed to parse media ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(ContentNotAvailableException::class)
|
||||||
|
private fun extractPlayQueueFromInfoItemMediaId(
|
||||||
|
path: List<String>,
|
||||||
|
url: String?
|
||||||
|
): Single<PlayQueue> {
|
||||||
|
if (path.size != 2) {
|
||||||
|
throw parseError()
|
||||||
|
}
|
||||||
|
val infoItemType = infoItemTypeFromString(path[0])
|
||||||
|
val serviceId = path[1].toInt()
|
||||||
|
return when (infoItemType) {
|
||||||
|
InfoType.STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false)
|
||||||
|
.map(::SinglePlayQueue)
|
||||||
|
|
||||||
|
InfoType.PLAYLIST -> ExtractorHelper.getPlaylistInfo(serviceId, url, false)
|
||||||
|
.map(::PlaylistPlayQueue)
|
||||||
|
|
||||||
|
InfoType.CHANNEL -> ExtractorHelper.getChannelInfo(serviceId, url, false)
|
||||||
|
.map { info ->
|
||||||
|
val playableTab = info.tabs
|
||||||
|
.stream()
|
||||||
|
.filter { tab -> ChannelTabHelper.isStreamsTab(tab) }
|
||||||
|
.findFirst()
|
||||||
|
if (playableTab.isPresent) {
|
||||||
|
return@map ChannelTabPlayQueue(
|
||||||
|
serviceId,
|
||||||
|
ListLinkHandler(playableTab.get())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw ContentNotAvailableException("No streams tab found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw parseError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user