Merge pull request #2160 from Bnyro/playlists-import-export

Playlists import export
This commit is contained in:
Bnyro 2022-12-01 15:18:22 +01:00 committed by GitHub
commit db739b9000
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 391 additions and 227 deletions

View File

@ -6,16 +6,21 @@ import com.github.libretube.R
import com.github.libretube.api.obj.Playlist import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.PlaylistId import com.github.libretube.api.obj.PlaylistId
import com.github.libretube.api.obj.Playlists import com.github.libretube.api.obj.Playlists
import com.github.libretube.constants.YOUTUBE_FRONTEND_URL
import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.DatabaseHolder
import com.github.libretube.db.obj.LocalPlaylist import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.enums.PlaylistType import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.awaitQuery import com.github.libretube.extensions.awaitQuery
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toLocalPlaylistItem import com.github.libretube.extensions.toLocalPlaylistItem
import com.github.libretube.extensions.toStreamItem import com.github.libretube.extensions.toStreamItem
import com.github.libretube.extensions.toastFromMainThread import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.obj.ImportPlaylist
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import com.github.libretube.util.ProxyHelper import com.github.libretube.util.ProxyHelper
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
@ -46,9 +51,9 @@ object PlaylistsHelper {
return playlists return playlists
} }
suspend fun getPlaylist(playlistType: PlaylistType, playlistId: String): Playlist { suspend fun getPlaylist(playlistId: String): Playlist {
// load locally stored playlists with the auth api // load locally stored playlists with the auth api
return when (playlistType) { return when (getPlaylistType()) {
PlaylistType.PRIVATE -> RetrofitInstance.authApi.getPlaylist(playlistId) PlaylistType.PRIVATE -> RetrofitInstance.authApi.getPlaylist(playlistId)
PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId) PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId)
PlaylistType.LOCAL -> { PlaylistType.LOCAL -> {
@ -65,7 +70,10 @@ object PlaylistsHelper {
} }
} }
suspend fun createPlaylist(playlistName: String, appContext: Context, onSuccess: () -> Unit) { suspend fun createPlaylist(
playlistName: String,
appContext: Context
): String? {
if (!loggedIn()) { if (!loggedIn()) {
awaitQuery { awaitQuery {
DatabaseHolder.Database.localPlaylistsDao().createPlaylist( DatabaseHolder.Database.localPlaylistsDao().createPlaylist(
@ -75,8 +83,9 @@ object PlaylistsHelper {
) )
) )
} }
onSuccess.invoke() return awaitQuery {
return DatabaseHolder.Database.localPlaylistsDao().getAll()
}.last().playlist.id.toString()
} }
val response = try { val response = try {
RetrofitInstance.authApi.createPlaylist( RetrofitInstance.authApi.createPlaylist(
@ -85,22 +94,25 @@ object PlaylistsHelper {
) )
} catch (e: IOException) { } catch (e: IOException) {
appContext.toastFromMainThread(R.string.unknown_error) appContext.toastFromMainThread(R.string.unknown_error)
return return null
} catch (e: HttpException) { } catch (e: HttpException) {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())
appContext.toastFromMainThread(R.string.server_error) appContext.toastFromMainThread(R.string.server_error)
return return null
} }
if (response.playlistId != null) { if (response.playlistId != null) {
appContext.toastFromMainThread(R.string.playlistCreated) appContext.toastFromMainThread(R.string.playlistCreated)
onSuccess.invoke() return response.playlistId!!
} else {
appContext.toastFromMainThread(R.string.unknown_error)
} }
return null
} }
suspend fun addToPlaylist(playlistId: String, videoId: String): Boolean { suspend fun addToPlaylist(playlistId: String, vararg videoIds: String): Boolean {
if (!loggedIn()) { if (!loggedIn()) {
val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }
for (videoId in videoIds) {
val localPlaylistItem = RetrofitInstance.api.getStreams(videoId).toLocalPlaylistItem(playlistId, videoId) val localPlaylistItem = RetrofitInstance.api.getStreams(videoId).toLocalPlaylistItem(playlistId, videoId)
awaitQuery { awaitQuery {
// avoid duplicated videos in a playlist // avoid duplicated videos in a playlist
@ -108,8 +120,6 @@ object PlaylistsHelper {
// add the new video to the database // add the new video to the database
DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem) DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem)
val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }
if (localPlaylist.playlist.thumbnailUrl == "") { if (localPlaylist.playlist.thumbnailUrl == "") {
// set the new playlist thumbnail URL // set the new playlist thumbnail URL
@ -119,12 +129,16 @@ object PlaylistsHelper {
} }
} }
} }
}
return true return true
} }
return RetrofitInstance.authApi.addToPlaylist( return RetrofitInstance.authApi.addToPlaylist(
token, token,
PlaylistId(playlistId, videoId) PlaylistId(
playlistId = playlistId,
videoIds = videoIds.toList()
)
).message == "ok" ).message == "ok"
} }
@ -175,11 +189,49 @@ object PlaylistsHelper {
) )
} }
fun getPrivateType(): PlaylistType { suspend fun importPlaylists(appContext: Context, playlists: List<ImportPlaylist>) {
for (playlist in playlists) {
val playlistId = createPlaylist(playlist.name!!, appContext) ?: continue
addToPlaylist(
playlistId,
*playlist.videos.map {
it.substringAfter("=")
}.toTypedArray()
)
}
}
suspend fun exportPlaylists(): List<ImportPlaylist> {
val playlists = getPlaylists()
val importLists = mutableListOf<ImportPlaylist>()
runBlocking {
val tasks = playlists.map {
async {
val list = getPlaylist(it.id!!)
importLists.add(
ImportPlaylist(
name = list.name,
type = "playlist",
visibility = "private",
videos = list.relatedStreams.orEmpty().map {
YOUTUBE_FRONTEND_URL + "/watch?v=" + it.url!!.toID()
}
)
)
}
}
tasks.forEach {
it.await()
}
}
return importLists
}
fun getPlaylistType(): PlaylistType {
return if (loggedIn()) PlaylistType.PRIVATE else PlaylistType.LOCAL return if (loggedIn()) PlaylistType.PRIVATE else PlaylistType.LOCAL
} }
fun getPrivateType(playlistId: String): PlaylistType { fun getPlaylistType(playlistId: String): PlaylistType {
if (playlistId.all { it.isDigit() }) return PlaylistType.LOCAL if (playlistId.all { it.isDigit() }) return PlaylistType.LOCAL
if (playlistId.matches(pipedPlaylistRegex)) return PlaylistType.PRIVATE if (playlistId.matches(pipedPlaylistRegex)) return PlaylistType.PRIVATE
return PlaylistType.PUBLIC return PlaylistType.PUBLIC

View File

@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
data class PlaylistId( data class PlaylistId(
var playlistId: String? = null, var playlistId: String? = null,
var videoId: String? = null, var videoId: String? = null,
var videoIds: List<String>? = null,
var newName: String? = null, var newName: String? = null,
var index: Int = -1 var index: Int = -1
) )

View File

@ -49,8 +49,6 @@ object PreferenceKeys {
const val CLEAR_CUSTOM_INSTANCES = "clearCustomInstances" const val CLEAR_CUSTOM_INSTANCES = "clearCustomInstances"
const val LOGIN_REGISTER = "login_register" const val LOGIN_REGISTER = "login_register"
const val DELETE_ACCOUNT = "delete_account" const val DELETE_ACCOUNT = "delete_account"
const val IMPORT_SUBS = "import_from_yt"
const val EXPORT_SUBS = "export_subs"
/** /**
* Player * Player

View File

@ -0,0 +1,8 @@
package com.github.libretube.obj
data class ImportPlaylist(
val name: String? = null,
val type: String? = null,
val visibility: String? = null,
val videos: List<String> = listOf()
)

View File

@ -0,0 +1,7 @@
package com.github.libretube.obj
data class ImportPlaylistFile(
val format: String? = null,
val version: Int? = null,
val playlists: List<ImportPlaylist>? = null
)

View File

@ -1,6 +1,7 @@
package com.github.libretube.ui.base package com.github.libretube.ui.base
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
@ -70,4 +71,10 @@ open class BasePreferenceFragment : PreferenceFragmentCompat() {
else -> super.onDisplayPreferenceDialog(preference) else -> super.onDisplayPreferenceDialog(preference)
} }
} }
fun Fragment?.runOnUiThread(action: () -> Unit) {
this ?: return
if (!isAdded) return // Fragment not attached to an Activity
activity?.runOnUiThread(action)
}
} }

View File

@ -31,11 +31,10 @@ class CreatePlaylistDialog(
val listName = binding.playlistName.text.toString() val listName = binding.playlistName.text.toString()
if (listName != "") { if (listName != "") {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
PlaylistsHelper.createPlaylist(listName, requireContext().applicationContext) { PlaylistsHelper.createPlaylist(listName, requireContext().applicationContext)
onSuccess.invoke() onSuccess.invoke()
dismiss() dismiss()
} }
}
} else { } else {
Toast.makeText(context, R.string.emptyPlaylistName, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.emptyPlaylistName, Toast.LENGTH_LONG).show()
} }

View File

@ -108,7 +108,7 @@ class HomeFragment : BaseFragment() {
runOnUiThread { runOnUiThread {
makeVisible(binding.playlistsRV, binding.playlistsTV) makeVisible(binding.playlistsRV, binding.playlistsTV)
binding.playlistsRV.layoutManager = LinearLayoutManager(context) binding.playlistsRV.layoutManager = LinearLayoutManager(context)
binding.playlistsRV.adapter = PlaylistsAdapter(playlists.toMutableList(), PlaylistsHelper.getPrivateType()) binding.playlistsRV.adapter = PlaylistsAdapter(playlists.toMutableList(), PlaylistsHelper.getPlaylistType())
binding.playlistsRV.adapter?.registerAdapterDataObserver(object : binding.playlistsRV.adapter?.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() { RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {

View File

@ -123,7 +123,7 @@ class LibraryFragment : BaseFragment() {
val playlistsAdapter = PlaylistsAdapter( val playlistsAdapter = PlaylistsAdapter(
playlists.toMutableList(), playlists.toMutableList(),
PlaylistsHelper.getPrivateType() PlaylistsHelper.getPlaylistType()
) )
// listen for playlists to become deleted // listen for playlists to become deleted

View File

@ -99,7 +99,7 @@ class PlaylistFragment : BaseFragment() {
binding.playlistScrollview.visibility = View.GONE binding.playlistScrollview.visibility = View.GONE
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val response = try { val response = try {
PlaylistsHelper.getPlaylist(playlistType, playlistId!!) PlaylistsHelper.getPlaylist(playlistId!!)
} catch (e: IOException) { } catch (e: IOException) {
println(e) println(e)
Log.e(TAG(), "IOException, you might not have internet connection") Log.e(TAG(), "IOException, you might not have internet connection")

View File

@ -1,49 +1,18 @@
package com.github.libretube.ui.preferences package com.github.libretube.ui.preferences
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.obj.BackupFile
import com.github.libretube.ui.activities.SettingsActivity import com.github.libretube.ui.activities.SettingsActivity
import com.github.libretube.ui.base.BasePreferenceFragment import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.ui.dialogs.BackupDialog
import com.github.libretube.util.BackupHelper
import com.github.libretube.util.ImageHelper import com.github.libretube.util.ImageHelper
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.time.LocalDate
import java.time.LocalTime
class AdvancedSettings : BasePreferenceFragment() { class AdvancedSettings : BasePreferenceFragment() {
// backup and restore database
private lateinit var getBackupFile: ActivityResultLauncher<String>
private lateinit var createBackupFile: ActivityResultLauncher<String>
private var backupFile = BackupFile()
override fun onCreate(savedInstanceState: Bundle?) {
getBackupFile =
registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
BackupHelper(requireContext()).restoreAdvancedBackup(uri)
}
createBackupFile = registerForActivityResult(
CreateDocument("application/json")
) { uri: Uri? ->
BackupHelper(requireContext()).advancedBackup(uri, backupFile)
}
super.onCreate(savedInstanceState)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.advanced_settings, rootKey) setPreferencesFromResource(R.xml.advanced_settings, rootKey)
@ -61,22 +30,6 @@ class AdvancedSettings : BasePreferenceFragment() {
showResetDialog() showResetDialog()
true true
} }
val advancesBackup = findPreference<Preference>("backup")
advancesBackup?.setOnPreferenceClickListener {
BackupDialog {
backupFile = it
createBackupFile.launch(getBackupFileName())
}
.show(childFragmentManager, null)
true
}
val restoreAdvancedBackup = findPreference<Preference>("restore")
restoreAdvancedBackup?.setOnPreferenceClickListener {
getBackupFile.launch("application/json")
true
}
} }
private fun showResetDialog() { private fun showResetDialog() {
@ -95,9 +48,4 @@ class AdvancedSettings : BasePreferenceFragment() {
} }
.show() .show()
} }
private fun getBackupFileName(): String {
val time = LocalTime.now().toString().split(".").firstOrNull()
return "libretube-backup-${LocalDate.now()}-$time.json"
}
} }

View File

@ -0,0 +1,124 @@
package com.github.libretube.ui.preferences
import android.net.Uri
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.preference.Preference
import com.github.libretube.R
import com.github.libretube.obj.BackupFile
import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.ui.dialogs.BackupDialog
import com.github.libretube.util.BackupHelper
import com.github.libretube.util.ImportHelper
import java.time.LocalDate
import java.time.LocalTime
class BackupRestoreSettings : BasePreferenceFragment() {
// backup and restore database
private lateinit var getBackupFile: ActivityResultLauncher<String>
private lateinit var createBackupFile: ActivityResultLauncher<String>
private var backupFile = BackupFile()
/**
* result listeners for importing and exporting subscriptions
*/
private lateinit var getSubscriptionsFile: ActivityResultLauncher<String>
private lateinit var createSubscriptionsFile: ActivityResultLauncher<String>
/**
* result listeners for importing and exporting playlists
*/
private lateinit var getPlaylistsFile: ActivityResultLauncher<String>
private lateinit var createPlaylistsFile: ActivityResultLauncher<String>
override fun onCreate(savedInstanceState: Bundle?) {
getSubscriptionsFile =
registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
ImportHelper(requireActivity()).importSubscriptions(uri)
}
createSubscriptionsFile = registerForActivityResult(
CreateDocument("application/json")
) { uri ->
ImportHelper(requireActivity()).exportSubscriptions(uri)
}
getPlaylistsFile = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
ImportHelper(requireActivity()).importPlaylists(uri)
}
createPlaylistsFile = registerForActivityResult(
CreateDocument("application/json")
) { uri ->
ImportHelper(requireActivity()).exportPlaylists(uri)
}
getBackupFile =
registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
BackupHelper(requireContext()).restoreAdvancedBackup(uri)
}
createBackupFile = registerForActivityResult(
CreateDocument("application/json")
) { uri: Uri? ->
BackupHelper(requireContext()).advancedBackup(uri, backupFile)
}
super.onCreate(savedInstanceState)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.import_export_settings, rootKey)
val importSubscriptions = findPreference<Preference>("import_subscriptions")
importSubscriptions?.setOnPreferenceClickListener {
getSubscriptionsFile.launch("*/*")
true
}
val exportSubscriptions = findPreference<Preference>("export_subscriptions")
exportSubscriptions?.setOnPreferenceClickListener {
createSubscriptionsFile.launch("subscriptions.json")
true
}
val importPlaylists = findPreference<Preference>("import_playlists")
importPlaylists?.setOnPreferenceClickListener {
getPlaylistsFile.launch("*/*")
true
}
val exportPlaylists = findPreference<Preference>("export_playlists")
exportPlaylists?.setOnPreferenceClickListener {
createPlaylistsFile.launch("playlists.json")
true
}
val advancesBackup = findPreference<Preference>("backup")
advancesBackup?.setOnPreferenceClickListener {
BackupDialog {
backupFile = it
createBackupFile.launch(getBackupFileName())
}
.show(childFragmentManager, null)
true
}
val restoreAdvancedBackup = findPreference<Preference>("restore")
restoreAdvancedBackup?.setOnPreferenceClickListener {
getBackupFile.launch("application/json")
true
}
}
private fun getBackupFileName(): String {
val time = LocalTime.now().toString().split(".").firstOrNull()
return "libretube-backup-${LocalDate.now()}-$time.json"
}
}

