Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ dependencies {
// HTTP client
implementation "com.squareup.okhttp3:okhttp:4.12.0"

// Apache Commons Collections
implementation group: 'org.apache.commons', name: 'commons-collections4', version: '4.4'
Comment thread
Stypox marked this conversation as resolved.
Outdated
Comment thread
Stypox marked this conversation as resolved.
Outdated

// Media player
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.schabi.newpipe.local.playlist

import android.content.Context
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.schabi.newpipe.R
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
import org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS
import org.schabi.newpipe.local.playlist.PlayListShareMode.WITH_TITLES
import org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST
import java.util.Objects.nonNull

fun export(
shareMode: PlayListShareMode,
playlist: List<PlaylistStreamEntry>,
context: Context
): String {
return when (shareMode) {
WITH_TITLES -> exportWithTitles(playlist, context)
JUST_URLS -> exportJustUrls(playlist)
YOUTUBE_TEMP_PLAYLIST -> exportAsYoutubeTempPlaylist(playlist)
}
}

fun exportWithTitles(
playlist: List<PlaylistStreamEntry>,
context: Context
): String {

return playlist.asSequence()
.map { it.streamEntity }
.map { entity ->
context.getString(
R.string.video_details_list_item,
entity.title,
entity.url
)
}
.joinToString(separator = "\n")
}

fun exportJustUrls(playlist: List<PlaylistStreamEntry>): String {

return playlist.asSequence()
.map { it.streamEntity.url }
.joinToString(separator = "\n")
}

fun exportAsYoutubeTempPlaylist(playlist: List<PlaylistStreamEntry>): String {

val videoIDs = playlist.asReversed().asSequence()
.map { it.streamEntity }
.map { getYouTubeId(it.url) }
.filter(::nonNull)
.take(50)
Comment thread
Stypox marked this conversation as resolved.
Outdated
.toList()
.asReversed()
.joinToString(separator = ",")

return "http://www.youtube.com/watch_videos?video_ids=$videoIDs"
Comment thread
tfga marked this conversation as resolved.
Outdated
}

/**
* Gets the video id from a YouTube URL.
*
* @param url YouTube URL
* @return the video id
*/
fun getYouTubeId(url: String): String? {
val httpUrl = url.toHttpUrlOrNull()

return httpUrl?.queryParameter("v")
}
Comment thread
Stypox marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.local.playlist.ExportPlaylistKt.export;
import static org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS;
import static org.schabi.newpipe.local.playlist.PlayListShareMode.WITH_TITLES;
import static org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;


import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
Expand All @@ -27,7 +32,6 @@
import androidx.viewbinding.ViewBinding;

import com.evernote.android.state.State;

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
Expand Down Expand Up @@ -384,35 +388,41 @@ public boolean onOptionsItemSelected(final MenuItem item) {
return true;
}

/**
* Shares the playlist as a list of stream URLs if {@code shouldSharePlaylistDetails} is
* set to {@code false}. Shares the playlist name along with a list of video titles and URLs
* if {@code shouldSharePlaylistDetails} is set to {@code true}.
*
* @param shouldSharePlaylistDetails Whether the playlist details should be included in the
* shared content.
*/
private void sharePlaylist(final boolean shouldSharePlaylistDetails) {
///
/// Shares the playlist in one of 3 ways, depending on the value of `shareMode`:
///
/// - `JUST_URLS`: shares the URLs only.
/// - `WITH_TITLES`: each entry in the list is accompanied by its title.
/// - `YOUTUBE_TEMP_PLAYLIST`: shares as a YouTube temporary playlist.
///
/// @param shareMode The way the playlist should be shared.
///
Comment thread
Stypox marked this conversation as resolved.
Outdated
private void sharePlaylist(final PlayListShareMode shareMode) {
final Context context = requireContext();

disposables.add(playlistManager.getPlaylistStreams(playlistId)
.flatMapSingle(playlist -> Single.just(playlist.stream()
.map(PlaylistStreamEntry::getStreamEntity)
.map(streamEntity -> {
if (shouldSharePlaylistDetails) {
return context.getString(R.string.video_details_list_item,
streamEntity.getTitle(), streamEntity.getUrl());
} else {
return streamEntity.getUrl();
}
})
.collect(Collectors.joining("\n"))))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(urlsText -> ShareUtils.shareText(
context, name, shouldSharePlaylistDetails
? context.getString(R.string.share_playlist_content_details,
name, urlsText) : urlsText),
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
.flatMapSingle(playlist -> Single.just(export(

shareMode,
playlist,
context
)))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
urlsText -> {

final String content = shareMode == JUST_URLS
? urlsText
: context.getString(R.string.share_playlist_content_details,
name,
urlsText
);

ShareUtils.shareText(context, name, content);
},
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)
)
);
}

public void removeWatchedStreams(final boolean removePartiallyWatched) {
Expand Down Expand Up @@ -875,10 +885,14 @@ private void createShareConfirmationDialog() {
.setMessage(R.string.share_playlist_with_titles_message)
.setCancelable(true)
.setPositiveButton(R.string.share_playlist_with_titles, (dialog, which) ->
sharePlaylist(/* shouldSharePlaylistDetails= */ true)
sharePlaylist(WITH_TITLES)
)
// TODO R.string.share_playlist_as_YouTube_temporary_playlist
.setNeutralButton("Share as YouTube temporary playlist", (dialog, which) ->
Comment thread
Stypox marked this conversation as resolved.
Outdated
sharePlaylist(YOUTUBE_TEMP_PLAYLIST)
)
.setNegativeButton(R.string.share_playlist_with_list, (dialog, which) ->
sharePlaylist(/* shouldSharePlaylistDetails= */ false)
sharePlaylist(JUST_URLS)
)
.show();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.schabi.newpipe.local.playlist;

public enum PlayListShareMode {

JUST_URLS,
WITH_TITLES,
YOUTUBE_TEMP_PLAYLIST
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.schabi.newpipe.local.playlist

import android.content.Context
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mockito.Mockito.mock
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS
import org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST
import java.util.stream.Stream

class ExportPlaylistTest {

@Test
fun exportAsYouTubeTempPlaylist() {
val playlist = asPlaylist(
"https://www.youtube.com/watch?v=1",
"https://soundcloud.com/cautious-clayofficial/cold-war-2", // non-Youtube URLs should be ignored
"https://www.youtube.com/watch?v=2",
"https://www.youtube.com/watch?v=3"
)

val url = export(YOUTUBE_TEMP_PLAYLIST, playlist, mock(Context::class.java))

assertEquals("http://www.youtube.com/watch_videos?video_ids=1,2,3", url)
}

@Test
fun exportMoreThan50Items() {
/*
* Playlist has more than 50 items => take the last 50
Comment thread
Stypox marked this conversation as resolved.
* (YouTube limitation)
*/

val ids = listOf(
-1, 0,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50
)

val playlist = asPlaylist(
ids.stream()
.map { id: Int -> "https://www.youtube.com/watch?v=$id" }
)
Comment thread
Stypox marked this conversation as resolved.
Outdated

val url = export(YOUTUBE_TEMP_PLAYLIST, playlist, mock(Context::class.java))

assertEquals(
"http://www.youtube.com/watch_videos?video_ids=" +
"1,2,3,4,5,6,7,8,9,10," +
"11,12,13,14,15,16,17,18,19,20," +
"21,22,23,24,25,26,27,28,29,30," +
"31,32,33,34,35,36,37,38,39,40," +
"41,42,43,44,45,46,47,48,49,50",
Comment thread
Stypox marked this conversation as resolved.
Outdated

url
)
}

@Test
fun exportJustUrls() {
val playlist = asPlaylist(
"https://www.youtube.com/watch?v=1",
"https://www.youtube.com/watch?v=2",
"https://www.youtube.com/watch?v=3"
Comment thread
tfga marked this conversation as resolved.
Outdated
)

val exported = export(JUST_URLS, playlist, mock(Context::class.java))

assertEquals(
"""
https://www.youtube.com/watch?v=1
https://www.youtube.com/watch?v=2
https://www.youtube.com/watch?v=3
Comment thread
tfga marked this conversation as resolved.
Outdated
""".trimIndent(),
exported
)
}
}

fun asPlaylist(vararg urls: String): List<PlaylistStreamEntry> {
return asPlaylist(Stream.of(*urls))
}

fun asPlaylist(urls: Stream<String>): List<PlaylistStreamEntry> {
return urls
.map { url: String -> newPlaylistStreamEntry(url) }
.toList()
}

fun newPlaylistStreamEntry(url: String): PlaylistStreamEntry {
return PlaylistStreamEntry(newStreamEntity(url), 0, 0, 0)
}

fun newStreamEntity(url: String): StreamEntity {
return StreamEntity(
0,
1,
url,
"Title",
StreamType.VIDEO_STREAM,
100,
"Uploader"
)
}