Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
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"
)
}