View File

@ -1,12 +1,7 @@
package com.github.libretube.ui.preferences package com.github.libretube.ui.preferences
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
@ -22,33 +17,10 @@ import com.github.libretube.ui.dialogs.CustomInstanceDialog
import com.github.libretube.ui.dialogs.DeleteAccountDialog import com.github.libretube.ui.dialogs.DeleteAccountDialog
import com.github.libretube.ui.dialogs.LoginDialog import com.github.libretube.ui.dialogs.LoginDialog
import com.github.libretube.ui.dialogs.LogoutDialog import com.github.libretube.ui.dialogs.LogoutDialog
import com.github.libretube.util.ImportHelper
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
class InstanceSettings : BasePreferenceFragment() { class InstanceSettings : BasePreferenceFragment() {
/**
* result listeners for importing and exporting subscriptions
*/
private lateinit var getContent: ActivityResultLauncher<String>
private lateinit var createFile: ActivityResultLauncher<String>
override fun onCreate(savedInstanceState: Bundle?) {
getContent =
registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri: Uri? ->
ImportHelper(requireActivity()).importSubscriptions(uri)
}
createFile = registerForActivityResult(
CreateDocument("application/json")
) { uri: Uri? ->
ImportHelper(requireActivity()).exportSubscriptions(uri)
}
super.onCreate(savedInstanceState)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.instance_settings, rootKey) setPreferencesFromResource(R.xml.instance_settings, rootKey)
@ -138,19 +110,6 @@ class InstanceSettings : BasePreferenceFragment() {
newFragment.show(childFragmentManager, DeleteAccountDialog::class.java.name) newFragment.show(childFragmentManager, DeleteAccountDialog::class.java.name)
true true
} }
val importSubscriptions = findPreference<Preference>(PreferenceKeys.IMPORT_SUBS)
importSubscriptions?.setOnPreferenceClickListener {
// check StorageAccess
getContent.launch("*/*")
true
}
val exportSubscriptions = findPreference<Preference>(PreferenceKeys.EXPORT_SUBS)
exportSubscriptions?.setOnPreferenceClickListener {
createFile.launch("subscriptions.json")
true
}
} }
private fun initCustomInstances(instancePref: ListPreference) { private fun initCustomInstances(instancePref: ListPreference) {
@ -201,10 +160,4 @@ class InstanceSettings : BasePreferenceFragment() {
PreferenceHelper.setToken("") PreferenceHelper.setToken("")
Toast.makeText(context, getString(R.string.loggedout), Toast.LENGTH_SHORT).show() Toast.makeText(context, getString(R.string.loggedout), Toast.LENGTH_SHORT).show()
} }
private fun Fragment?.runOnUiThread(action: () -> Unit) {
this ?: return
if (!isAdded) return // Fragment not attached to an Activity
activity?.runOnUiThread(action)
}
} }

