Merge pull request #5667 from Bnyro/master

feat: make LibreTube app backups import-compatible with Piped
This commit is contained in:
Bnyro 2024-02-27 14:07:09 +01:00 committed by GitHub
commit 19bc8025c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 78 additions and 107 deletions

View File

@ -1,11 +1,16 @@
package com.github.libretube.db.obj
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.github.libretube.ui.dialogs.ShareDialog
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "localSubscription")
data class LocalSubscription(
@PrimaryKey val channelId: String = ""
)
@PrimaryKey val channelId: String,
@Ignore val url: String = "",
) {
constructor(channelId: String): this(channelId, "${ShareDialog.YOUTUBE_FRONTEND_URL}/channel/$channelId")
}

View File

@ -2,12 +2,19 @@ package com.github.libretube.db.obj
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable
@OptIn(ExperimentalSerializationApi::class)
@Entity(tableName = "subscriptionGroups")
data class SubscriptionGroup(
@PrimaryKey var name: String,
@PrimaryKey
@SerialName("groupName")
@JsonNames("groupName", "name")
var name: String,
var channels: List<String> = listOf(),
var index: Int = 0
)

View File

@ -49,10 +49,10 @@ object BackupHelper {
Database.watchHistoryDao().insertAll(backupFile.watchHistory.orEmpty())
Database.searchHistoryDao().insertAll(backupFile.searchHistory.orEmpty())
Database.watchPositionDao().insertAll(backupFile.watchPositions.orEmpty())
Database.localSubscriptionDao().insertAll(backupFile.localSubscriptions.orEmpty())
Database.localSubscriptionDao().insertAll(backupFile.subscriptions.orEmpty())
Database.customInstanceDao().insertAll(backupFile.customInstances.orEmpty())
Database.playlistBookmarkDao().insertAll(backupFile.playlistBookmarks.orEmpty())
Database.subscriptionGroupsDao().insertAll(backupFile.channelGroups.orEmpty())
Database.subscriptionGroupsDao().insertAll(backupFile.groups.orEmpty())
backupFile.localPlaylists?.forEach {
// the playlist will be created with an id of 0, so that Room will auto generate a
@ -72,6 +72,7 @@ object BackupHelper {
*/
private fun restorePreferences(context: Context, preferences: List<PreferenceItem>?) {
if (preferences == null) return
PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true) {
// clear the previous settings
clear()

View File

@ -9,7 +9,6 @@ import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.SubscriptionGroup
import com.github.libretube.enums.ImportFormat
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainDispatcher
@ -19,8 +18,8 @@ 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.PipedBackupFile
import com.github.libretube.obj.PipedChannelGroup
import com.github.libretube.obj.PipedPlaylistFile
import com.github.libretube.ui.dialogs.ShareDialog
import java.util.Date
import java.util.stream.Collectors
import kotlinx.serialization.ExperimentalSerializationApi
@ -63,7 +62,7 @@ object ImportHelper {
JsonHelper.json.decodeFromStream<NewPipeSubscriptions>(it)
}
subscriptions?.subscriptions.orEmpty().map {
it.url.replace("https://www.youtube.com/channel/", "")
it.url.replace("${ShareDialog.YOUTUBE_FRONTEND_URL}/channel/", "")
}
}
@ -72,7 +71,7 @@ object ImportHelper {
JsonHelper.json.decodeFromStream<FreetubeSubscriptions>(it)
}
subscriptions?.subscriptions.orEmpty().map {
it.url.replace("https://www.youtube.com/channel/", "")
it.url.replace("${ShareDialog.YOUTUBE_FRONTEND_URL}/channel/", "")
}
}
@ -107,7 +106,7 @@ object ImportHelper {
when (importFormat) {
ImportFormat.NEWPIPE -> {
val newPipeChannels = subs.map {
NewPipeSubscription(it.name, 0, "https://www.youtube.com${it.url}")
NewPipeSubscription(it.name, 0, "${ShareDialog.YOUTUBE_FRONTEND_URL}${it.url}")
}
val newPipeSubscriptions = NewPipeSubscriptions(subscriptions = newPipeChannels)
activity.contentResolver.openOutputStream(uri)?.use {
@ -117,7 +116,7 @@ object ImportHelper {
ImportFormat.FREETUBE -> {
val freeTubeChannels = subs.map {
FreetubeSubscription(it.name, "", "https://www.youtube.com${it.url}")
FreetubeSubscription(it.name, "", "${ShareDialog.YOUTUBE_FRONTEND_URL}${it.url}")
}
val freeTubeSubscriptions = FreetubeSubscriptions(subscriptions = freeTubeChannels)
activity.contentResolver.openOutputStream(uri)?.use {
@ -141,7 +140,7 @@ object ImportHelper {
when (importFormat) {
ImportFormat.PIPED -> {
val playlistFile = activity.contentResolver.openInputStream(uri)?.use {
JsonHelper.json.decodeFromStream<PipedBackupFile>(it)
JsonHelper.json.decodeFromStream<PipedPlaylistFile>(it)
}
importPlaylists.addAll(playlistFile?.playlists.orEmpty())
@ -231,7 +230,7 @@ object ImportHelper {
when (importFormat) {
ImportFormat.PIPED -> {
val playlists = PlaylistsHelper.exportPipedPlaylists()
val playlistFile = PipedBackupFile("Piped", 1, playlists = playlists)
val playlistFile = PipedPlaylistFile(playlists = playlists)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(playlistFile, it)
@ -251,28 +250,4 @@ object ImportHelper {
else -> Unit
}
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun importGroups(activity: Activity, uri: Uri) {
val pipedFile = activity.contentResolver.openInputStream(uri)?.use {
JsonHelper.json.decodeFromStream<PipedBackupFile>(it)
} ?: return
pipedFile.groups.forEach {
val group = SubscriptionGroup(it.groupName, it.channels)
Database.subscriptionGroupsDao().createGroup(group)
}
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun exportGroups(activity: Activity, uri: Uri) {
val channelGroups = Database.subscriptionGroupsDao().getAll().map {
PipedChannelGroup(it.name, it.channels)
}
val pipedFile = PipedBackupFile("Piped", 1, groups = channelGroups)
activity.contentResolver.openOutputStream(uri)?.use {
JsonHelper.json.encodeToStream(pipedFile, it)
}
}
}

View File

@ -8,17 +8,43 @@ import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.db.obj.SubscriptionGroup
import com.github.libretube.db.obj.WatchHistoryItem
import com.github.libretube.db.obj.WatchPosition
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
@Serializable
@OptIn(ExperimentalSerializationApi::class)
data class BackupFile(
//
// some stuff for compatibility with Piped imports
//
val format: String = "Piped",
val version: Int = 1,
//
// only compatible with LibreTube itself, database objects
//
var watchHistory: List<WatchHistoryItem>? = emptyList(),
var watchPositions: List<WatchPosition>? = emptyList(),
var searchHistory: List<SearchHistoryItem>? = emptyList(),
var localSubscriptions: List<LocalSubscription>? = emptyList(),
var customInstances: List<CustomInstance>? = emptyList(),
var playlistBookmarks: List<PlaylistBookmark>? = emptyList(),
var localPlaylists: List<LocalPlaylistWithVideos>? = emptyList(),
//
// Preferences, stored as a key value map
//
var preferences: List<PreferenceItem>? = emptyList(),
var channelGroups: List<SubscriptionGroup>? = emptyList()
//
// Database objects with compatibility for Piped imports/exports
//
@JsonNames("groups", "channelGroups")
var groups: List<SubscriptionGroup>? = emptyList(),
@JsonNames("subscriptions", "localSubscriptions")
var subscriptions: List<LocalSubscription>? = emptyList(),
// playlists are exported in two different formats because the formats differ too much unfortunately
var localPlaylists: List<LocalPlaylistWithVideos>? = emptyList(),
var playlists: List<PipedImportPlaylist>? = emptyList(),
)

View File

@ -1,11 +1,12 @@
package com.github.libretube.obj
import com.github.libretube.ui.dialogs.ShareDialog
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class FreetubeSubscription(
val name: String,
@SerialName("id") val serviceId: String,
val url: String = "https://www.youtube.com/channel/$serviceId"
@SerialName("id") val channelId: String,
val url: String = "${ShareDialog.YOUTUBE_FRONTEND_URL}/channel/$channelId"
)

View File

@ -1,11 +0,0 @@
package com.github.libretube.obj
import kotlinx.serialization.Serializable
@Serializable
data class PipedBackupFile(
val format: String,
val version: Int,
val playlists: List<PipedImportPlaylist> = emptyList(),
val groups: List<PipedChannelGroup> = emptyList()
)

View File

@ -1,9 +0,0 @@
package com.github.libretube.obj
import kotlinx.serialization.Serializable
@Serializable
data class PipedChannelGroup(
val groupName: String,
val channels: List<String>
)

View File

@ -0,0 +1,10 @@
package com.github.libretube.obj
import kotlinx.serialization.Serializable
@Serializable
data class PipedPlaylistFile(
val format: String = "Piped",
val version: Int = 1,
val playlists: List<PipedImportPlaylist> = emptyList()
)

View File

@ -13,7 +13,9 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.obj.PreferenceItem
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -40,7 +42,7 @@ class BackupDialog : DialogFragment() {
})
data object LocalSubscriptions : BackupOption(R.string.local_subscriptions, onSelected = {
it.localSubscriptions = Database.localSubscriptionDao().getAll()
it.subscriptions = Database.localSubscriptionDao().getAll()
})
data object CustomInstances : BackupOption(R.string.backup_customInstances, onSelected = {
@ -53,10 +55,16 @@ class BackupDialog : DialogFragment() {
data object LocalPlaylists : BackupOption(R.string.local_playlists, onSelected = {
it.localPlaylists = Database.localPlaylistsDao().getAll()
it.playlists = it.localPlaylists?.map { (playlist, playlistVideos) ->
val videos = playlistVideos.map { item ->
"${ShareDialog.YOUTUBE_FRONTEND_URL}/watch?v=${item.videoId}"
}
PipedImportPlaylist(playlist.name, "playlist", "private", videos)
}
})
data object SubscriptionGroups : BackupOption(R.string.channel_groups, onSelected = {
it.channelGroups = Database.subscriptionGroupsDao().getAll()
it.groups = Database.subscriptionGroupsDao().getAll()
})
data object Preferences : BackupOption(R.string.preferences, onSelected = { file ->

View File

@ -114,22 +114,6 @@ class BackupRestoreSettings : BasePreferenceFragment() {
}
}
private val getChannelGroupsFile = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) {
it?.forEach { uri ->
CoroutineScope(Dispatchers.IO).launch {
ImportHelper.importGroups(requireActivity(), uri)
}
}
}
private val createChannelGroupsFile = registerForActivityResult(ActivityResultContracts.CreateDocument(JSON)) {
it?.let { uri ->
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.exportGroups(requireActivity(), uri)
}
}
}
private fun createImportFormatDialog(
@StringRes titleStringId: Int,
items: List<String>,
@ -195,18 +179,6 @@ class BackupRestoreSettings : BasePreferenceFragment() {
true
}
val importChannelGroups = findPreference<Preference>("import_groups")
importChannelGroups?.setOnPreferenceClickListener {
getChannelGroupsFile.launch(arrayOf(JSON))
true
}
val exportChannelGroups = findPreference<Preference>("export_groups")
exportChannelGroups?.setOnPreferenceClickListener {
createChannelGroupsFile.launch("piped-channel-groups.json")
true
}
childFragmentManager.setFragmentResultListener(
BACKUP_DIALOG_REQUEST_KEY,
this

View File

@ -31,20 +31,6 @@
</PreferenceCategory>
<PreferenceCategory app:title="@string/channel_groups">
<Preference
android:icon="@drawable/ic_download_filled"
app:key="import_groups"
app:title="@string/import_groups" />
<Preference
android:icon="@drawable/ic_upload"
app:key="export_groups"
app:title="@string/export_groups" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/app_backup">
<Preference