Skip to content

Commit

Permalink
feat: support for exporting single playlists
Browse files Browse the repository at this point in the history
  • Loading branch information
Bnyro committed Dec 5, 2024
1 parent b062c61 commit fa493db
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 74 deletions.
13 changes: 13 additions & 0 deletions app/src/main/java/com/github/libretube/enums/ImportFormat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.libretube.enums

import androidx.annotation.StringRes
import com.github.libretube.R

enum class ImportFormat(@StringRes val value: Int, val fileExtension: String) {
NEWPIPE(R.string.import_format_newpipe, "json"),
FREETUBE(R.string.import_format_freetube, "json"),
YOUTUBECSV(R.string.import_format_youtube_csv, "csv"),
YOUTUBEJSON(R.string.youtube, "json"),
PIPED(R.string.import_format_piped, "json"),
URLSORIDS(R.string.import_format_list_of_urls, "txt")
}
13 changes: 0 additions & 13 deletions app/src/main/java/com/github/libretube/enums/SupportedClient.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.view.View
import android.view.ViewTreeObserver
import android.widget.ScrollView
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.ColorInt
import androidx.appcompat.widget.SearchView
Expand All @@ -36,8 +37,10 @@ import com.github.libretube.compat.PictureInPictureCompat
import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.enums.ImportFormat
import com.github.libretube.extensions.anyChildFocused
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImportHelper
import com.github.libretube.helpers.IntentHelper
import com.github.libretube.helpers.NavBarHelper
import com.github.libretube.helpers.NavigationHelper
Expand All @@ -54,11 +57,14 @@ import com.github.libretube.ui.fragments.PlayerFragment
import com.github.libretube.ui.models.CommonPlayerViewModel
import com.github.libretube.ui.models.SearchViewModel
import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.ui.preferences.BackupRestoreSettings.Companion.FILETYPE_ANY
import com.github.libretube.ui.preferences.BackupRestoreSettings.Companion.JSON
import com.github.libretube.util.UpdateChecker
import com.google.android.material.elevation.SurfaceColors
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import kotlin.math.exp

