diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index eef870f82e6..1c77c190aaf 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -1298,7 +1298,7 @@ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) { if (playQueue != null) { if (shuffleModeEnabled) { - playQueue.shuffle(); + playQueue.shuffle(false); } else { playQueue.unshuffle(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt index b8f07fd7142..abcc50b7ab4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt @@ -148,8 +148,8 @@ class PlayerService : MediaBrowserServiceCompat() { // a (dummy) foreground notification, otherwise we'd incur in // "Context.startForegroundService() did not then call Service.startForeground()". Then // we stop the service again. - Log.d(TAG, "onStartCommand() got a useless intent, closing the service"); - NotificationUtil.startForegroundWithDummyNotification(this); + Log.d(TAG, "onStartCommand() got a useless intent, closing the service") + NotificationUtil.startForegroundWithDummyNotification(this) return START_NOT_STICKY } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserCommon.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserCommon.kt index 12d69a163a0..32449af017f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserCommon.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserCommon.kt @@ -17,6 +17,8 @@ internal const val ID_STREAM = "stream" internal const val ID_PLAYLIST = "playlist" internal const val ID_CHANNEL = "channel" +internal const val ID_SHUFFLE = "ID_SHUFFLE" + internal fun infoItemTypeToString(type: InfoType): String { return when (type) { InfoType.STREAM -> ID_STREAM diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt index 149f47c1a5b..54c9bd90aeb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt @@ -257,6 +257,31 @@ class MediaBrowserImpl( .build().toString() } + private fun createShuffleAndPlayMediaItem( + isRemote: Boolean, + playlistId: Long + ): MediaBrowserCompat.MediaItem { + val resources = context.resources + + val builder = MediaDescriptionCompat.Builder() + .setMediaId(createMediaIdForPlaylistShuffle(isRemote, playlistId)) + .setTitle(resources.getString(R.string.shuffle_and_play)) + + @DrawableRes val iconResId = R.drawable.ic_shuffle_white + builder.setIconUri( + Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(iconResId)) + .appendPath(resources.getResourceTypeName(iconResId)) + .appendPath(resources.getResourceEntryName(iconResId)) + .build() + ) + + return MediaBrowserCompat.MediaItem( + builder.build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + ) + } + private fun createLocalPlaylistStreamMediaItem( playlistId: Long, item: PlaylistStreamEntry, @@ -301,6 +326,15 @@ class MediaBrowserImpl( .build().toString() } + private fun createMediaIdForPlaylistShuffle( + isRemote: Boolean, + playlistId: Long, + ): String { + return buildLocalPlaylistItemMediaId(isRemote, playlistId) + .appendPath(ID_SHUFFLE) + .build().toString() + } + private fun createMediaIdForInfoItem(item: InfoItem): String { return buildInfoItemMediaId(item).build().toString() } @@ -346,7 +380,10 @@ class MediaBrowserImpl( private fun populateLocalPlaylist(playlistId: Long): Single> { val playlist = LocalPlaylistManager(database).getPlaylistStreams(playlistId).firstOrError() return playlist.map { items -> - items.mapIndexed { index, item -> + val quickActions = if (items.isEmpty()) emptyList() else listOf( + createShuffleAndPlayMediaItem(false, playlistId) + ) + quickActions + items.mapIndexed { index, item -> createLocalPlaylistStreamMediaItem(playlistId, item, index) } } @@ -356,9 +393,12 @@ class MediaBrowserImpl( return RemotePlaylistManager(database).getPlaylist(playlistId).firstOrError() .flatMap { ExtractorHelper.getPlaylistInfo(it.serviceId, it.url, false) } .map { + val quickActions = if (it.relatedItems.isEmpty()) emptyList() else listOf( + createShuffleAndPlayMediaItem(true, playlistId) + ) // ignore it.errors, i.e. ignore errors about specific items, since there would // be no way to show the error properly in Android Auto anyway - it.relatedItems.mapIndexed { index, item -> + quickActions + it.relatedItems.mapIndexed { index, item -> createRemotePlaylistStreamMediaItem(playlistId, item, index) } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt index fbd50645651..fd2f7ea43a3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt @@ -138,6 +138,17 @@ class MediaBrowserPlaybackPreparer( .map { info -> PlaylistPlayQueue(info, index) } } + private fun extractShufflePlayQueue(isRemote: Boolean, playlistId: Long): Single { + return if (isRemote) { + extractRemotePlayQueue(playlistId, 0) + } else { + extractLocalPlayQueue(playlistId, 0) + }.map { playQueue -> + playQueue.shuffle(true) + playQueue + } + } + private fun extractPlayQueueFromMediaId(mediaId: String): Single { try { val mediaIdUri = mediaId.toUri() @@ -184,6 +195,10 @@ class MediaBrowserPlaybackPreparer( throw parseError(mediaId) } val playlistId = path[0].toLong() + if (ID_SHUFFLE == path[1]) { + return extractShufflePlayQueue(playlistType == ID_REMOTE, playlistId) + } + val index = path[1].toInt() return if (playlistType == ID_LOCAL) extractLocalPlayQueue(playlistId, index) diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt index ab7dee2d04f..863c09e967f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.kt @@ -399,17 +399,21 @@ abstract class PlayQueue internal constructor( /** * Shuffles the current play queue * - * This method first backs up the existing play queue and item being played. Then a newly - * shuffled play queue will be generated along with currently playing item placed at the - * beginning of the queue. This item will also be added to the history. + * This method first backs up the existing play queue. By default, the currently playing item + * is preserved at the beginning of the queue, with the remaining items shuffled. + * If [shuffleAll] is true, all items in the queue will be shuffled without preserving the + * currently playing item at the head of the queue. * - * Will emit a [ReorderEvent] if shuffled. + * When the currently playing item is preserved, it will also be added to the history and will + * emit a [ReorderEvent] if the currently playing item position changes. * + * @param shuffleAll whether to shuffle all items in the queue or preserve the currently + * playing item at the head * @implNote Does nothing if the queue has a size <= 2 (the currently playing video must stay on * top, so shuffling a size-2 list does nothing) */ @Synchronized - fun shuffle() { + fun shuffle(shuffleAll: Boolean = false) { // Create a backup if it doesn't already exist // Note: The backup-list has to be created at all cost (even when size <= 2). // Otherwise it's not possible to enter shuffle-mode! @@ -421,13 +425,18 @@ abstract class PlayQueue internal constructor( return } + if (shuffleAll) { + streams.shuffle() + return + } + val originalIndex = this.index - val currentItem = this.item + val currentItem = this.item!! streams.shuffle() // Move currentItem to the head of the queue - streams.remove(currentItem!!) + streams.remove(currentItem) streams.add(0, currentItem) queueIndex.set(0) diff --git a/app/src/main/res/drawable/ic_shuffle_white.xml b/app/src/main/res/drawable/ic_shuffle_white.xml new file mode 100644 index 00000000000..91e664abdbb --- /dev/null +++ b/app/src/main/res/drawable/ic_shuffle_white.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c439f19e272..9d5f7981f2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -666,6 +666,7 @@ App language System default Remove watched + Shuffle and play Remove watched videos? Remove duplicates Remove duplicates? diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ab8103909d8..35190b99aad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ teamnewpipe-nanojson = "e9d656ddb49a412a5a0a5d5ef20ca7ef09549996" # the corresponding commit hash, since JitPack sometimes deletes artifacts. # If there’s already a git hash, just add more of it to the end (or remove a letter) # to cause jitpack to regenerate the artifact. -teamnewpipe-newpipe-extractor = "0023b22095a2d62a60cdfc87f4b5cd85c8b266c3" +teamnewpipe-newpipe-extractor = "0023b22095a2d62a60cdfc87f4b5cd85c8b266c" webkit = "1.9.0" work = "2.10.0"