mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 14:20:30 +05:30
support freetube playlists import/export (#3821)
* support freetube playlists import/export --------- Co-authored-by: karen <karen@host.com>
This commit is contained in:
parent
d4ed656587
commit
eef9437326
@ -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<ImportPlaylist>) = withContext(Dispatchers.IO) {
|
||||
suspend fun importPlaylists(playlists: List<PipedImportPlaylist>) = withContext(Dispatchers.IO) {
|
||||
playlists.map { playlist ->
|
||||
val playlistId = createPlaylist(playlist.name!!)
|
||||
async {
|
||||
@ -167,7 +169,7 @@ object PlaylistsHelper {
|
||||
}.awaitAll()
|
||||
}
|
||||
|
||||
suspend fun exportPlaylists(): List<ImportPlaylist> = withContext(Dispatchers.IO) {
|
||||
suspend fun exportPipedPlaylists(): List<PipedImportPlaylist> = 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<FreeTubeImportPlaylist> =
|
||||
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)
|
||||
|
@ -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);
|
||||
}
|
@ -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<ImportPlaylist>()
|
||||
suspend fun importPlaylists(activity: Activity, uri: Uri, importFormat: ImportFormat) {
|
||||
val importPlaylists = mutableListOf<PipedImportPlaylist>()
|
||||
|
||||
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<PipedImportPlaylistFile>(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<List<FreeTubeImportPlaylist>>(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<ImportPlaylistFile>(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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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<FreeTubeVideo> = listOf(),
|
||||
)
|
11
app/src/main/java/com/github/libretube/obj/FreeTubeVideo.kt
Normal file
11
app/src/main/java/com/github/libretube/obj/FreeTubeVideo.kt
Normal file
@ -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,
|
||||
)
|
@ -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,
|
@ -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<ImportPlaylist> = emptyList(),
|
||||
val playlists: List<PipedImportPlaylist> = emptyList(),
|
||||
)
|
@ -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<Preference>("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<Preference>("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<Preference>("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<Preference>("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
|
||||
}
|
||||
|
||||
|
@ -416,6 +416,9 @@
|
||||
<!-- Backup & Restore Settings -->
|
||||
<string name="import_subscriptions_from">Import subscriptions from</string>
|
||||
<string name="export_subscriptions_to">Export subscriptions to</string>
|
||||
<string name="import_playlists_from">Import playlists from</string>
|
||||
<string name="export_playlists_to">Export playlists to</string>
|
||||
<string name="import_format_piped">Piped / LibreTube</string>
|
||||
<string name="import_format_newpipe">NewPipe</string>
|
||||
<string name="import_format_freetube">FreeTube</string>
|
||||
<string name="import_format_youtube_csv">YouTube (CSV)</string>
|
||||
|
Loading…
Reference in New Issue
Block a user