Skip to content

Commit

Permalink
Merge pull request #6855 from Bnyro/master
Browse files Browse the repository at this point in the history
feat: support for exporting single playlists
  • Loading branch information
Bnyro authored Dec 5, 2024
2 parents e4bbd51 + fa493db commit 2b40807
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 119 deletions.
33 changes: 3 additions & 30 deletions app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import com.github.libretube.extensions.parallelMap
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.FreeTubeImportPlaylist
import com.github.libretube.obj.FreeTubeVideo
import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
import com.github.libretube.util.deArrow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
Expand Down Expand Up @@ -201,35 +198,11 @@ object PlaylistsHelper {
}
}

suspend fun exportPipedPlaylists(): List<PipedImportPlaylist> = withContext(Dispatchers.IO) {
getPlaylists()
.map { async { getPlaylist(it.id!!) } }
.awaitAll()
.map {
val videos = it.relatedStreams.map { item ->
"$YOUTUBE_FRONTEND_URL/watch?v=${item.url!!.toID()}"
}
PipedImportPlaylist(it.name, "playlist", "private", videos)
}
}

suspend fun exportFreeTubePlaylists(): List<FreeTubeImportPlaylist> =
suspend fun getAllPlaylistsWithVideos(playlistIds: List<String>? = null): List<Playlist> =
withContext(Dispatchers.IO) {
getPlaylists()
.map { async { getPlaylist(it.id!!) } }
(playlistIds ?: getPlaylists().map { it.id!! })
.map { async { getPlaylist(it) } }
.awaitAll()
.map { playlist ->
val videos = playlist.relatedStreams.map { videoInfo ->
FreeTubeVideo(
videoId = videoInfo.url.orEmpty().toID(),
title = videoInfo.title.orEmpty(),
author = videoInfo.uploaderName.orEmpty(),
authorId = videoInfo.uploaderUrl.orEmpty().toID(),
lengthSeconds = videoInfo.duration ?: 0L
)
}
FreeTubeImportPlaylist(playlist.name.orEmpty(), videos)
}
}