class MainActivity : BaseActivity() {
lateinit var binding: ActivityMainBinding
Expand All @@ -75,6 +81,25 @@ class MainActivity : BaseActivity() {
private var savedSearchQuery: String? = null
private var shouldOpenSuggestions = true

// registering for activity results is only possible, this here should have been part of
// PlaylistOptionsBottomSheet instead if Android allowed us to
private var playlistExportFormat: ImportFormat = ImportFormat.NEWPIPE
private var exportPlaylistId: String? = null
private val createPlaylistsFile = registerForActivityResult(
ActivityResultContracts.CreateDocument(FILETYPE_ANY)
) { uri ->
if (uri == null) return@registerForActivityResult

lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.exportPlaylists(
this@MainActivity,
uri,
playlistExportFormat,
selectedPlaylistIds = listOf(exportPlaylistId!!)
)
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -652,4 +677,10 @@ class MainActivity : BaseActivity() {
?.let(action)
?: false
}

fun startPlaylistExport(playlistId: String, playlistName: String, format: ImportFormat) {
playlistExportFormat = format
exportPlaylistId = playlistId
createPlaylistsFile.launch("${playlistName}.${format.fileExtension}")
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.libretube.ui.preferences

import android.content.Context
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
Expand Down Expand Up @@ -29,26 +30,6 @@ 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 importSubscriptionFormatList = listOf(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV
)
private val exportSubscriptionFormatList = listOf(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE
)
private val importPlaylistFormatList = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV,
ImportFormat.URLSORIDS
)
private val exportPlaylistFormatList = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE
)
private val importWatchHistoryFormatList = listOf(ImportFormat.YOUTUBEJSON)

override val titleResourceId: Int = R.string.backup_restore

Expand All @@ -66,7 +47,7 @@ class BackupRestoreSettings : BasePreferenceFragment() {
}
}
}
private val createBackupFile = registerForActivityResult(CreateDocument(JSON)) { uri ->
private val createBackupFile = registerForActivityResult(CreateDocument(FILETYPE_ANY)) { uri ->
if (uri == null) return@registerForActivityResult
CoroutineScope(Dispatchers.IO).launch {
BackupHelper.createAdvancedBackup(requireContext(), uri, backupFile)
Expand All @@ -85,7 +66,7 @@ class BackupRestoreSettings : BasePreferenceFragment() {
}
}

private val createSubscriptionsFile = registerForActivityResult(CreateDocument(JSON)) { uri ->
private val createSubscriptionsFile = registerForActivityResult(CreateDocument(FILETYPE_ANY)) { uri ->
if (uri == null) return@registerForActivityResult
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.exportSubscriptions(requireActivity(), uri, importFormat)
Expand All @@ -112,84 +93,61 @@ class BackupRestoreSettings : BasePreferenceFragment() {
}
}

private val createPlaylistsFile = registerForActivityResult(CreateDocument(JSON)) { uri ->
private val createPlaylistsFile = registerForActivityResult(CreateDocument(FILETYPE_ANY)) { uri ->
uri?.let {
lifecycleScope.launch(Dispatchers.IO) {
ImportHelper.exportPlaylists(requireActivity(), uri, importFormat)
}
}
}

private fun createImportFormatDialog(
@StringRes titleStringId: Int,
items: List<String>,
onConfirm: (Int) -> Unit
) {
var selectedIndex = 0
MaterialAlertDialogBuilder(this.requireContext())
.setTitle(getString(titleStringId))
.setSingleChoiceItems(items.toTypedArray(), selectedIndex) { _, i ->
selectedIndex = i
}
.setPositiveButton(
R.string.okay
) { _, _ -> onConfirm(selectedIndex) }
.setNegativeButton(R.string.cancel, null)
.show()
}

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.import_export_settings, rootKey)

val importSubscriptions = findPreference<Preference>("import_subscriptions")
importSubscriptions?.setOnPreferenceClickListener {
val list = importSubscriptionFormatList.map { getString(it.value) }
createImportFormatDialog(R.string.import_subscriptions_from, list) {
importFormat = importSubscriptionFormatList[it]
createImportFormatDialog(requireContext(), R.string.import_subscriptions_from, importSubscriptionFormatList) {
importFormat = it
getSubscriptionsFile.launch("*/*")
}
true
}

val exportSubscriptions = findPreference<Preference>("export_subscriptions")
exportSubscriptions?.setOnPreferenceClickListener {
val list = exportSubscriptionFormatList.map { getString(it.value) }
createImportFormatDialog(R.string.export_subscriptions_to, list) {
importFormat = exportSubscriptionFormatList[it]
createImportFormatDialog(requireContext(), R.string.export_subscriptions_to, exportSubscriptionFormatList) {
importFormat = it
createSubscriptionsFile.launch(
"${getString(importFormat.value).lowercase()}-subscriptions.json"
"${getString(importFormat.value).lowercase()}-subscriptions.${importFormat.fileExtension}"
)
}
true
}

val importPlaylists = findPreference<Preference>("import_playlists")
importPlaylists?.setOnPreferenceClickListener {
val list = importPlaylistFormatList.map { getString(it.value) }
createImportFormatDialog(R.string.import_playlists_from, list) {
importFormat = importPlaylistFormatList[it]
createImportFormatDialog(requireContext(), R.string.import_playlists_from, importPlaylistFormatList) {
importFormat = it
getPlaylistsFile.launch(arrayOf("*/*"))
}
true
}

val exportPlaylists = findPreference<Preference>("export_playlists")
exportPlaylists?.setOnPreferenceClickListener {
val list = exportPlaylistFormatList.map { getString(it.value) }
createImportFormatDialog(R.string.export_playlists_to, list) {
importFormat = exportPlaylistFormatList[it]
createImportFormatDialog(requireContext(), R.string.export_playlists_to, exportPlaylistFormatList) {
importFormat = it
createPlaylistsFile.launch(
"${getString(importFormat.value).lowercase()}-playlists.json"
"${getString(importFormat.value).lowercase()}-playlists.${importFormat.fileExtension}"
)
}
true
}

val importWatchHistory = findPreference<Preference>("import_watch_history")
importWatchHistory?.setOnPreferenceClickListener {
val list = importWatchHistoryFormatList.map { getString(it.value) }
createImportFormatDialog(R.string.import_watch_history, list) {
importFormat = importWatchHistoryFormatList[it]
createImportFormatDialog(requireContext(), R.string.import_watch_history, importWatchHistoryFormatList) {
importFormat = it
getWatchHistoryFile.launch(arrayOf("*/*"))
}
true
Expand Down Expand Up @@ -219,5 +177,50 @@ class BackupRestoreSettings : BasePreferenceFragment() {

companion object {
const val JSON = "application/json"

/**
* Mimetype to use to create new files when setting extension manually
*/
const val FILETYPE_ANY = "application/octet-stream"

val importSubscriptionFormatList = listOf(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV
)
val exportSubscriptionFormatList = listOf(
ImportFormat.NEWPIPE,
ImportFormat.FREETUBE
)
val importPlaylistFormatList = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE,
ImportFormat.YOUTUBECSV,
ImportFormat.URLSORIDS
)
val exportPlaylistFormatList = listOf(
ImportFormat.PIPED,
ImportFormat.FREETUBE
)
val importWatchHistoryFormatList = listOf(ImportFormat.YOUTUBEJSON)

fun createImportFormatDialog(
context: Context,
@StringRes titleStringId: Int,
formats: List<ImportFormat>,
onConfirm: (ImportFormat) -> Unit
) {
var selectedIndex = 0
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(titleStringId))
.setSingleChoiceItems(formats.map { context.getString(it.value) }.toTypedArray(), selectedIndex) { _, i ->
selectedIndex = i
}
.setPositiveButton(
R.string.okay
) { _, _ -> onConfirm(formats[selectedIndex]) }
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHolder
import com.github.libretube.enums.ImportFormat
import com.github.libretube.enums.PlaylistType
import com.github.libretube.enums.ShareObjectType
import com.github.libretube.extensions.serializable
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.BackgroundHelper
import com.github.libretube.helpers.ContextHelper
import com.github.libretube.helpers.DownloadHelper
import com.github.libretube.obj.ShareData
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.DeletePlaylistDialog
import com.github.libretube.ui.dialogs.PlaylistDescriptionDialog
import com.github.libretube.ui.dialogs.RenamePlaylistDialog
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.preferences.BackupRestoreSettings
import com.github.libretube.util.PlayingQueue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
Expand All @@ -30,7 +34,11 @@ class PlaylistOptionsBottomSheet : BaseBottomSheet() {
private lateinit var playlistId: String
private lateinit var playlistType: PlaylistType

private var exportFormat: ImportFormat = ImportFormat.NEWPIPE

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

arguments?.let {
playlistName = it.getString(IntentData.playlistName)!!
playlistId = it.getString(IntentData.playlistId)!!
Expand All @@ -57,6 +65,7 @@ class PlaylistOptionsBottomSheet : BaseBottomSheet() {
if (isBookmarked) R.string.remove_bookmark else R.string.add_to_bookmarks
)
} else {
optionsList.add(R.string.export_playlist)
optionsList.add(R.string.renamePlaylist)
optionsList.add(R.string.change_playlist_description)
optionsList.add(R.string.deletePlaylist)
Expand Down Expand Up @@ -139,7 +148,27 @@ class PlaylistOptionsBottomSheet : BaseBottomSheet() {
}

R.string.download -> {
DownloadHelper.startDownloadPlaylistDialog(requireContext(), mFragmentManager, playlistId, playlistName, playlistType)
DownloadHelper.startDownloadPlaylistDialog(
requireContext(),
mFragmentManager,
playlistId,
playlistName,
playlistType
)
}

R.string.export_playlist -> {
val context = requireContext()

BackupRestoreSettings.createImportFormatDialog(
context,
R.string.export_playlist,
BackupRestoreSettings.exportPlaylistFormatList + listOf(ImportFormat.URLSORIDS)
) {
exportFormat = it
ContextHelper.unwrapActivity<MainActivity>(context)
.startPlaylistExport(playlistId, playlistName, exportFormat)
}
}

else -> {
Expand All @@ -158,7 +187,6 @@ class PlaylistOptionsBottomSheet : BaseBottomSheet() {
}
}
}
super.onCreate(savedInstanceState)
}

companion object {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@
<string name="all_caught_up_summary">You\'ve seen all new videos</string>
<string name="import_playlists">Import playlists</string>
<string name="export_playlists">Export playlists</string>
<string name="export_playlist">Export playlist</string>
<string name="import_watch_history">Import watch history</string>
<string name="import_watch_history_desc">Please note that not everything will be imported due to YouTube\'s limited export data.</string>
<string name="app_backup">App Backup</string>
Expand Down Expand Up @@ -480,7 +481,7 @@
<string name="import_format_freetube" translatable="false">FreeTube</string>
<string name="import_format_youtube_csv" translatable="false">YouTube (CSV)</string>
<string name="import_format_youtube_json" translatable="false">YouTube (JSON)</string>
<string name="import_format_list_of_urls">List of URls or video IDs</string>
<string name="import_format_list_of_urls">List of URLs or video IDs</string>
<string name="home_tab_content">Home tab content</string>
<string name="show_search_suggestions">Show search suggestions</string>
<string name="audio_track_format">%1$s - %2$s</string>
Expand Down

0 comments on commit fa493db

Please sign in to comment.