package com.github.libretube.repo import com.github.libretube.api.MediaServiceRepository import com.github.libretube.api.PlaylistsHelper 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.obj.PipedImportPlaylist class LocalPlaylistsRepository: PlaylistRepository { private fun toLocalPlaylistId(playlistId: String): Int? = playlistId.toIntOrNull() override suspend fun getPlaylist(playlistId: String): Playlist { val relation = DatabaseHolder.Database.localPlaylistsDao() .getPlaylistWithVideos(toLocalPlaylistId(playlistId) ?: throw IllegalArgumentException("Invalid playlist id")) ?: throw NoSuchElementException("No local playlist with id $playlistId") return Playlist( name = relation.playlist.name, description = relation.playlist.description, thumbnailUrl = relation.playlist.thumbnailUrl, videos = relation.videos.size, relatedStreams = relation.videos.map { it.toStreamItem() } ) } override suspend fun getPlaylists(): List { return DatabaseHolder.Database.localPlaylistsDao().getAll() .map { Playlists( id = it.playlist.id.toString(), name = it.playlist.name, shortDescription = it.playlist.description, thumbnail = it.playlist.thumbnailUrl, videos = it.videos.size.toLong() ) } } override suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem): Boolean { val playlistIdInt = toLocalPlaylistId(playlistId) ?: return false val localPlaylist = DatabaseHolder.Database.localPlaylistsDao() .getPlaylistWithVideos(playlistIdInt) ?: return false for (video in videos) { val localPlaylistItem = video.toLocalPlaylistItem(playlistId) val existingVideo = DatabaseHolder.Database.localPlaylistsDao() .getPlaylistVideo(playlistIdInt, localPlaylistItem.videoId) if (existingVideo != null) { localPlaylistItem.id = existingVideo.id DatabaseHolder.Database.localPlaylistsDao().updatePlaylistVideo(localPlaylistItem) continue } DatabaseHolder.Database.localPlaylistsDao().addPlaylistVideo(localPlaylistItem) val playlist = localPlaylist.playlist if (playlist.thumbnailUrl.isEmpty()) { localPlaylistItem.thumbnailUrl?.let { playlist.thumbnailUrl = it DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist) } } } return true } override suspend fun renamePlaylist(playlistId: String, newName: String): Boolean { val playlistIdInt = toLocalPlaylistId(playlistId) ?: return false val playlist = DatabaseHolder.Database.localPlaylistsDao() .getPlaylistWithVideos(playlistIdInt)?.playlist ?: return false playlist.name = newName DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist) return true } override suspend fun changePlaylistDescription(playlistId: String, newDescription: String): Boolean { val playlistIdInt = toLocalPlaylistId(playlistId) ?: return false val playlist = DatabaseHolder.Database.localPlaylistsDao() .getPlaylistWithVideos(playlistIdInt)?.playlist ?: return false playlist.description = newDescription DatabaseHolder.Database.localPlaylistsDao().updatePlaylist(playlist) return true } override suspend fun clonePlaylist(playlistId: String): String { val playlist = MediaServiceRepository.instance.getPlaylist(playlistId) val newPlaylist = createPlaylist(playlist.name ?: "Unknown name") PlaylistsHelper.addToPlaylist(newPlaylist, *playlist.relatedStreams.toTypedArray()) var nextPage = playlist.nextpage while (nextPage != null) { nextPage = runCatching { MediaServiceRepository.instance.getPlaylistNextPage(playlistId, nextPage!!).apply { PlaylistsHelper.addToPlaylist(newPlaylist, *relatedStreams.toTypedArray()) }.nextpage }.getOrNull() } return newPlaylist } override suspend fun removeFromPlaylist(playlistId: String, index: Int): Boolean { val playlistIdInt = toLocalPlaylistId(playlistId) ?: return false val transaction = DatabaseHolder.Database.localPlaylistsDao() .getPlaylistWithVideos(playlistIdInt) ?: return false 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) return true } override suspend fun importPlaylists(playlists: List) { for (playlist in playlists) { val playlistId = createPlaylist(playlist.name!!) // 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 { MediaServiceRepository.instance.getStreams(it) } .getOrNull() ?.toStreamItem(it) }.filterNotNull() PlaylistsHelper.addToPlaylist(playlistId, *streams.toTypedArray()) } } } override suspend fun createPlaylist(playlistName: String): String { val playlist = LocalPlaylist(name = playlistName, thumbnailUrl = "") return DatabaseHolder.Database.localPlaylistsDao().createPlaylist(playlist).toString() } override suspend fun deletePlaylist(playlistId: String): Boolean { val playlistIdInt = toLocalPlaylistId(playlistId) ?: return false DatabaseHolder.Database.localPlaylistsDao().deletePlaylistById(playlistIdInt) DatabaseHolder.Database.localPlaylistsDao().deletePlaylistItemsByPlaylistId(playlistIdInt) return true } }