View File

@ -23,65 +23,52 @@ class MainSettings : BasePreferenceFragment() {
val general = findPreference<Preference>("general") val general = findPreference<Preference>("general")
general?.setOnPreferenceClickListener { general?.setOnPreferenceClickListener {
val newFragment = GeneralSettings() navigateToSettingsFragment(GeneralSettings())
navigateToSettingsFragment(newFragment)
true
} }
val instance = findPreference<Preference>("instance") val instance = findPreference<Preference>("instance")
instance?.setOnPreferenceClickListener { instance?.setOnPreferenceClickListener {
val newFragment = InstanceSettings() navigateToSettingsFragment(InstanceSettings())
navigateToSettingsFragment(newFragment)
true
} }
val appearance = findPreference<Preference>("appearance") val appearance = findPreference<Preference>("appearance")
appearance?.setOnPreferenceClickListener { appearance?.setOnPreferenceClickListener {
val newFragment = AppearanceSettings() navigateToSettingsFragment(AppearanceSettings())
navigateToSettingsFragment(newFragment)
true
} }
val sponsorBlock = findPreference<Preference>("sponsorblock") val sponsorBlock = findPreference<Preference>("sponsorblock")
sponsorBlock?.setOnPreferenceClickListener { sponsorBlock?.setOnPreferenceClickListener {
val newFragment = SponsorBlockSettings() navigateToSettingsFragment(SponsorBlockSettings())
navigateToSettingsFragment(newFragment)
true
} }
val player = findPreference<Preference>("player") val player = findPreference<Preference>("player")
player?.setOnPreferenceClickListener { player?.setOnPreferenceClickListener {
val newFragment = PlayerSettings() navigateToSettingsFragment(PlayerSettings())
navigateToSettingsFragment(newFragment)
true
} }
val audioVideo = findPreference<Preference>("audio_video") val audioVideo = findPreference<Preference>("audio_video")
audioVideo?.setOnPreferenceClickListener { audioVideo?.setOnPreferenceClickListener {
val newFragment = AudioVideoSettings() navigateToSettingsFragment(AudioVideoSettings())
navigateToSettingsFragment(newFragment)
true
} }
val history = findPreference<Preference>("history") val history = findPreference<Preference>("history")
history?.setOnPreferenceClickListener { history?.setOnPreferenceClickListener {
val newFragment = HistorySettings() navigateToSettingsFragment(HistorySettings())
navigateToSettingsFragment(newFragment)
true
} }
val notifications = findPreference<Preference>("notifications") val notifications = findPreference<Preference>("notifications")
notifications?.setOnPreferenceClickListener { notifications?.setOnPreferenceClickListener {
val newFragment = NotificationSettings() navigateToSettingsFragment(NotificationSettings())
navigateToSettingsFragment(newFragment) }
true
val backupRestore = findPreference<Preference>("backup_restore")
backupRestore?.setOnPreferenceClickListener {
navigateToSettingsFragment(BackupRestoreSettings())
} }
val advanced = findPreference<Preference>("advanced") val advanced = findPreference<Preference>("advanced")
advanced?.setOnPreferenceClickListener { advanced?.setOnPreferenceClickListener {
val newFragment = AdvancedSettings() navigateToSettingsFragment(AdvancedSettings())
navigateToSettingsFragment(newFragment)
true
} }
val update = findPreference<Preference>("update") val update = findPreference<Preference>("update")
@ -131,9 +118,10 @@ class MainSettings : BasePreferenceFragment() {
} }
} }
private fun navigateToSettingsFragment(newFragment: Fragment) { private fun navigateToSettingsFragment(newFragment: Fragment): Boolean {
parentFragmentManager.beginTransaction() parentFragmentManager.beginTransaction()
.replace(R.id.settings, newFragment) .replace(R.id.settings, newFragment)
.commitNow() .commitNow()
return true
} }
} }

