diff --git a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt index d48aadea7..de1028e70 100644 --- a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt +++ b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt @@ -12,7 +12,9 @@ import com.github.libretube.enums.PlaylistType import com.github.libretube.extensions.toID import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.ProxyHelper -import com.github.libretube.obj.ImportPlaylist +import com.github.libretube.obj.FreeTubeImportPlaylist +import com.github.libretube.obj.FreeTubeVideo +import com.github.libretube.obj.PipedImportPlaylist import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -133,7 +135,7 @@ object PlaylistsHelper { } } - suspend fun importPlaylists(playlists: List) = withContext(Dispatchers.IO) { + suspend fun importPlaylists(playlists: List) = withContext(Dispatchers.IO) { playlists.map { playlist -> val playlistId = createPlaylist(playlist.name!!) async { @@ -167,7 +169,7 @@ object PlaylistsHelper { }.awaitAll() } - suspend fun exportPlaylists(): List = withContext(Dispatchers.IO) { + suspend fun exportPipedPlaylists(): List = withContext(Dispatchers.IO) { getPlaylists() .map { async { getPlaylist(it.id!!) } } .awaitAll() @@ -175,10 +177,25 @@ object PlaylistsHelper { val videos = it.relatedStreams.map { item -> "$YOUTUBE_FRONTEND_URL/watch?v=${item.url!!.toID()}" } - ImportPlaylist(it.name, "playlist", "private", videos) + PipedImportPlaylist(it.name, "playlist", "private", videos) } } + suspend fun exportFreeTubePlaylists(): List = + withContext(Dispatchers.IO) { + getPlaylists() + .map { async { getPlaylist(it.id!!) } } + .awaitAll() + .map { + val videos = it.relatedStreams.map { item -> + item.url.orEmpty().replace("$YOUTUBE_FRONTEND_URL/watch?v=${item.url}", "") + }.map { id -> + FreeTubeVideo(id, it.name.orEmpty(), "", "") + } + FreeTubeImportPlaylist(it.name.orEmpty(), videos) + } + } + suspend fun clonePlaylist(playlistId: String): String? { if (!loggedIn) { val playlist = RetrofitInstance.api.getPlaylist(playlistId) diff --git a/app/src/main/java/com/github/libretube/enums/SupportedClient.kt b/app/src/main/java/com/github/libretube/enums/SupportedClient.kt index afb92ed28..1a212e3d3 100644 --- a/app/src/main/java/com/github/libretube/enums/SupportedClient.kt +++ b/app/src/main/java/com/github/libretube/enums/SupportedClient.kt @@ -6,5 +6,6 @@ import com.github.libretube.R enum class ImportFormat(@StringRes val value: Int) { NEWPIPE(R.string.import_format_newpipe), FREETUBE(R.string.import_format_freetube), - YOUTUBECSV(R.string.import_format_youtube_csv); + YOUTUBECSV(R.string.import_format_youtube_csv), + PIPED(R.string.import_format_piped); } \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt b/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt index eef68d349..42b3425a0 100644 --- a/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt @@ -12,10 +12,11 @@ import com.github.libretube.db.DatabaseHolder.Database import com.github.libretube.enums.ImportFormat import com.github.libretube.extensions.TAG import com.github.libretube.extensions.toastFromMainDispatcher +import com.github.libretube.obj.FreeTubeImportPlaylist import com.github.libretube.obj.FreetubeSubscription import com.github.libretube.obj.FreetubeSubscriptions -import com.github.libretube.obj.ImportPlaylist -import com.github.libretube.obj.ImportPlaylistFile +import com.github.libretube.obj.PipedImportPlaylist +import com.github.libretube.obj.PipedImportPlaylistFile import com.github.libretube.obj.NewPipeSubscription import com.github.libretube.obj.NewPipeSubscriptions import kotlinx.serialization.ExperimentalSerializationApi @@ -76,6 +77,7 @@ object ImportHelper { } }.orEmpty() } + else -> throw IllegalArgumentException() } } @@ -123,12 +125,38 @@ object ImportHelper { * Import Playlists */ @OptIn(ExperimentalSerializationApi::class) - suspend fun importPlaylists(activity: Activity, uri: Uri) { - val importPlaylists = mutableListOf() + suspend fun importPlaylists(activity: Activity, uri: Uri, importFormat: ImportFormat) { + val importPlaylists = mutableListOf() - when (val fileType = activity.contentResolver.getType(uri)) { - "text/csv", "text/comma-separated-values" -> { - val playlist = ImportPlaylist() + when (importFormat) { + ImportFormat.PIPED -> { + val playlistFile = activity.contentResolver.openInputStream(uri)?.use { + JsonHelper.json.decodeFromStream(it) + } + importPlaylists.addAll(playlistFile?.playlists.orEmpty()) + + // convert the YouTube URLs to videoIds + importPlaylists.forEach { playlist -> + playlist.videos = playlist.videos.map { it.takeLast(11) } + } + } + ImportFormat.FREETUBE -> { + val playlistFile = activity.contentResolver.openInputStream(uri)?.use { + JsonHelper.json.decodeFromStream>(it) + } + val playlists = playlistFile?.map { playlist -> + // convert FreeTube videos to list of string + // convert FreeTube playlists to piped playlists + PipedImportPlaylist( + playlist.name, + null, + null, + playlist.videos.map { it.videoId }) + } + importPlaylists.addAll(playlists.orEmpty()) + } + ImportFormat.YOUTUBECSV -> { + val playlist = PipedImportPlaylist() activity.contentResolver.openInputStream(uri)?.use { val lines = it.bufferedReader().use { reader -> reader.lines().toList() } playlist.name = lines[1].split(",").reversed()[2] @@ -144,23 +172,13 @@ object ImportHelper { } importPlaylists.add(playlist) } - } - "application/json", "application/*", "application/octet-stream" -> { - val playlistFile = activity.contentResolver.openInputStream(uri)?.use { - JsonHelper.json.decodeFromStream(it) - } - importPlaylists.addAll(playlistFile?.playlists.orEmpty()) - } - else -> { - val message = activity.getString(R.string.unsupported_file_format, fileType) - activity.toastFromMainDispatcher(message) - return - } - } - // convert the YouTube URLs to videoIds - importPlaylists.forEach { playlist -> - playlist.videos = playlist.videos.map { it.takeLast(11) } + // convert the YouTube URLs to videoIds + importPlaylists.forEach { importPlaylist -> + importPlaylist.videos = importPlaylist.videos.map { it.takeLast(11) } + } + } + else -> throw IllegalArgumentException() } try { PlaylistsHelper.importPlaylists(importPlaylists) @@ -177,14 +195,26 @@ object ImportHelper { * Export Playlists */ @OptIn(ExperimentalSerializationApi::class) - suspend fun exportPlaylists(activity: Activity, uri: Uri) { - val playlists = PlaylistsHelper.exportPlaylists() - val playlistFile = ImportPlaylistFile("Piped", 1, playlists) + suspend fun exportPlaylists(activity: Activity, uri: Uri, importFormat: ImportFormat) { + when (importFormat) { + ImportFormat.PIPED -> { + val playlists = PlaylistsHelper.exportPipedPlaylists() + val playlistFile = PipedImportPlaylistFile("Piped", 1, playlists) - activity.contentResolver.openOutputStream(uri)?.use { - JsonHelper.json.encodeToStream(playlistFile, it) + activity.contentResolver.openOutputStream(uri)?.use { + JsonHelper.json.encodeToStream(playlistFile, it) + } + activity.toastFromMainDispatcher(R.string.exportsuccess) + } + ImportFormat.FREETUBE -> { + val playlists = PlaylistsHelper.exportFreeTubePlaylists() + + activity.contentResolver.openOutputStream(uri)?.use { + JsonHelper.json.encodeToStream(playlists, it) + } + activity.toastFromMainDispatcher(R.string.exportsuccess) + } + else -> Unit } - - activity.toastFromMainDispatcher(R.string.exportsuccess) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/obj/FreeTubeImportPlaylist.kt b/app/src/main/java/com/github/libretube/obj/FreeTubeImportPlaylist.kt new file mode 100644 index 000000000..546e3c6f1 --- /dev/null +++ b/app/src/main/java/com/github/libretube/obj/FreeTubeImportPlaylist.kt @@ -0,0 +1,11 @@ +package com.github.libretube.obj + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class FreeTubeImportPlaylist( + @SerialName("playlistName") val name: String = "", + // if type is `video` -> https://www.youtube.com/watch?v=IT734HriiHQ, works with shorts too + var videos: List = listOf(), +) diff --git a/app/src/main/java/com/github/libretube/obj/FreeTubeVideo.kt b/app/src/main/java/com/github/libretube/obj/FreeTubeVideo.kt new file mode 100644 index 000000000..5cb71eeee --- /dev/null +++ b/app/src/main/java/com/github/libretube/obj/FreeTubeVideo.kt @@ -0,0 +1,11 @@ +package com.github.libretube.obj + +import kotlinx.serialization.Serializable + +@Serializable +data class FreeTubeVideo( + val videoId: String, + val title: String, + val author: String, + val authorId: String, +) diff --git a/app/src/main/java/com/github/libretube/obj/ImportPlaylist.kt b/app/src/main/java/com/github/libretube/obj/PipedImportPlaylist.kt similarity index 87% rename from app/src/main/java/com/github/libretube/obj/ImportPlaylist.kt rename to app/src/main/java/com/github/libretube/obj/PipedImportPlaylist.kt index 97435bc7d..0b3a4801f 100644 --- a/app/src/main/java/com/github/libretube/obj/ImportPlaylist.kt +++ b/app/src/main/java/com/github/libretube/obj/PipedImportPlaylist.kt @@ -3,7 +3,7 @@ package com.github.libretube.obj import kotlinx.serialization.Serializable @Serializable -data class ImportPlaylist( +data class PipedImportPlaylist( var name: String? = null, val type: String? = null, val visibility: String? = null, diff --git a/app/src/main/java/com/github/libretube/obj/ImportPlaylistFile.kt b/app/src/main/java/com/github/libretube/obj/PipedImportPlaylistFile.kt similarity index 59% rename from app/src/main/java/com/github/libretube/obj/ImportPlaylistFile.kt rename to app/src/main/java/com/github/libretube/obj/PipedImportPlaylistFile.kt index c217403ef..1a25a211a 100644 --- a/app/src/main/java/com/github/libretube/obj/ImportPlaylistFile.kt +++ b/app/src/main/java/com/github/libretube/obj/PipedImportPlaylistFile.kt @@ -3,8 +3,8 @@ package com.github.libretube.obj import kotlinx.serialization.Serializable @Serializable -data class ImportPlaylistFile( +data class PipedImportPlaylistFile( val format: String, val version: Int, - val playlists: List = emptyList(), + val playlists: List = emptyList(), ) diff --git a/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt index 1a687326e..048a78841 100644 --- a/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt +++ b/app/src/main/java/com/github/libretube/ui/preferences/BackupRestoreSettings.kt @@ -24,15 +24,24 @@ class BackupRestoreSettings : BasePreferenceFragment() { private val backupDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss") private var backupFile = BackupFile() private var importFormat: ImportFormat = ImportFormat.NEWPIPE - private val importFormatList get() = listOf( + private val importSubscriptionFormatList get() = listOf( ImportFormat.NEWPIPE, ImportFormat.FREETUBE, ImportFormat.YOUTUBECSV - ).map { getString(it.value) } - private val exportFormatList get() = listOf( + ) + private val exportSubscriptionFormatList get() = listOf( ImportFormat.NEWPIPE, ImportFormat.FREETUBE - ).map { getString(it.value) } + ) + private val importPlaylistFormatList get() = listOf( + ImportFormat.PIPED, + ImportFormat.FREETUBE, + ImportFormat.YOUTUBECSV + ) + private val exportPlaylistFormatList get() = listOf( + ImportFormat.PIPED, + ImportFormat.FREETUBE + ) override val titleResourceId: Int = R.string.backup_restore @@ -80,14 +89,14 @@ class BackupRestoreSettings : BasePreferenceFragment() { registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { it?.forEach { CoroutineScope(Dispatchers.IO).launch { - ImportHelper.importPlaylists(requireActivity(), it) + ImportHelper.importPlaylists(requireActivity(), it, importFormat) } } } private val createPlaylistsFile = registerForActivityResult(CreateDocument(JSON)) { it?.let { lifecycleScope.launch(Dispatchers.IO) { - ImportHelper.exportPlaylists(requireActivity(), it) + ImportHelper.exportPlaylists(requireActivity(), it, importFormat) } } } @@ -115,8 +124,9 @@ class BackupRestoreSettings : BasePreferenceFragment() { val importSubscriptions = findPreference("import_subscriptions") importSubscriptions?.setOnPreferenceClickListener { - createImportFormatDialog(R.string.import_subscriptions_from, importFormatList) { - importFormat = ImportFormat.values()[it] + val list = importSubscriptionFormatList.map { getString(it.value) } + createImportFormatDialog(R.string.import_subscriptions_from, list) { + importFormat = importSubscriptionFormatList[it] getSubscriptionsFile.launch("*/*") } true @@ -124,22 +134,31 @@ class BackupRestoreSettings : BasePreferenceFragment() { val exportSubscriptions = findPreference("export_subscriptions") exportSubscriptions?.setOnPreferenceClickListener { - createImportFormatDialog(R.string.export_subscriptions_to, exportFormatList) { - importFormat = ImportFormat.values()[it] - createSubscriptionsFile.launch("subscriptions.json") + val list = exportSubscriptionFormatList.map { getString(it.value) } + createImportFormatDialog(R.string.export_subscriptions_to, list) { + importFormat = exportSubscriptionFormatList[it] + createSubscriptionsFile.launch("${getString(importFormat.value).lowercase()}-subscriptions.json") } true } val importPlaylists = findPreference("import_playlists") importPlaylists?.setOnPreferenceClickListener { - getPlaylistsFile.launch(arrayOf("*/*")) + val list = importPlaylistFormatList.map { getString(it.value) } + createImportFormatDialog(R.string.import_playlists_from, list) { + importFormat = importPlaylistFormatList[it] + getPlaylistsFile.launch(arrayOf("*/*")) + } true } val exportPlaylists = findPreference("export_playlists") exportPlaylists?.setOnPreferenceClickListener { - createPlaylistsFile.launch("playlists.json") + val list = exportPlaylistFormatList.map { getString(it.value) } + createImportFormatDialog(R.string.export_playlists_to, list) { + importFormat = exportPlaylistFormatList[it] + createPlaylistsFile.launch("${getString(importFormat.value).lowercase()}-playlists.json") + } true } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 29b46b036..748f35fe9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -416,6 +416,9 @@ Import subscriptions from Export subscriptions to + Import playlists from + Export playlists to + Piped / LibreTube NewPipe FreeTube YouTube (CSV)