refactor: bind playlist repositories to interfaces

This commit is contained in:
Bnyro 2025-01-10 15:42:59 +01:00
parent f54d0fd8a0
commit 2698368aee
6 changed files with 162 additions and 120 deletions

View File

@ -1,16 +1,16 @@
package com.github.libretube.api package com.github.libretube.api
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import com.github.libretube.api.obj.EditPlaylistBody
import com.github.libretube.api.obj.Message
import com.github.libretube.api.obj.Playlist import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.Playlists import com.github.libretube.api.obj.Playlists
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.enums.PlaylistType import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.obj.PipedImportPlaylist import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.repo.LocalPlaylistsRepository
import com.github.libretube.repo.PipedPlaylistRepository
import com.github.libretube.repo.PlaylistRepository
import com.github.libretube.util.deArrow import com.github.libretube.util.deArrow
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -24,14 +24,14 @@ object PlaylistsHelper {
private val token get() = PreferenceHelper.getToken() private val token get() = PreferenceHelper.getToken()
val loggedIn: Boolean get() = token.isNotEmpty() val loggedIn: Boolean get() = token.isNotEmpty()
private fun Message.isOk() = this.message == "ok" private val playlistsRepository: PlaylistRepository
get() = when {
loggedIn -> PipedPlaylistRepository()
else -> LocalPlaylistsRepository()
}
suspend fun getPlaylists(): List<Playlists> = withContext(Dispatchers.IO) { suspend fun getPlaylists(): List<Playlists> = withContext(Dispatchers.IO) {
val playlists = if (loggedIn) { val playlists = playlistsRepository.getPlaylists()
RetrofitInstance.authApi.getUserPlaylists(token)
} else {
LocalPlaylistsRepository.getPlaylists()
}
sortPlaylists(playlists) sortPlaylists(playlists)
} }
@ -52,109 +52,41 @@ object PlaylistsHelper {
suspend fun getPlaylist(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 (getPrivatePlaylistType(playlistId)) { return when (getPrivatePlaylistType(playlistId)) {
PlaylistType.PRIVATE -> RetrofitInstance.authApi.getPlaylist(playlistId)
PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId) PlaylistType.PUBLIC -> RetrofitInstance.api.getPlaylist(playlistId)
PlaylistType.LOCAL -> LocalPlaylistsRepository.getPlaylist(playlistId) else -> playlistsRepository.getPlaylist(playlistId)
}.apply { }.apply {
relatedStreams = relatedStreams.deArrow() relatedStreams = relatedStreams.deArrow()
} }
} }
suspend fun createPlaylist(playlistName: String): String? { suspend fun getAllPlaylistsWithVideos(playlistIds: List<String>? = null): List<Playlist> {
return if (!loggedIn) { return withContext(Dispatchers.IO) {
LocalPlaylistsRepository.createPlaylist(playlistName)
} else {
RetrofitInstance.authApi.createPlaylist(
token,
Playlists(name = playlistName)
).playlistId
}
}
suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem): Boolean {
if (!loggedIn) {
LocalPlaylistsRepository.addToPlaylist(playlistId, *videos)
return true
}
val playlist = EditPlaylistBody(playlistId, videoIds = videos.map { it.url!!.toID() })
return RetrofitInstance.authApi.addToPlaylist(token, playlist).isOk()
}
suspend fun renamePlaylist(playlistId: String, newName: String): Boolean {
if (!loggedIn) {
LocalPlaylistsRepository.renamePlaylist(playlistId, newName)
return true
}
val playlist = EditPlaylistBody(playlistId, newName = newName)
return RetrofitInstance.authApi.renamePlaylist(token, playlist).isOk()
}
suspend fun changePlaylistDescription(playlistId: String, newDescription: String): Boolean {
if (!loggedIn) {
LocalPlaylistsRepository.changePlaylistDescription(playlistId, newDescription)
return true
}
val playlist = EditPlaylistBody(playlistId, description = newDescription)
return RetrofitInstance.authApi.changePlaylistDescription(token, playlist).isOk()
}
suspend fun removeFromPlaylist(playlistId: String, index: Int): Boolean {
if (!loggedIn) {
LocalPlaylistsRepository.removeFromPlaylist(playlistId, index)
return true
}
return RetrofitInstance.authApi.removeFromPlaylist(
PreferenceHelper.getToken(),
EditPlaylistBody(playlistId = playlistId, index = index)
).isOk()
}
suspend fun importPlaylists(playlists: List<PipedImportPlaylist>) =
withContext(Dispatchers.IO) {
if (!loggedIn) return@withContext LocalPlaylistsRepository.importPlaylists(playlists)
for (playlist in playlists) {
val playlistId = createPlaylist(playlist.name!!) ?: return@withContext
val streams = playlist.videos.map { StreamItem(url = it) }
addToPlaylist(playlistId, *streams.toTypedArray())
}
}
suspend fun getAllPlaylistsWithVideos(playlistIds: List<String>? = null): List<Playlist> =
withContext(Dispatchers.IO) {
(playlistIds ?: getPlaylists().map { it.id!! }) (playlistIds ?: getPlaylists().map { it.id!! })
.map { async { getPlaylist(it) } } .map { async { getPlaylist(it) } }
.awaitAll() .awaitAll()
} }
suspend fun clonePlaylist(playlistId: String): String? {
if (!loggedIn) {
return LocalPlaylistsRepository.clonePlaylist(playlistId)
} }
return RetrofitInstance.authApi.clonePlaylist( suspend fun createPlaylist(playlistName: String) =
token, playlistsRepository.createPlaylist(playlistName)
EditPlaylistBody(playlistId)
).playlistId
}
suspend fun deletePlaylist(playlistId: String, playlistType: PlaylistType): Boolean { suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem) =
if (playlistType == PlaylistType.LOCAL) { playlistsRepository.addToPlaylist(playlistId, *videos)
LocalPlaylistsRepository.deletePlaylist(playlistId)
return true
}
return runCatching { suspend fun renamePlaylist(playlistId: String, newName: String) =
RetrofitInstance.authApi.deletePlaylist( playlistsRepository.renamePlaylist(playlistId, newName)
PreferenceHelper.getToken(),
EditPlaylistBody(playlistId) suspend fun changePlaylistDescription(playlistId: String, newDescription: String) =
).isOk() playlistsRepository.changePlaylistDescription(playlistId, newDescription)
}.getOrDefault(false)
} suspend fun removeFromPlaylist(playlistId: String, index: Int) =
playlistsRepository.removeFromPlaylist(playlistId, index)
suspend fun importPlaylists(playlists: List<PipedImportPlaylist>) =
playlistsRepository.importPlaylists(playlists)
suspend fun clonePlaylist(playlistId: String) = playlistsRepository.clonePlaylist(playlistId)
suspend fun deletePlaylist(playlistId: String) = playlistsRepository.deletePlaylist(playlistId)
fun getPrivatePlaylistType(): PlaylistType { fun getPrivatePlaylistType(): PlaylistType {
return if (loggedIn) PlaylistType.PRIVATE else PlaylistType.LOCAL return if (loggedIn) PlaylistType.PRIVATE else PlaylistType.LOCAL

View File

@ -1,6 +1,9 @@
package com.github.libretube.api package com.github.libretube.repo
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.PlaylistsHelper.MAX_CONCURRENT_IMPORT_CALLS import com.github.libretube.api.PlaylistsHelper.MAX_CONCURRENT_IMPORT_CALLS
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.StreamsExtractor
import com.github.libretube.api.obj.Playlist import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.Playlists import com.github.libretube.api.obj.Playlists
import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.StreamItem
@ -10,8 +13,8 @@ import com.github.libretube.extensions.parallelMap
import com.github.libretube.helpers.ProxyHelper import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.PipedImportPlaylist import com.github.libretube.obj.PipedImportPlaylist
object LocalPlaylistsRepository { class LocalPlaylistsRepository: PlaylistRepository {
suspend fun getPlaylist(playlistId: String): Playlist { override suspend fun getPlaylist(playlistId: String): Playlist {
val relation = DatabaseHolder.Database.localPlaylistsDao().getAll() val relation = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId } .first { it.playlist.id.toString() == playlistId }
@ -24,7 +27,7 @@ object LocalPlaylistsRepository {
) )
} }
suspend fun getPlaylists(): List<Playlists> { override suspend fun getPlaylists(): List<Playlists> {
return DatabaseHolder.Database.localPlaylistsDao().getAll() return DatabaseHolder.Database.localPlaylistsDao().getAll()
.map { .map {
Playlists( Playlists(
@ -37,7 +40,7 @@ object LocalPlaylistsRepository {
} }
} }
suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem) { override suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem): Boolean {
val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll() val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId } .first { it.playlist.id.toString() == playlistId }
@ -59,25 +62,31 @@ object LocalPlaylistsRepository {
} }
} }
} }
return true
} }
suspend fun renamePlaylist(playlistId: String, newName: String) { override suspend fun renamePlaylist(playlistId: String, newName: String): Boolean {
val playlist = DatabaseHolder.Database.localPlaylistsDao().getAll() val playlist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }.playlist .first { it.playlist.id.toString() == playlistId }.playlist
playlist.name = newName playlist.name = newName
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist) DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
return true
} }
suspend fun changePlaylistDescription(playlistId: String, newDescription: String) { override suspend fun changePlaylistDescription(playlistId: String, newDescription: String): Boolean {
val playlist = DatabaseHolder.Database.localPlaylistsDao().getAll() val playlist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }.playlist .first { it.playlist.id.toString() == playlistId }.playlist
playlist.description = newDescription playlist.description = newDescription
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist) DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
return true
} }
suspend fun clonePlaylist(playlistId: String): String? { override suspend fun clonePlaylist(playlistId: String): String {
val playlist = RetrofitInstance.api.getPlaylist(playlistId) val playlist = RetrofitInstance.api.getPlaylist(playlistId)
val newPlaylist = createPlaylist(playlist.name ?: "Unknown name") ?: return null val newPlaylist = createPlaylist(playlist.name ?: "Unknown name")
PlaylistsHelper.addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray()) PlaylistsHelper.addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray())
@ -93,7 +102,7 @@ object LocalPlaylistsRepository {
return playlistId return playlistId
} }
suspend fun removeFromPlaylist(playlistId: String, index: Int) { override suspend fun removeFromPlaylist(playlistId: String, index: Int): Boolean {
val transaction = DatabaseHolder.Database.localPlaylistsDao().getAll() val transaction = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId } .first { it.playlist.id.toString() == playlistId }
DatabaseHolder.Database.localPlaylistsDao().removePlaylistVideo( DatabaseHolder.Database.localPlaylistsDao().removePlaylistVideo(
@ -105,11 +114,13 @@ object LocalPlaylistsRepository {
transaction.videos.getOrNull(1)?.thumbnailUrl.orEmpty() transaction.videos.getOrNull(1)?.thumbnailUrl.orEmpty()
} }
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(transaction.playlist) DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(transaction.playlist)
return true
} }
suspend fun importPlaylists(playlists: List<PipedImportPlaylist>) { override suspend fun importPlaylists(playlists: List<PipedImportPlaylist>) {
for (playlist in playlists) { for (playlist in playlists) {
val playlistId = createPlaylist(playlist.name!!) ?: return val playlistId = createPlaylist(playlist.name!!)
// if not logged in, all video information needs to become fetched manually // if not logged in, all video information needs to become fetched manually
// Only do so with `MAX_CONCURRENT_IMPORT_CALLS` videos at once to prevent performance issues // Only do so with `MAX_CONCURRENT_IMPORT_CALLS` videos at once to prevent performance issues
@ -125,13 +136,15 @@ object LocalPlaylistsRepository {
} }
} }
suspend fun createPlaylist(playlistName: String): String { override suspend fun createPlaylist(playlistName: String): String {
val playlist = LocalPlaylist(name = playlistName, thumbnailUrl = "") val playlist = LocalPlaylist(name = playlistName, thumbnailUrl = "")
return DatabaseHolder.Database.localPlaylistsDao().createPlaylist(playlist).toString() return DatabaseHolder.Database.localPlaylistsDao().createPlaylist(playlist).toString()
} }
suspend fun deletePlaylist(playlistId: String) { override suspend fun deletePlaylist(playlistId: String): Boolean {
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId) DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId)
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(playlistId) DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(playlistId)
return true
} }
} }

