mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-01-06 01:20:29 +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.extensions.toID
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
import com.github.libretube.helpers.ProxyHelper
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
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 ->
|
playlists.map { playlist ->
|
||||||
val playlistId = createPlaylist(playlist.name!!)
|
val playlistId = createPlaylist(playlist.name!!)
|
||||||
async {
|
async {
|
||||||
@ -167,7 +169,7 @@ object PlaylistsHelper {
|
|||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun exportPlaylists(): List<ImportPlaylist> = withContext(Dispatchers.IO) {
|
suspend fun exportPipedPlaylists(): List<PipedImportPlaylist> = withContext(Dispatchers.IO) {
|
||||||
getPlaylists()
|
getPlaylists()
|
||||||
.map { async { getPlaylist(it.id!!) } }
|
.map { async { getPlaylist(it.id!!) } }
|
||||||
.awaitAll()
|
.awaitAll()
|
||||||
@ -175,10 +177,25 @@ object PlaylistsHelper {
|
|||||||
val videos = it.relatedStreams.map { item ->
|
val videos = it.relatedStreams.map { item ->
|
||||||
"$YOUTUBE_FRONTEND_URL/watch?v=${item.url!!.toID()}"
|
"$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? {
|
suspend fun clonePlaylist(playlistId: String): String? {
|
||||||
if (!loggedIn) {
|
if (!loggedIn) {
|
||||||
val playlist = RetrofitInstance.api.getPlaylist(playlistId)
|
val playlist = RetrofitInstance.api.getPlaylist(playlistId)
|
||||||
|
@ -6,5 +6,6 @@ import com.github.libretube.R
|
|||||||
enum class ImportFormat(@StringRes val value: Int) {
|
enum class ImportFormat(@StringRes val value: Int) {
|
||||||
NEWPIPE(R.string.import_format_newpipe),
|
NEWPIPE(R.string.import_format_newpipe),
|
||||||
FREETUBE(R.string.import_format_freetube),
|
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.enums.ImportFormat
|
||||||
import com.github.libretube.extensions.TAG
|
import com.github.libretube.extensions.TAG
|
||||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||||
|
import com.github.libretube.obj.FreeTubeImportPlaylist
|
||||||
import com.github.libretube.obj.FreetubeSubscription
|
import com.github.libretube.obj.FreetubeSubscription
|
||||||
import com.github.libretube.obj.FreetubeSubscriptions
|
import com.github.libretube.obj.FreetubeSubscriptions
|
||||||
import com.github.libretube.obj.ImportPlaylist
|
import com.github.libretube.obj.PipedImportPlaylist
|
||||||
import com.github.libretube.obj.ImportPlaylistFile
|
import com.github.libretube.obj.PipedImportPlaylistFile
|
||||||
import com.github.libretube.obj.NewPipeSubscription
|
import com.github.libretube.obj.NewPipeSubscription
|
||||||
import com.github.libretube.obj.NewPipeSubscriptions
|
import com.github.libretube.obj.NewPipeSubscriptions
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
@ -76,6 +77,7 @@ object ImportHelper {
|
|||||||
}
|
}
|
||||||
}.orEmpty()
|
}.orEmpty()
|
||||||
}
|
}
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,12 +125,38 @@ object ImportHelper {
|
|||||||
* Import Playlists
|
* Import Playlists
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
suspend fun importPlaylists(activity: Activity, uri: Uri) {
|
suspend fun importPlaylists(activity: Activity, uri: Uri, importFormat: ImportFormat) {
|
||||||
val importPlaylists = mutableListOf<ImportPlaylist>()
|
val importPlaylists = mutableListOf<PipedImportPlaylist>()
|
||||||
|
|
||||||
when (val fileType = activity.contentResolver.getType(uri)) {
|
when (importFormat) {
|
||||||
"text/csv", "text/comma-separated-values" -> {
|
ImportFormat.PIPED -> {
|
||||||
val playlist = ImportPlaylist()
|
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 {
|
activity.contentResolver.openInputStream(uri)?.use {
|
||||||
val lines = it.bufferedReader().use { reader -> reader.lines().toList() }
|
val lines = it.bufferedReader().use { reader -> reader.lines().toList() }
|
||||||
playlist.name = lines[1].split(",").reversed()[2]
|
playlist.name = lines[1].split(",").reversed()[2]
|
||||||
@ -144,23 +172,13 @@ object ImportHelper {
|
|||||||
}
|
}
|
||||||
importPlaylists.add(playlist)
|
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
|
// convert the YouTube URLs to videoIds
|
||||||
importPlaylists.forEach { playlist ->
|
importPlaylists.forEach { importPlaylist ->
|
||||||
playlist.videos = playlist.videos.map { it.takeLast(11) }
|
importPlaylist.videos = importPlaylist.videos.map { it.takeLast(11) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
PlaylistsHelper.importPlaylists(importPlaylists)
|
PlaylistsHelper.importPlaylists(importPlaylists)
|
||||||
@ -177,14 +195,26 @@ object ImportHelper {
|
|||||||
* Export Playlists
|
* Export Playlists
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
suspend fun exportPlaylists(activity: Activity, uri: Uri) {
|
suspend fun exportPlaylists(activity: Activity, uri: Uri, importFormat: ImportFormat) {
|
||||||
val playlists = PlaylistsHelper.exportPlaylists()
|
when (importFormat) {
|
||||||
val playlistFile = ImportPlaylistFile("Piped", 1, playlists)
|
ImportFormat.PIPED -> {
|
||||||
|
val playlists = PlaylistsHelper.exportPipedPlaylists()
|
||||||
|
val playlistFile = PipedImportPlaylistFile("Piped", 1, playlists)
|
||||||
|
|
||||||
activity.contentResolver.openOutputStream(uri)?.use {
|
activity.contentResolver.openOutputStream(uri)?.use {
|
||||||
JsonHelper.json.encodeToStream(playlistFile, it)
|
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
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ImportPlaylist(
|
data class PipedImportPlaylist(
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
val type: String? = null,
|
val type: String? = null,
|
||||||
val visibility: String? = null,
|
val visibility: String? = null,
|
@ -3,8 +3,8 @@ package com.github.libretube.obj
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ImportPlaylistFile(
|
data class PipedImportPlaylistFile(
|
||||||
val format: String,
|
val format: String,
|
||||||
val version: Int,
|
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 val backupDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss")
|
||||||
private var backupFile = BackupFile()
|
private var backupFile = BackupFile()
|
||||||
private var importFormat: ImportFormat = ImportFormat.NEWPIPE
|
private var importFormat: ImportFormat = ImportFormat.NEWPIPE
|
||||||
private val importFormatList get() = listOf(
|
private val importSubscriptionFormatList get() = listOf(
|
||||||
ImportFormat.NEWPIPE,
|
ImportFormat.NEWPIPE,
|
||||||
ImportFormat.FREETUBE,
|
ImportFormat.FREETUBE,
|
||||||
ImportFormat.YOUTUBECSV
|
ImportFormat.YOUTUBECSV
|
||||||
).map { getString(it.value) }
|
)
|
||||||
private val exportFormatList get() = listOf(
|
private val exportSubscriptionFormatList get() = listOf(
|
||||||
ImportFormat.NEWPIPE,
|
ImportFormat.NEWPIPE,
|
||||||
ImportFormat.FREETUBE
|
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
|
override val titleResourceId: Int = R.string.backup_restore
|
||||||
|
|
||||||
@ -80,14 +89,14 @@ class BackupRestoreSettings : BasePreferenceFragment() {
|
|||||||
registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) {
|
registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) {
|
||||||
it?.forEach {
|
it?.forEach {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
ImportHelper.importPlaylists(requireActivity(), it)
|
ImportHelper.importPlaylists(requireActivity(), it, importFormat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val createPlaylistsFile = registerForActivityResult(CreateDocument(JSON)) {
|
private val createPlaylistsFile = registerForActivityResult(CreateDocument(JSON)) {
|
||||||
it?.let {
|
it?.let {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
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")
|
val importSubscriptions = findPreference<Preference>("import_subscriptions")
|
||||||
importSubscriptions?.setOnPreferenceClickListener {
|
importSubscriptions?.setOnPreferenceClickListener {
|
||||||
createImportFormatDialog(R.string.import_subscriptions_from, importFormatList) {
|
val list = importSubscriptionFormatList.map { getString(it.value) }
|
||||||
importFormat = ImportFormat.values()[it]
|
createImportFormatDialog(R.string.import_subscriptions_from, list) {
|
||||||
|
importFormat = importSubscriptionFormatList[it]
|
||||||
getSubscriptionsFile.launch("*/*")
|
getSubscriptionsFile.launch("*/*")
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@ -124,22 +134,31 @@ class BackupRestoreSettings : BasePreferenceFragment() {
|
|||||||
|
|
||||||
val exportSubscriptions = findPreference<Preference>("export_subscriptions")
|
val exportSubscriptions = findPreference<Preference>("export_subscriptions")
|
||||||
exportSubscriptions?.setOnPreferenceClickListener {
|
exportSubscriptions?.setOnPreferenceClickListener {
|
||||||
createImportFormatDialog(R.string.export_subscriptions_to, exportFormatList) {
|
val list = exportSubscriptionFormatList.map { getString(it.value) }
|
||||||
importFormat = ImportFormat.values()[it]
|
createImportFormatDialog(R.string.export_subscriptions_to, list) {
|
||||||
createSubscriptionsFile.launch("subscriptions.json")
|
importFormat = exportSubscriptionFormatList[it]
|
||||||
|
createSubscriptionsFile.launch("${getString(importFormat.value).lowercase()}-subscriptions.json")
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
val importPlaylists = findPreference<Preference>("import_playlists")
|
val importPlaylists = findPreference<Preference>("import_playlists")
|
||||||
importPlaylists?.setOnPreferenceClickListener {
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
val exportPlaylists = findPreference<Preference>("export_playlists")
|
val exportPlaylists = findPreference<Preference>("export_playlists")
|
||||||
exportPlaylists?.setOnPreferenceClickListener {
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,6 +416,9 @@
|
|||||||
<!-- Backup & Restore Settings -->
|
<!-- Backup & Restore Settings -->
|
||||||
<string name="import_subscriptions_from">Import subscriptions from</string>
|
<string name="import_subscriptions_from">Import subscriptions from</string>
|
||||||
<string name="export_subscriptions_to">Export subscriptions to</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_newpipe">NewPipe</string>
|
||||||
<string name="import_format_freetube">FreeTube</string>
|
<string name="import_format_freetube">FreeTube</string>
|
||||||
<string name="import_format_youtube_csv">YouTube (CSV)</string>
|
<string name="import_format_youtube_csv">YouTube (CSV)</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user