suspend fun clonePlaylist(playlistId: String): String? {
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/github/libretube/enums/ImportFormat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.libretube.enums

import androidx.annotation.StringRes
import com.github.libretube.R

enum class ImportFormat(@StringRes val value: Int, val fileExtension: String) {
NEWPIPE(R.string.import_format_newpipe, "json"),
FREETUBE(R.string.import_format_freetube, "json"),
YOUTUBECSV(R.string.import_format_youtube_csv, "csv"),
YOUTUBEJSON(R.string.youtube, "json"),
PIPED(R.string.import_format_piped, "json"),
URLSORIDS(R.string.import_format_list_of_urls, "txt")
}
13 changes: 0 additions & 13 deletions app/src/main/java/com/github/libretube/enums/SupportedClient.kt

This file was deleted.

66 changes: 51 additions & 15 deletions app/src/main/java/com/github/libretube/helpers/ImportHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.enums.ImportFormat
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.obj.FreeTubeImportPlaylist
import com.github.libretube.obj.FreeTubeVideo
import com.github.libretube.obj.FreetubeSubscription
import com.github.libretube.obj.FreetubeSubscriptions
import com.github.libretube.obj.NewPipeSubscription
import com.github.libretube.obj.NewPipeSubscriptions
import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.obj.PipedPlaylistFile
import com.github.libretube.obj.YouTubeWatchHistoryFileItem
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_SHORT_URL
import com.github.libretube.util.TextUtils
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
Expand All @@ -34,6 +37,7 @@ import java.util.stream.Collectors

object ImportHelper {
private const val IMPORT_THUMBNAIL_QUALITY = "mqdefault"
private const val VIDEO_ID_LENGTH = 11

/**
* Import subscriptions by a file uri
Expand Down Expand Up @@ -70,7 +74,7 @@ object ImportHelper {
JsonHelper.json.decodeFromStream<NewPipeSubscriptions>(it)
}
subscriptions?.subscriptions.orEmpty().map {
it.url.replace("${ShareDialog.YOUTUBE_FRONTEND_URL}/channel/", "")
it.url.replace("$YOUTUBE_FRONTEND_URL/channel/", "")
}
}

Expand All @@ -79,7 +83,7 @@ object ImportHelper {
JsonHelper.json.decodeFromStream<FreetubeSubscriptions>(it)
}
subscriptions?.subscriptions.orEmpty().map {
it.url.replace("${ShareDialog.YOUTUBE_FRONTEND_URL}/channel/", "")
it.url.replace("$YOUTUBE_FRONTEND_URL/channel/", "")
}
}

Expand Down Expand Up @@ -114,7 +118,7 @@ object ImportHelper {
when (importFormat) {
ImportFormat.NEWPIPE -> {
val newPipeChannels = subs.map {
NewPipeSubscription(it.name, 0, "${ShareDialog.YOUTUBE_FRONTEND_URL}${it.url}")
NewPipeSubscription(it.name, 0, "$YOUTUBE_FRONTEND_URL${it.url}")
}
val newPipeSubscriptions = NewPipeSubscriptions(subscriptions = newPipeChannels)
activity.contentResolver.openOutputStream(uri)?.use {
Expand All @@ -127,7 +131,7 @@ object ImportHelper {
FreetubeSubscription(
it.name,
"",
"${ShareDialog.YOUTUBE_FRONTEND_URL}${it.url}"
"$YOUTUBE_FRONTEND_URL${it.url}"
)
}
val freeTubeSubscriptions = FreetubeSubscriptions(subscriptions = freeTubeChannels)
Expand Down Expand Up @@ -158,7 +162,7 @@ object ImportHelper {

// convert the YouTube URLs to videoIds
importPlaylists.forEach { playlist ->
playlist.videos = playlist.videos.map { it.takeLast(11) }
playlist.videos = playlist.videos.map { it.takeLast(VIDEO_ID_LENGTH) }
}
}

Expand Down Expand Up @@ -225,7 +229,7 @@ object ImportHelper {

// convert the YouTube URLs to videoIds
importPlaylists.forEach { importPlaylist ->
importPlaylist.videos = importPlaylist.videos.map { it.takeLast(11) }
importPlaylist.videos = importPlaylist.videos.map { it.takeLast(VIDEO_ID_LENGTH) }
}
}

Expand All @@ -236,7 +240,7 @@ object ImportHelper {
playlist.videos = inputStream.bufferedReader().readLines()
.flatMap { it.split(",") }
.mapNotNull { videoUrlOrId ->
if (videoUrlOrId.length == 11) {
if (videoUrlOrId.length == VIDEO_ID_LENGTH) {
videoUrlOrId
} else {
TextUtils.getVideoIdFromUri(videoUrlOrId.toUri())
Expand Down Expand Up @@ -272,11 +276,22 @@ object ImportHelper {
* Export Playlists
*/
@OptIn(ExperimentalSerializationApi::class)
suspend fun exportPlaylists(activity: Activity, uri: Uri, importFormat: ImportFormat) {
suspend fun exportPlaylists(
activity: Activity,
uri: Uri,
importFormat: ImportFormat,
selectedPlaylistIds: List<String>? = null
) {
val playlists = PlaylistsHelper.getAllPlaylistsWithVideos(selectedPlaylistIds)

when (importFormat) {
ImportFormat.PIPED -> {
val playlists = PlaylistsHelper.exportPipedPlaylists()
val playlistFile = PipedPlaylistFile(playlists = playlists)
val playlistFile = PipedPlaylistFile(playlists = playlists.map {
val videos = it.relatedStreams.map { item ->
"$YOUTUBE_FRONTEND_URL/watch?v=${item.url!!.toID()}"
}
PipedImportPlaylist(it.name, "playlist", "private", videos)
})

activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(playlistFile, it)
Expand All @@ -285,17 +300,38 @@ object ImportHelper {
}

ImportFormat.FREETUBE -> {
val playlists = PlaylistsHelper.exportFreeTubePlaylists()

val freeTubeExportDb = playlists.joinToString("\n") { playlist ->
val freeTubeExportDb = playlists.map { playlist ->
val videos = playlist.relatedStreams.map { videoInfo ->
FreeTubeVideo(
videoId = videoInfo.url.orEmpty().toID(),
title = videoInfo.title.orEmpty(),
author = videoInfo.uploaderName.orEmpty(),
authorId = videoInfo.uploaderUrl.orEmpty().toID(),
lengthSeconds = videoInfo.duration ?: 0L
)
}
FreeTubeImportPlaylist(playlist.name.orEmpty(), videos)
}.joinToString("\n") { playlist ->
JsonHelper.json.encodeToString(playlist)
}

activity.contentResolver.openOutputStream(uri)?.use {
it.write(freeTubeExportDb.toByteArray())
}
activity.toastFromMainDispatcher(R.string.exportsuccess)
}

ImportFormat.URLSORIDS -> {
val urlListExport = playlists
.flatMap { it.relatedStreams }
.joinToString("\n") { YOUTUBE_SHORT_URL + "/watch?v=" + it.url!!.toID() }

activity.contentResolver.openOutputStream(uri)?.use {
it.write(urlListExport.toByteArray())
}
activity.toastFromMainDispatcher(R.string.exportsuccess)
}

else -> Unit
}
}
Expand All @@ -311,7 +347,7 @@ object ImportHelper {
.filter { it.activityControls.contains("YouTube watch history") && it.subtitles.isNotEmpty() && it.titleUrl.isNotEmpty() }
.reversed()
.map {
val videoId = it.titleUrl.substring(it.titleUrl.length - 11)
val videoId = it.titleUrl.takeLast(VIDEO_ID_LENGTH)

WatchHistoryItem(
videoId = videoId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.view.View
import android.view.ViewTreeObserver
import android.widget.ScrollView
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.ColorInt
import androidx.appcompat.widget.SearchView
Expand All @@ -36,8 +37,10 @@ import com.github.libretube.compat.PictureInPictureCompat
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.enums.ImportFormat
import com.github.libretube.extensions.anyChildFocused
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImportHelper
import com.github.libretube.helpers.IntentHelper
import com.github.libretube.helpers.NavBarHelper
import com.github.libretube.helpers.NavigationHelper
Expand All @@ -54,11 +57,14 @@ import com.github.libretube.ui.fragments.PlayerFragment
import com.github.libretube.ui.models.CommonPlayerViewModel
import com.github.libretube.ui.models.SearchViewModel
import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.ui.preferences.BackupRestoreSettings.Companion.FILETYPE_ANY
import com.github.libretube.ui.preferences.BackupRestoreSettings.Companion.JSON
import com.github.libretube.util.UpdateChecker
import com.google.android.material.elevation.SurfaceColors
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import kotlin.math.exp

class MainActivity : BaseActivity() {
lateinit var binding: ActivityMainBinding
Expand All @@ -75,6 +81,25 @@ class MainActivity : BaseActivity() {
private var savedSearchQuery: String? = null
private var shouldOpenSuggestions = true

// registering for activity results is only possible, this here should have been part of
// PlaylistOptionsBottomSheet instead if Android allowed us to
private var playlistExportFormat: ImportFormat = ImportFormat.NEWPIPE
private var exportPlaylistId: String? = null
private val createPlaylistsFile = registerForActivityResult(
ActivityResultContracts.CreateDocument(FILETYPE_ANY)
) { uri ->
if (uri == null) return@registerForActivityResult

lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.exportPlaylists(
this@MainActivity,
uri,
playlistExportFormat,
selectedPlaylistIds = listOf(exportPlaylistId!!)
)
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -652,4 +677,10 @@ class MainActivity : BaseActivity() {
?.let(action)
?: false
}

fun startPlaylistExport(playlistId: String, playlistName: String, format: ImportFormat) {
playlistExportFormat = format
exportPlaylistId = playlistId
createPlaylistsFile.launch("${playlistName}.${format.fileExtension}")
}
}
Loading

0 comments on commit 2b40807

Please sign in to comment.