View File

@ -0,0 +1,81 @@
package com.github.libretube.repo
import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.EditPlaylistBody
import com.github.libretube.api.obj.Message
import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.Playlists
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.obj.PipedImportPlaylist
class PipedPlaylistRepository: PlaylistRepository {
private fun Message.isOk() = this.message == "ok"
private val token get() = PreferenceHelper.getToken()
override suspend fun getPlaylist(playlistId: String): Playlist {
return RetrofitInstance.authApi.getPlaylist(playlistId)
}
override suspend fun getPlaylists(): List<Playlists> {
return RetrofitInstance.authApi.getUserPlaylists(token)
}
override suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem): Boolean {
val playlist = EditPlaylistBody(playlistId, videoIds = videos.map { it.url!!.toID() })
return RetrofitInstance.authApi.addToPlaylist(token, playlist).isOk()
}
override suspend fun renamePlaylist(playlistId: String, newName: String): Boolean {
val playlist = EditPlaylistBody(playlistId, newName = newName)
return RetrofitInstance.authApi.renamePlaylist(token, playlist).isOk()
}
override suspend fun changePlaylistDescription(playlistId: String, newDescription: String): Boolean {
val playlist = EditPlaylistBody(playlistId, description = newDescription)
return RetrofitInstance.authApi.changePlaylistDescription(token, playlist).isOk()
}
override suspend fun clonePlaylist(playlistId: String): String? {
return RetrofitInstance.authApi.clonePlaylist(
token,
EditPlaylistBody(playlistId)
).playlistId
}
override suspend fun removeFromPlaylist(playlistId: String, index: Int): Boolean {
return RetrofitInstance.authApi.removeFromPlaylist(
PreferenceHelper.getToken(),
EditPlaylistBody(playlistId = playlistId, index = index)
).isOk()
}
override suspend fun importPlaylists(playlists: List<PipedImportPlaylist>) {
for (playlist in playlists) {
val playlistId = PlaylistsHelper.createPlaylist(playlist.name!!) ?: return
val streams = playlist.videos.map { StreamItem(url = it) }
PlaylistsHelper.addToPlaylist(playlistId, *streams.toTypedArray())
}
}
override suspend fun createPlaylist(playlistName: String): String? {
return RetrofitInstance.authApi.createPlaylist(
token,
Playlists(name = playlistName)
).playlistId
}
override suspend fun deletePlaylist(playlistId: String): Boolean {
return runCatching {
RetrofitInstance.authApi.deletePlaylist(
PreferenceHelper.getToken(),
EditPlaylistBody(playlistId)
).isOk()
}.getOrDefault(false)
}
}

