diff --git a/app/src/main/java/com/github/libretube/db/obj/LocalSubscription.kt b/app/src/main/java/com/github/libretube/db/obj/LocalSubscription.kt index 3fe2a1abb..cb7056b8c 100644 --- a/app/src/main/java/com/github/libretube/db/obj/LocalSubscription.kt +++ b/app/src/main/java/com/github/libretube/db/obj/LocalSubscription.kt @@ -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") +} diff --git a/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt b/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt index 6876c01f4..f56050cdf 100644 --- a/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt +++ b/app/src/main/java/com/github/libretube/db/obj/SubscriptionGroup.kt @@ -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 = listOf(), var index: Int = 0 ) diff --git a/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt b/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt index fb6b2c492..eebf013a8 100644 --- a/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt @@ -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?) { if (preferences == null) return + PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true) { // clear the previous settings clear() 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 2087d8266..cd1268921 100644 --- a/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/ImportHelper.kt @@ -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(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(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(it) + JsonHelper.json.decodeFromStream(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(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) - } - } } diff --git a/app/src/main/java/com/github/libretube/obj/BackupFile.kt b/app/src/main/java/com/github/libretube/obj/BackupFile.kt index 503a3f318..90e3be598 100644 --- a/app/src/main/java/com/github/libretube/obj/BackupFile.kt +++ b/app/src/main/java/com/github/libretube/obj/BackupFile.kt @@ -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? = emptyList(), var watchPositions: List? = emptyList(), var searchHistory: List? = emptyList(), - var localSubscriptions: List? = emptyList(), var customInstances: List? = emptyList(), var playlistBookmarks: List? = emptyList(), - var localPlaylists: List? = emptyList(), + + // + // Preferences, stored as a key value map + // var preferences: List? = emptyList(), - var channelGroups: List? = emptyList() + + // + // Database objects with compatibility for Piped imports/exports + // + @JsonNames("groups", "channelGroups") + var groups: List? = emptyList(), + + @JsonNames("subscriptions", "localSubscriptions") + var subscriptions: List? = emptyList(), + + // playlists are exported in two different formats because the formats differ too much unfortunately + var localPlaylists: List? = emptyList(), + var playlists: List? = emptyList(), ) diff --git a/app/src/main/java/com/github/libretube/obj/FreetubeSubscription.kt b/app/src/main/java/com/github/libretube/obj/FreetubeSubscription.kt index 50e71b7c8..34bb175e9 100644 --- a/app/src/main/java/com/github/libretube/obj/FreetubeSubscription.kt +++ b/app/src/main/java/com/github/libretube/obj/FreetubeSubscription.kt @@ -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" ) diff --git a/app/src/main/java/com/github/libretube/obj/PipedBackupFile.kt b/app/src/main/java/com/github/libretube/obj/PipedBackupFile.kt deleted file mode 100644 index 9a30d5d23..000000000 --- a/app/src/main/java/com/github/libretube/obj/PipedBackupFile.kt +++ /dev/null @@ -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 = emptyList(), - val groups: List = emptyList() -) diff --git a/app/src/main/java/com/github/libretube/obj/PipedChannelGroup.kt b/app/src/main/java/com/github/libretube/obj/PipedChannelGroup.kt deleted file mode 100644 index 85404183a..000000000 --- a/app/src/main/java/com/github/libretube/obj/PipedChannelGroup.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.libretube.obj - -import kotlinx.serialization.Serializable - -@Serializable -data class PipedChannelGroup( - val groupName: String, - val channels: List -) diff --git a/app/src/main/java/com/github/libretube/obj/PipedPlaylistFile.kt b/app/src/main/java/com/github/libretube/obj/PipedPlaylistFile.kt new file mode 100644 index 000000000..4f43d1003 --- /dev/null +++ b/app/src/main/java/com/github/libretube/obj/PipedPlaylistFile.kt @@ -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 = emptyList() +) diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt index 9a9036318..796d90d61 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt @@ -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 -> 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 702fa4678..c931f1d55 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 @@ -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, @@ -195,18 +179,6 @@ class BackupRestoreSettings : BasePreferenceFragment() { true } - val importChannelGroups = findPreference("import_groups") - importChannelGroups?.setOnPreferenceClickListener { - getChannelGroupsFile.launch(arrayOf(JSON)) - true - } - - val exportChannelGroups = findPreference("export_groups") - exportChannelGroups?.setOnPreferenceClickListener { - createChannelGroupsFile.launch("piped-channel-groups.json") - true - } - childFragmentManager.setFragmentResultListener( BACKUP_DIALOG_REQUEST_KEY, this diff --git a/app/src/main/res/xml/import_export_settings.xml b/app/src/main/res/xml/import_export_settings.xml index 83eeaa309..a0ee3759e 100644 --- a/app/src/main/res/xml/import_export_settings.xml +++ b/app/src/main/res/xml/import_export_settings.xml @@ -31,20 +31,6 @@ - - - - - - - -