View File

@ -6,10 +6,12 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainThread import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.obj.ImportPlaylistFile
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.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -36,12 +38,10 @@ class ImportHelper(
} }
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())
Toast.makeText( activity.toastFromMainThread(
activity,
activity.getString(R.string.unsupported_file_format) + activity.getString(R.string.unsupported_file_format) +
" (${activity.contentResolver.getType(uri)}", " (${activity.contentResolver.getType(uri)}"
Toast.LENGTH_SHORT )
).show()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())
Toast.makeText(activity, e.localizedMessage, Toast.LENGTH_SHORT).show() Toast.makeText(activity, e.localizedMessage, Toast.LENGTH_SHORT).show()
@ -55,12 +55,7 @@ class ImportHelper(
return when (val fileType = activity.contentResolver.getType(uri)) { return when (val fileType = activity.contentResolver.getType(uri)) {
"application/json", "application/octet-stream" -> { "application/json", "application/octet-stream" -> {
// NewPipe subscriptions format // NewPipe subscriptions format
val mapper = ObjectMapper() val subscriptions = ObjectMapper().readValue(uri.readText(), NewPipeSubscriptions::class.java)
val json = activity.contentResolver.openInputStream(uri)?.use {
it.bufferedReader().use { reader -> reader.readText() }
}.orEmpty()
val subscriptions = mapper.readValue(json, NewPipeSubscriptions::class.java)
subscriptions.subscriptions.orEmpty().map { subscriptions.subscriptions.orEmpty().map {
it.url!!.replace("https://www.youtube.com/channel/", "") it.url!!.replace("https://www.youtube.com/channel/", "")
} }
@ -84,12 +79,9 @@ class ImportHelper(
*/ */
fun exportSubscriptions(uri: Uri?) { fun exportSubscriptions(uri: Uri?) {
if (uri == null) return if (uri == null) return
try {
val mapper = ObjectMapper()
val token = PreferenceHelper.getToken()
runBlocking { runBlocking {
val subs = if (token != "") { val subs = if (PreferenceHelper.getToken() != "") {
RetrofitInstance.authApi.subscriptions(token) RetrofitInstance.authApi.subscriptions(PreferenceHelper.getToken())
} else { } else {
RetrofitInstance.authApi.unauthenticatedSubscriptions( RetrofitInstance.authApi.unauthenticatedSubscriptions(
SubscriptionHelper.getFormattedLocalSubscriptions() SubscriptionHelper.getFormattedLocalSubscriptions()
@ -108,16 +100,62 @@ class ImportHelper(
subscriptions = newPipeChannels subscriptions = newPipeChannels
) )
val data = mapper.writeValueAsBytes(newPipeSubscriptions) uri.write(newPipeSubscriptions)
activity.contentResolver.openFileDescriptor(uri, "w")?.use { activity.toastFromMainThread(R.string.exportsuccess)
}
}
/**
* Import Playlists
*/
fun importPlaylists(uri: Uri?) {
if (uri == null) return
val playlistFile = ObjectMapper().readValue(uri.readText(), ImportPlaylistFile::class.java)
CoroutineScope(Dispatchers.IO).launch {
playlistFile.playlists?.let {
PlaylistsHelper.importPlaylists(activity, it)
}
}
activity.toastFromMainThread(R.string.success)
}
/**
* Export Playlists
*/
fun exportPlaylists(uri: Uri?) {
if (uri == null) return
runBlocking {
val playlists = PlaylistsHelper.exportPlaylists()
val playlistFile = ImportPlaylistFile(
format = "Piped",
version = 1,
playlists = playlists
)
uri.write(playlistFile)
activity.toastFromMainThread(R.string.exportsuccess)
}
}
private fun Uri.readText(): String {
return activity.contentResolver.openInputStream(this)?.use {
it.bufferedReader().use { reader -> reader.readText() }
}.orEmpty()
}
private fun Uri.write(text: Any) {
activity.contentResolver.openFileDescriptor(this, "w")?.use {
FileOutputStream(it.fileDescriptor).use { fileOutputStream -> FileOutputStream(it.fileDescriptor).use { fileOutputStream ->
fileOutputStream.write(data) fileOutputStream.write(
ObjectMapper().writeValueAsBytes(text)
)
} }
} }
} }
} catch (e: Exception) {
e.printStackTrace()
}
}
} }

View File

@ -110,8 +110,8 @@ object PlayingQueue {
fun insertPlaylist(playlistId: String, newCurrentStream: StreamItem) { fun insertPlaylist(playlistId: String, newCurrentStream: StreamItem) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val playlistType = PlaylistsHelper.getPrivateType(playlistId) val playlistType = PlaylistsHelper.getPlaylistType(playlistId)
val playlist = PlaylistsHelper.getPlaylist(playlistType, playlistId) val playlist = PlaylistsHelper.getPlaylist(playlistId)
add( add(
*playlist.relatedStreams *playlist.relatedStreams
.orEmpty() .orEmpty()

View File

@ -17,7 +17,6 @@ import com.github.libretube.extensions.toID
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.views.TimePickerPreference import com.github.libretube.ui.views.TimePickerPreference
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.time.LocalTime import java.time.LocalTime
@ -82,12 +81,9 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
var success = true var success = true
runBlocking { runBlocking {
val task = async {
SubscriptionHelper.getFeed()
}
// fetch the users feed // fetch the users feed
val videoFeed = try { val videoFeed = try {
task.await() SubscriptionHelper.getFeed()
} catch (e: Exception) { } catch (e: Exception) {
success = false success = false
return@runBlocking return@runBlocking

View File

@ -403,6 +403,11 @@
<string name="double_tap_seek_summary">Tap twice at the left or right to rewind or forward the player position.</string> <string name="double_tap_seek_summary">Tap twice at the left or right to rewind or forward the player position.</string>
<string name="all_caught_up">You\'re all caught up</string> <string name="all_caught_up">You\'re all caught up</string>
<string name="all_caught_up_summary">You\'ve seen all new videos</string> <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="app_backup">App Backup</string>
<string name="backup_restore_summary">Import &amp; export subscriptions, playlists, …</string>
<string name="exportsuccess">Successfully exported!</string>
<!-- Notification channel strings --> <!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string> <string name="download_channel_name">Download Service</string>

View File

@ -39,20 +39,6 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/backup_restore">
<Preference
android:icon="@drawable/ic_backup"
app:key="backup"
app:title="@string/backup" />
<Preference
android:icon="@drawable/ic_restore"
app:key="restore"
app:title="@string/restore" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/misc"> <PreferenceCategory app:title="@string/misc">
<Preference <Preference

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/subscriptions">
<Preference
android:icon="@drawable/ic_download_filled"
android:summary="@string/import_from_yt_summary"
app:key="import_subscriptions"
app:title="@string/import_from_yt" />
<Preference
android:icon="@drawable/ic_upload"
app:key="export_subscriptions"
app:title="@string/export_subscriptions" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/playlists">
<Preference
android:icon="@drawable/ic_download_filled"
app:key="import_playlists"
app:title="@string/import_playlists" />
<Preference
android:icon="@drawable/ic_upload"
app:key="export_playlists"
app:title="@string/export_playlists" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/app_backup">
<Preference
android:icon="@drawable/ic_backup"
app:key="backup"
app:title="@string/backup" />
<Preference
android:icon="@drawable/ic_restore"
app:key="restore"
app:title="@string/restore" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -50,6 +50,12 @@
app:key="notifications" app:key="notifications"
app:title="@string/notifications" /> app:title="@string/notifications" />
<Preference
android:icon="@drawable/ic_backup"
app:key="backup_restore"
app:summary="@string/backup_restore_summary"
app:title="@string/backup_restore" />
<Preference <Preference
android:icon="@drawable/ic_list" android:icon="@drawable/ic_list"
app:key="advanced" app:key="advanced"