package com.suno.android.common_data.mappers.hooks import arrow.core.Either import arrow.core.raise.either import com.suno.android.common_core_utils.Id import com.suno.android.common_core_utils.constants.ReactionType import com.suno.android.common_core_utils.constants.SunoMediaType import com.suno.android.common_core_utils.model.Url import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_data.mappers.clips.ClipStatus import com.suno.android.common_data.mappers.clips.LocalClipData import com.suno.android.common_networking.remote.entities.hooks.GeneratedClipSchema import com.suno.android.common_networking.remote.entities.hooks.HooksFeedSchema import com.suno.android.common_networking.remote.entities.hooks.RemoteGetUserVideoHooksResponse import com.suno.android.common_networking.remote.entities.hooks.RemoteTabCarouselResponse import com.suno.android.common_networking.remote.entities.hooks.RemoteTabItem import com.suno.android.common_networking.remote.entities.hooks.RemoteTabItemType import com.suno.android.common_networking.remote.entities.hooks.VideoHookSchema import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.time.Duration.Companion.seconds fun HooksFeedSchema.xAsHooksFeed(): HooksFeed = HooksFeed( hooks = items.mapNotNull { item -> item.xAsLocalHookData().getOrNull() }, ) /** * Maps [VideoHookSchema] network model to [LocalHookData] client model. */ fun VideoHookSchema.xAsLocalHookData(): Either = either { LocalHookData( hookId = Id(id), title = title, caption = caption, videoUrl = Url(getBestQualityVideoUrl()), thumbnailImageUrl = thumbnailImageUrl?.let(::Url), viewCount = viewCount, likeCount = likeCount, commentCount = commentCount, currentUserLiked = currentUserLiked, currentUserFollowsCreator = currentUserFollowsCreator, creator = LocalHookData.Creator( userId = Id(userId.toString()), handle = user?.handle?.let(::UserHandle), avatarUrl = user?.avatarImageUrl?.let(::Url), displayName = user?.displayName, ), createdAt = Result.runCatching { LocalDateTime.parse(createdAt, DateTimeFormatter.ISO_DATE_TIME) }.getOrNull(), clip = clip?.xAsLocalClipDataOrNull() ?: raise("failed to parse clip"), recommendationItemId = recommendationItemId?.let(::Id), humanRating = LocalHookData.HumanRating.fromString(humanRating), lyricDisplayType = toLyricsDisplayType(), clipTime = if (startClipTimestamp <= endClipTimestamp) { startClipTimestamp.seconds..endClipTimestamp.seconds } else { raise("startClipTimestamp > endClipTimestamp") }, ) } private fun VideoHookSchema.toLyricsDisplayType(): LocalHookData.LyricDisplayType? = if (!this.showLyrics) { null } else { when (this.lyricDisplay) { VideoHookSchema.LyricsDisplay.BottomLeftLine -> LocalHookData.LyricDisplayType.BottomLeftLine VideoHookSchema.LyricsDisplay.BottomLeftWord -> LocalHookData.LyricDisplayType.BottomLeftWord VideoHookSchema.LyricsDisplay.CenteredLine -> LocalHookData.LyricDisplayType.CenteredLine VideoHookSchema.LyricsDisplay.CenteredWord -> LocalHookData.LyricDisplayType.CenteredWord else -> null } } private fun VideoHookSchema.getBestQualityVideoUrl(): String { val streamingUrl = videoStreamingResolutions?.firstOrNull()?.url if (!streamingUrl.isNullOrBlank()) { return streamingUrl } return renderedVideoUrl ?: renderedVideoPreviewUrl ?: "" } private fun GeneratedClipSchema.xAsLocalClipDataOrNull(): LocalClipData? { return LocalClipData( clipId = Id(id), mediaType = SunoMediaType.AUDIO, mediaUrl = audioUrl?.let(::Url) ?: return null, artistName = displayName, artistUserId = userId?.let(::Id), handle = handle?.let(::UserHandle), artistAvatarUrl = avatarImageUrl?.let(::Url), nowPlayingTitle = title, albumImageUrl = imageLargeUrl?.let(::Url), videoCoverUrl = null, previewUrl = null, isPublic = true, modelName = modelName, majorModelVersion = majorModelVersion, reaction = if (isLiked) ReactionType.LIKE else null, prompt = null, gptPrompt = null, tags = null, displayTags = null, status = ClipStatus.fromString(status), downloadDisabledReason = null, commentCount = commentCount, upvoteCount = upvoteCount, playCount = playCount, canRemix = metadata?.canRemix, isRemix = metadata?.isRemix, ) } fun RemoteGetUserVideoHooksResponse.xAsUserVideoHooks(): UserVideoHooks = UserVideoHooks( hooks = items.mapNotNull { item -> item.xAsLocalHookData().getOrNull() }, startIndex = startIndex, pageSize = pageSize, ) fun RemoteTabCarouselResponse.xAsTabCarousel(): HooksShortcutCarousel = HooksShortcutCarousel( tabs = tabs?.mapNotNull { item -> item.xAsTabItem().getOrNull() }?.toImmutableList() ?: persistentListOf(), ) private fun RemoteTabItem.xAsTabItem() = either { when (shortcutType) { RemoteTabItemType.Playlist -> HooksShortcutCarousel.Item.Playlist( id = Id(id ?: raise("`id` not found")), name = name ?: raise("`name` not found"), imageUrl = Url(imageUrl ?: raise("`imageUrl` not found")), description = description ?: raise("`description` not found"), redirectUrl = redirectUrl?.let(::Url) ?: raise("`redirect_url` is required for type `playlist`"), ) RemoteTabItemType.Following -> HooksShortcutCarousel.Item.Following( id = Id(id ?: raise("`id` not found")), name = name ?: raise("`name` not found"), imageUrl = Url(imageUrl ?: raise("`imageUrl` not found")), description = description ?: raise("`description` not found"), ) RemoteTabItemType.MySongs -> raise("`shortcut_type`: `my_songs` is not supported") RemoteTabItemType.ContinueListening -> raise("`shortcut_type`: `continue_listening` is not supported") RemoteTabItemType.LikedSongs -> HooksShortcutCarousel.Item.Liked( id = Id(id ?: raise("`id` not found")), name = name ?: raise("`name` not found"), imageUrl = Url(imageUrl ?: raise("`imageUrl` not found")), description = description ?: raise("`description` not found"), ) null -> raise("`shortcut_type` not found") } }