refactor: move local playlists logic to LocalPlaylistsRepository

This commit is contained in:
Bnyro 2025-01-08 15:29:19 +01:00
parent daf22c499a
commit 0a724e56dc
2 changed files with 168 additions and 118 deletions

View File

@ -0,0 +1,137 @@
package com.github.libretube.api
import com.github.libretube.api.PlaylistsHelper.MAX_CONCURRENT_IMPORT_CALLS
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.db.DatabaseHolder
import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.extensions.parallelMap
import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.PipedImportPlaylist
object LocalPlaylistsRepository {
suspend fun getPlaylist(playlistId: String): Playlist {
val relation = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }
return Playlist(
name = relation.playlist.name,
description = relation.playlist.description,
thumbnailUrl = ProxyHelper.rewriteUrl(relation.playlist.thumbnailUrl),
videos = relation.videos.size,
relatedStreams = relation.videos.map { it.toStreamItem() }
)
}
suspend fun getPlaylists(): List<Playlists> {
return DatabaseHolder.Database.localPlaylistsDao().getAll()
.map {
Playlists(
id = it.playlist.id.toString(),
name = it.playlist.name,
shortDescription = it.playlist.description,
thumbnail = ProxyHelper.rewriteUrl(it.playlist.thumbnailUrl),
videos = it.videos.size.toLong()
)
}
}
suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem) {
val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }
for (video in videos) {
val localPlaylistItem = video.toLocalPlaylistItem(playlistId)
// avoid duplicated videos in a playlist
DatabaseHolder.Database.localPlaylistsDao()
.deletePlaylistItemsByVideoId(playlistId, localPlaylistItem.videoId)
// add the new video to the database
DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem)
val playlist = localPlaylist.playlist
if (playlist.thumbnailUrl.isEmpty()) {
// set the new playlist thumbnail URL
localPlaylistItem.thumbnailUrl?.let {
playlist.thumbnailUrl = it
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
}
}
}
}
suspend fun renamePlaylist(playlistId: String, newName: String) {
val playlist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }.playlist
playlist.name = newName
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
}
suspend fun changePlaylistDescription(playlistId: String, newDescription: String) {
val playlist = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }.playlist
playlist.description = newDescription
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
}
suspend fun clonePlaylist(playlistId: String): String? {
val playlist = RetrofitInstance.api.getPlaylist(playlistId)
val newPlaylist = createPlaylist(playlist.name ?: "Unknown name") ?: return null
PlaylistsHelper.addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray())
var nextPage = playlist.nextpage
while (nextPage != null) {
nextPage = runCatching {
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage!!).apply {
PlaylistsHelper.addToPlaylist(newPlaylist, *relatedStreams.toTypedArray())
}.nextpage
}.getOrNull()
}
return playlistId
}
suspend fun removeFromPlaylist(playlistId: String, index: Int) {
val transaction = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }
DatabaseHolder.Database.localPlaylistsDao().removePlaylistVideo(
transaction.videos[index]
)
// set a new playlist thumbnail if the first video got removed
if (index == 0) {
transaction.playlist.thumbnailUrl =
transaction.videos.getOrNull(1)?.thumbnailUrl.orEmpty()
}
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(transaction.playlist)
}
suspend fun importPlaylists(playlists: List<PipedImportPlaylist>) {
for (playlist in playlists) {
val playlistId = createPlaylist(playlist.name!!) ?: return
// 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
for (videoIdList in playlist.videos.chunked(MAX_CONCURRENT_IMPORT_CALLS)) {
val streams = videoIdList.parallelMap {
runCatching { StreamsExtractor.extractStreams(it) }
.getOrNull()
?.toStreamItem(it)
}.filterNotNull()
PlaylistsHelper.addToPlaylist(playlistId, *streams.toTypedArray())
}
}
}
suspend fun createPlaylist(playlistName: String): String {
val playlist = LocalPlaylist(name = playlistName, thumbnailUrl = "")
return DatabaseHolder.Database.localPlaylistsDao().createPlaylist(playlist).toString()
}
suspend fun deletePlaylist(playlistId: String) {
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId)
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(playlistId)
}
}

View File