View File

@ -0,0 +1,19 @@
package com.github.libretube.repo
import com.github.libretube.api.obj.Playlist
import com.github.libretube.api.obj.Playlists
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.obj.PipedImportPlaylist
interface PlaylistRepository {
suspend fun getPlaylist(playlistId: String): Playlist
suspend fun getPlaylists(): List<Playlists>
suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem): Boolean
suspend fun renamePlaylist(playlistId: String, newName: String): Boolean
suspend fun changePlaylistDescription(playlistId: String, newDescription: String): Boolean
suspend fun clonePlaylist(playlistId: String): String?
suspend fun removeFromPlaylist(playlistId: String, index: Int): Boolean
suspend fun importPlaylists(playlists: List<PipedImportPlaylist>)
suspend fun createPlaylist(playlistName: String): String?
suspend fun deletePlaylist(playlistId: String): Boolean
}

View File

@ -10,8 +10,6 @@ import androidx.lifecycle.lifecycleScope
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.PlaylistsHelper import com.github.libretube.api.PlaylistsHelper
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.serializable
import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -21,14 +19,14 @@ import kotlinx.coroutines.withContext
class DeletePlaylistDialog : DialogFragment() { class DeletePlaylistDialog : DialogFragment() {
private lateinit var playlistId: String private lateinit var playlistId: String
private lateinit var playlistType: PlaylistType
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
arguments?.let { arguments?.let {
playlistId = it.getString(IntentData.playlistId)!! playlistId = it.getString(IntentData.playlistId)!!
playlistType = it.serializable(IntentData.playlistType)!!
} }
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext()) return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.deletePlaylist) .setTitle(R.string.deletePlaylist)
@ -39,7 +37,7 @@ class DeletePlaylistDialog : DialogFragment() {
.apply { .apply {
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val success = PlaylistsHelper.deletePlaylist(playlistId, playlistType) val success = PlaylistsHelper.deletePlaylist(playlistId)
context.toastFromMainDispatcher( context.toastFromMainDispatcher(
if (success) R.string.success else R.string.fail if (success) R.string.success else R.string.fail
) )

View File

@ -123,8 +123,7 @@ class PlaylistOptionsBottomSheet : BaseBottomSheet() {
R.string.deletePlaylist -> { R.string.deletePlaylist -> {
val newDeletePlaylistDialog = DeletePlaylistDialog() val newDeletePlaylistDialog = DeletePlaylistDialog()
newDeletePlaylistDialog.arguments = bundleOf( newDeletePlaylistDialog.arguments = bundleOf(
IntentData.playlistId to playlistId, IntentData.playlistId to playlistId
IntentData.playlistType to playlistType
) )
newDeletePlaylistDialog.show(mFragmentManager, null) newDeletePlaylistDialog.show(mFragmentManager, null)
} }