Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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 org.schabi.newpipe.R
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
import org.schabi.newpipe.extractor.exceptions.ParsingException
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeStreamLinkHandlerFactory
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

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.url }
.mapNotNull(::getYouTubeId)
.take(50) // YouTube limitation: temp playlists can't have more than 50 items
.toList()
.asReversed()
.joinToString(separator = ",")

return "https://www.youtube.com/watch_videos?video_ids=$videoIDs"
}

val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFactory.getInstance()

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

return try { linkHandler.getId(url) } catch (e: ParsingException) { null }
}
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,13 @@ 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)
)
.setNeutralButton(R.string.share_playlist_as_youtube_temporary_playlist,
(dialog, which) -> 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
}
5 changes: 3 additions & 2 deletions app/src/main/res/values-pt-rBR/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -791,8 +791,9 @@
<string name="channel_tab_livestreams">Ao vivo</string>
<string name="image_quality_title">Qualidade da imagem</string>
<string name="question_mark">\?</string>
<string name="share_playlist_with_list">Compartilhar URL</string>
<string name="share_playlist_with_titles">Compartilhar com título</string>
<string name="share_playlist_with_list">Compartilhar URLs</string>
<string name="share_playlist_with_titles">Compartilhar com títulos</string>
<string name="share_playlist_as_youtube_temporary_playlist">Compartilhar como playlist temporária do YouTube</string>
<string name="share_playlist_content_details">%1$s
\n%2$s</string>
<string name="toggle_screen_orientation">Alternar orientação da tela</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,7 @@
<string name="share_playlist_with_titles_message">Share playlist with details such as playlist name and video titles or as a simple list of video URLs</string>
<string name="share_playlist_with_titles">Share with Titles</string>
<string name="share_playlist_with_list">Share URL list</string>
<string name="share_playlist_as_youtube_temporary_playlist">Share as YouTube temporary playlist</string>
<string name="video_details_list_item">- %1$s: %2$s</string>
<string name="share_playlist_content_details">%1$s\n%2$s</string>
<plurals name="replies">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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=10000000000",
"https://soundcloud.com/cautious-clayofficial/cold-war-2", // non-Youtube URLs should be ignored
"https://www.youtube.com/watch?v=20000000000",
"https://www.youtube.com/watch?v=30000000000"
)

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

assertEquals(
"https://www.youtube.com/watch_videos?video_ids=" +
"10000000000," +
"20000000000," +
"30000000000",
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 playlist = asPlaylist(
(10..70)
.map { id -> "https://www.youtube.com/watch?v=aaaaaaaaa$id" } // YouTube video IDs are 11 characters long
.stream()
)

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

val videoIDs = (21..70).map { id -> "aaaaaaaaa$id" }.joinToString(",")

assertEquals(
"https://www.youtube.com/watch_videos?video_ids=$videoIDs",
url
)
}

@Test
fun exportJustUrls() {
val playlist = asPlaylist(
"https://www.youtube.com/watch?v=10000000000",
"https://www.youtube.com/watch?v=20000000000",
"https://www.youtube.com/watch?v=30000000000"
)

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

assertEquals(
"""
https://www.youtube.com/watch?v=10000000000
https://www.youtube.com/watch?v=20000000000
https://www.youtube.com/watch?v=30000000000
""".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"
)
}