@ -7,13 +7,9 @@ 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.db.DatabaseHolder
import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.enums.PlaylistType import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.parallelMap
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.PipedImportPlaylist import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.util.deArrow import com.github.libretube.util.deArrow
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -24,7 +20,7 @@ import kotlinx.coroutines.withContext
object PlaylistsHelper { object PlaylistsHelper {
private val pipedPlaylistRegex = private val pipedPlaylistRegex =
"[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}".toRegex() "[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}".toRegex()
private const val MAX_CONCURRENT_IMPORT_CALLS = 5 const val MAX_CONCURRENT_IMPORT_CALLS = 5
private val token get() = PreferenceHelper.getToken() private val token get() = PreferenceHelper.getToken()
val loggedIn: Boolean get() = token.isNotEmpty() val loggedIn: Boolean get() = token.isNotEmpty()
@ -34,16 +30,7 @@ object PlaylistsHelper {
val playlists = if (loggedIn) { val playlists = if (loggedIn) {
RetrofitInstance.authApi.getUserPlaylists(token) RetrofitInstance.authApi.getUserPlaylists(token)
} else { } else {
DatabaseHolder.Database.localPlaylistsDao().getAll() LocalPlaylistsRepository.getPlaylists()
.map {
Playlists(
id = it.playlist.id.toString(),
name = it.playlist.name,
shortDescription = it.playlist.description,
thumbnail = ProxyHelper.rewriteUrl(it.playlist.thumbnailUrl),
videos = it.videos.size.toLong()
)
}
} }
sortPlaylists(playlists) sortPlaylists(playlists)
} }
@ -67,17 +54,7 @@ object PlaylistsHelper {
return when (getPrivatePlaylistType(playlistId)) { return when (getPrivatePlaylistType(playlistId)) {
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 -> LocalPlaylistsRepository.getPlaylist(playlistId)
val relation = DatabaseHolder.Database.localPlaylistsDao().getAll()
.first { it.playlist.id.toString() == playlistId }
return Playlist(
name = relation.playlist.name,
description = relation.playlist.description,
thumbnailUrl = ProxyHelper.rewriteUrl(relation.playlist.thumbnailUrl),
videos = relation.videos.size,
relatedStreams = relation.videos.map { it.toStreamItem() }
)
}
}.apply { }.apply {
relatedStreams = relatedStreams.deArrow() relatedStreams = relatedStreams.deArrow()
} }
@ -85,8 +62,7 @@ object PlaylistsHelper {
suspend fun createPlaylist(playlistName: String): String? { suspend fun createPlaylist(playlistName: String): String? {
return if (!loggedIn) { return if (!loggedIn) {
val playlist = LocalPlaylist(name = playlistName, thumbnailUrl = "") LocalPlaylistsRepository.createPlaylist(playlistName)
DatabaseHolder.Database.localPlaylistsDao().createPlaylist(playlist).toString()
} else { } else {
RetrofitInstance.authApi.createPlaylist( RetrofitInstance.authApi.createPlaylist(
token, token,
@ -97,27 +73,7 @@ object PlaylistsHelper {
suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem): Boolean { suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem): Boolean {
if (!loggedIn) { if (!loggedIn) {
val localPlaylist = DatabaseHolder.Database.localPlaylistsDao().getAll() LocalPlaylistsRepository.addToPlaylist(playlistId, *videos)
.first { it.playlist.id.toString() == playlistId }
for (video in videos) {
val localPlaylistItem = video.toLocalPlaylistItem(playlistId)
// avoid duplicated videos in a playlist
DatabaseHolder.Database.localPlaylistsDao()
.deletePlaylistItemsByVideoId(playlistId, localPlaylistItem.videoId)
// add the new video to the database
DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem)
val playlist = localPlaylist.playlist
if (playlist.thumbnailUrl.isEmpty()) {
// set the new playlist thumbnail URL
localPlaylistItem.thumbnailUrl?.let {
playlist.thumbnailUrl = it
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
}
}
}
return true return true
} }
@ -126,74 +82,45 @@ object PlaylistsHelper {
} }
suspend fun renamePlaylist(playlistId: String, newName: String): Boolean { suspend fun renamePlaylist(playlistId: String, newName: String): Boolean {
return if (!loggedIn) { if (!loggedIn) {
val playlist = DatabaseHolder.Database.localPlaylistsDao().getAll() LocalPlaylistsRepository.renamePlaylist(playlistId, newName)
.first { it.playlist.id.toString() == playlistId }.playlist return true
playlist.name = newName
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
true
} else {
val playlist = EditPlaylistBody(playlistId, newName = newName)
RetrofitInstance.authApi.renamePlaylist(token, playlist).isOk()
} }
val playlist = EditPlaylistBody(playlistId, newName = newName)
return RetrofitInstance.authApi.renamePlaylist(token, playlist).isOk()
} }
suspend fun changePlaylistDescription(playlistId: String, newDescription: String): Boolean { suspend fun changePlaylistDescription(playlistId: String, newDescription: String): Boolean {
return if (!loggedIn) { if (!loggedIn) {
val playlist = DatabaseHolder.Database.localPlaylistsDao().getAll() LocalPlaylistsRepository.changePlaylistDescription(playlistId, newDescription)
.first { it.playlist.id.toString() == playlistId }.playlist return true
playlist.description = newDescription
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist)
true
} else {
val playlist = EditPlaylistBody(playlistId, description = newDescription)
RetrofitInstance.authApi.changePlaylistDescription(token, playlist).isOk()
} }
val playlist = EditPlaylistBody(playlistId, description = newDescription)
return RetrofitInstance.authApi.changePlaylistDescription(token, playlist).isOk()
} }
suspend fun removeFromPlaylist(playlistId: String, index: Int): Boolean { suspend fun removeFromPlaylist(playlistId: String, index: Int): Boolean {
return if (!loggedIn) { if (!loggedIn) {
val transaction = DatabaseHolder.Database.localPlaylistsDao().getAll() LocalPlaylistsRepository.removeFromPlaylist(playlistId, index)
.first { it.playlist.id.toString() == playlistId } return true
DatabaseHolder.Database.localPlaylistsDao().removePlaylistVideo(
transaction.videos[index]
)
// set a new playlist thumbnail if the first video got removed
if (index == 0) {
transaction.playlist.thumbnailUrl =
transaction.videos.getOrNull(1)?.thumbnailUrl.orEmpty()
} }
DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(transaction.playlist)
true return RetrofitInstance.authApi.removeFromPlaylist(
} else {
RetrofitInstance.authApi.removeFromPlaylist(
PreferenceHelper.getToken(), PreferenceHelper.getToken(),
EditPlaylistBody(playlistId = playlistId, index = index) EditPlaylistBody(playlistId = playlistId, index = index)
).isOk() ).isOk()
} }
}
suspend fun importPlaylists(playlists: List<PipedImportPlaylist>) = suspend fun importPlaylists(playlists: List<PipedImportPlaylist>) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!loggedIn) return@withContext LocalPlaylistsRepository.importPlaylists(playlists)
for (playlist in playlists) { for (playlist in playlists) {
val playlistId = createPlaylist(playlist.name!!) ?: return@withContext val playlistId = createPlaylist(playlist.name!!) ?: return@withContext
// if logged in, add the playlists by their ID via an api call
if (loggedIn) {
val streams = playlist.videos.map { StreamItem(url = it) } val streams = playlist.videos.map { StreamItem(url = it) }
addToPlaylist(playlistId, *streams.toTypedArray()) addToPlaylist(playlistId, *streams.toTypedArray())
} else {
// 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
for (videoIdList in playlist.videos.chunked(MAX_CONCURRENT_IMPORT_CALLS)) {
val streams = videoIdList.parallelMap {
runCatching { StreamsExtractor.extractStreams(it) }
.getOrNull()
?.toStreamItem(it)
}.filterNotNull()
addToPlaylist(playlistId, *streams.toTypedArray())
}
}
} }
} }
@ -206,20 +133,7 @@ object PlaylistsHelper {
suspend fun clonePlaylist(playlistId: String): String? { suspend fun clonePlaylist(playlistId: String): String? {
if (!loggedIn) { if (!loggedIn) {
val playlist = RetrofitInstance.api.getPlaylist(playlistId) return LocalPlaylistsRepository.clonePlaylist(playlistId)
val newPlaylist = createPlaylist(playlist.name ?: "Unknown name") ?: return null
addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray())
var nextPage = playlist.nextpage
while (nextPage != null) {
nextPage = runCatching {
RetrofitInstance.api.getPlaylistNextPage(playlistId, nextPage!!).apply {
addToPlaylist(newPlaylist, *relatedStreams.toTypedArray())
}.nextpage
}.getOrNull()
}
return playlistId
} }
return RetrofitInstance.authApi.clonePlaylist( return RetrofitInstance.authApi.clonePlaylist(
@ -230,8 +144,7 @@ object PlaylistsHelper {
suspend fun deletePlaylist(playlistId: String, playlistType: PlaylistType): Boolean { suspend fun deletePlaylist(playlistId: String, playlistType: PlaylistType): Boolean {
if (playlistType == PlaylistType.LOCAL) { if (playlistType == PlaylistType.LOCAL) {
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistId) LocalPlaylistsRepository.deletePlaylist(playlistId)
DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(playlistId)
return true return true
} }