Skip to content

Commit

Permalink
Merge pull request #5667 from Bnyro/master
Browse files Browse the repository at this point in the history
feat: make LibreTube app backups import-compatible with Piped
  • Loading branch information
Bnyro authored Feb 27, 2024
2 parents 81623c1 + aaa1b4d commit 19bc802
Show file tree
Hide file tree
Showing 12 changed files with 78 additions and 107 deletions.
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
41 changes: 8 additions & 33 deletions app/src/main/java/com/github/libretube/helpers/ImportHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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/", "")
}
}

Expand All @@ -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/", "")
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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())

Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
}
32 changes: 29 additions & 3 deletions app/src/main/java/com/github/libretube/obj/BackupFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
Original file line number Diff line number Diff line change
@@ -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"
)
11 changes: 0 additions & 11 deletions app/src/main/java/com/github/libretube/obj/PipedBackupFile.kt

This file was deleted.

This file was deleted.

10 changes: 10 additions & 0 deletions app/src/main/java/com/github/libretube/obj/PipedPlaylistFile.kt
Original file line number Diff line number Diff line change
@@ -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()
)
12 changes: 10 additions & 2 deletions app/src/main/java/com/github/libretube/ui/dialogs/BackupDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand All @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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
Expand Down
14 changes: 0 additions & 14 deletions app/src/main/res/xml/import_export_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 19bc802

Please sign in to comment.