package com.suno.android.common_data.mappers.clips 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.extensions.xToDate import com.suno.android.common_core_utils.model.Url import com.suno.android.common_core_utils.model.UserHandle import com.suno.android.common_data.badge.ModelBadgeData import com.suno.android.common_db.database.entities.CaptionMentionsEntity import com.suno.android.common_db.database.entities.LocalClipEntity import com.suno.android.common_db.database.entities.LocalClipMetadataEntity import com.suno.android.common_db.database.entities.LocalReactionEntity import com.suno.android.common_db.database.entities.MentionEntity import com.suno.android.common_networking.remote.common.BadgeSchema import com.suno.android.common_networking.remote.common.BadgeThemeSchema import com.suno.android.common_networking.remote.common.CaptionMentionsSchema import com.suno.android.common_networking.remote.common.ModelBadgeSchema import com.suno.android.common_networking.remote.common.RemoteClip import com.suno.android.common_networking.remote.common.RemoteClipMetadata import com.suno.android.common_networking.remote.common.RemoteClipReaction import com.suno.android.common_networking.remote.entities.GeneratedClipSSRSchema import com.suno.android.common_networking.remote.entities.MediaItemRemoteEntity import com.suno.android.common_networking.remote.entities.MentionSchema import com.suno.android.common_networking.remote.entities.ResultInner import kotlinx.serialization.SerializationException import java.util.Date import kotlin.time.DurationUnit import kotlin.time.toDuration fun ModelBadgeSchema.xAsModelBadgeData(): ModelBadgeData? = try { this.songRow?.xAsModelBadgeData() } catch (_: SerializationException) { null } catch (_: IllegalArgumentException) { null } private fun BadgeSchema.xAsModelBadgeData(): ModelBadgeData? { return ModelBadgeData( displayName = this.displayName ?: return null, themes = ModelBadgeData.Themes( lightTheme = light?.xAsModelBadgeDataTheme() ?: return null, darkTheme = dark?.xAsModelBadgeDataTheme() ?: return null, ), ) } private fun BadgeThemeSchema.xAsModelBadgeDataTheme(): ModelBadgeData.Themes.Theme? { return ModelBadgeData.Themes.Theme( textColor = this.textColor ?: return null, backgroundColor = this.backgroundColor ?: return null, borderColor = this.borderColor, ) } fun MentionEntity.xAsMentionSchema(): MentionSchema = MentionSchema( start = this.start, end = this.end, handle = this.handle, displayName = this.displayName, ) fun MentionSchema.xAsMentionEntity(): MentionEntity = MentionEntity( start = this.start, end = this.end, handle = this.handle, displayName = this.displayName, ) fun LocalClipEntity.xAsRemote(): RemoteClip = RemoteClip( audioUrl = this.audioUrl, avatarImageUrl = this.avatarImageUrl, createdAt = this.createdAt, displayName = this.displayName, displayTags = this.displayTags, handle = this.handle, id = this.clipId, imageLargeUrl = this.imageLargeUrl, imageUrl = this.imageUrl, videoCoverUrl = this.videoCoverUrl, previewUrl = this.previewUrl, isHandleUpdated = this.isHandleUpdated, isLiked = this.isLiked, isPublic = this.isPublic, isTrashed = this.isTrashed, isVideoPending = this.isVideoPending, majorModelVersion = this.majorModelVersion, clipMetadata = RemoteClipMetadata( coverClipId = this.metadata.coverClipId, duration = this.metadata.duration, gptDescriptionPrompt = this.metadata.gptDescriptionPrompt, negativeTags = this.metadata.negativeTags, prompt = this.metadata.prompt, refundCredits = this.metadata.refundCredits, stream = this.metadata.stream, tags = this.metadata.tags, task = this.metadata.task, type = this.metadata.type, canRemix = this.metadata.canRemix, isRemix = this.metadata.isRemix, optOutVideoCoverHook = this.metadata.optOutVideoCoverHook, modelBadge = null, // Local entities don't have model badges - they come from API ), modelName = this.modelName, playCount = this.playCount, clipReaction = RemoteClipReaction( clip = this.reaction?.clip, flagged = this.reaction?.flagged, flaggedReason = this.reaction?.flaggedReason, feedbackReason = this.reaction?.feedbackReason, playCount = this.reaction?.playCount, skipCount = this.reaction?.skipCount, reactionType = this.reaction?.reactionType, updatedAt = this.reaction?.updatedAt, ), status = this.status, title = this.title, upvoteCount = this.upvoteCount, userId = this.userId, videoUrl = this.videoUrl, commentCount = this.commentCount, caption = this.caption, captionMentions = CaptionMentionsSchema( userMentions = this.captionMentions?.userMentions?.map { it.xAsMentionSchema() }, ), downloadDisabledReason = this.downloadDisabledReason, ) fun RemoteClip.xAsDb(): LocalClipEntity = LocalClipEntity( clipId = this.id, audioUrl = this.audioUrl, avatarImageUrl = this.avatarImageUrl, createdAt = this.createdAt, displayName = this.displayName, displayTags = this.displayTags, handle = this.handle, imageLargeUrl = this.imageLargeUrl, imageUrl = this.imageUrl, videoCoverUrl = this.videoCoverUrl, previewUrl = this.previewUrl, isHandleUpdated = this.isHandleUpdated, isLiked = this.isLiked, isPublic = this.isPublic, isTrashed = this.isTrashed, isVideoPending = this.isVideoPending, majorModelVersion = this.majorModelVersion, metadata = LocalClipMetadataEntity( coverClipId = this.clipMetadata.coverClipId, duration = this.clipMetadata.duration, gptDescriptionPrompt = this.clipMetadata.gptDescriptionPrompt, negativeTags = this.clipMetadata.negativeTags, prompt = this.clipMetadata.prompt, refundCredits = this.clipMetadata.refundCredits, stream = this.clipMetadata.stream, tags = this.clipMetadata.tags, task = this.clipMetadata.task, type = this.clipMetadata.type, canRemix = this.clipMetadata.canRemix, isRemix = this.clipMetadata.isRemix, optOutVideoCoverHook = this.clipMetadata.optOutVideoCoverHook, ), modelName = this.modelName, playCount = this.playCount, reaction = LocalReactionEntity( clip = this.clipReaction?.clip, flagged = this.clipReaction?.flagged, flaggedReason = this.clipReaction?.flaggedReason, feedbackReason = this.clipReaction?.feedbackReason, playCount = this.clipReaction?.playCount, skipCount = this.clipReaction?.skipCount, reactionType = this.clipReaction?.reactionType, updatedAt = this.clipReaction?.updatedAt, ), status = this.status, title = this.title, upvoteCount = this.upvoteCount, userId = this.userId, videoUrl = this.videoUrl, commentCount = this.commentCount, caption = this.caption, captionMentions = CaptionMentionsEntity( userMentions = this.captionMentions?.userMentions?.map { it.xAsMentionEntity() }, ), downloadDisabledReason = this.downloadDisabledReason, canRemix = this.clipMetadata.canRemix, isRemix = this.clipMetadata.isRemix, remixTask = this.clipMetadata.task, ) // if RemoteClip.audioUrl is not found, returns null fun RemoteClip.xAsSongListDataOrNull(): SongListData? { val clipReaction = when (this.clipReaction?.reactionType) { "L" -> ReactionType.LIKE "D" -> ReactionType.DISLIKE else -> null } return SongListData( id = Id(this.id), createdAt = this.createdAt.xToDate(), status = ClipStatus.fromString(this.status), displayImageUrl = this.imageLargeUrl?.let { Url(it) }, mediaUrl = this.audioUrl?.let { Url(it) } ?: return null, videoCoverUrl = this.videoCoverUrl?.let { Url(it) }, previewUrl = this.previewUrl?.let { Url(it) }, artistName = this.displayName.orEmpty(), handle = this.handle?.let { UserHandle(it) }, artistAvatarUrl = this.avatarImageUrl?.let { Url(it) }, artistUserId = this.userId?.let { Id(it) }, title = this.title, tags = this.clipMetadata.tags, displayTags = this.displayTags, playCount = this.playCount, isPublic = this.isPublic ?: false, modelName = this.modelName, majorModelVersion = this.majorModelVersion, reaction = clipReaction, prompt = this.clipMetadata.prompt, gptPrompt = this.clipMetadata.gptDescriptionPrompt, commentCount = this.commentCount, upvoteCount = this.upvoteCount, caption = this.caption, captionMentions = CaptionMentionsEntity( userMentions = this.captionMentions?.userMentions?.map { it.xAsMentionEntity() }, ), downloadDisabledReason = this.downloadDisabledReason, duration = this.clipMetadata.duration?.toDuration(DurationUnit.SECONDS), canRemix = this.clipMetadata.canRemix, isRemix = this.clipMetadata.isRemix, remixTask = this.clipMetadata.task, optOutVideoCoverHook = this.clipMetadata.optOutVideoCoverHook, modelBadge = this.clipMetadata.modelBadge?.xAsModelBadgeData(), type = when (this.clipMetadata.type) { ClipType.PREVIEW.value -> ClipType.PREVIEW else -> null }, ) } fun RemoteClip.xAsLocalClipData(): LocalClipData { val reaction = when (this.clipReaction?.reactionType) { "L" -> ReactionType.LIKE "D" -> ReactionType.DISLIKE else -> null } return LocalClipData( clipId = Id(this.id), mediaUrl = this.audioUrl.orEmpty().let { Url(it) }, videoCoverUrl = this.videoCoverUrl?.let { Url(it) }, previewUrl = this.previewUrl?.let { Url(it) }, artistName = this.displayName, handle = this.handle?.let { UserHandle(it) }, albumImageUrl = this.imageLargeUrl?.let { Url(it) }, artistAvatarUrl = this.avatarImageUrl?.let { Url(it) }, artistUserId = this.userId?.let { Id(it) }, nowPlayingTitle = this.title, caption = this.caption, captionMentions = this.captionMentions?.let { CaptionMentionsEntity(userMentions = it.userMentions?.map { mention -> mention.xAsMentionEntity() }) }, mediaType = SunoMediaType.AUDIO, modelName = this.modelName, majorModelVersion = this.majorModelVersion, isPublic = this.isPublic ?: false, reaction = reaction, prompt = this.clipMetadata.prompt, gptPrompt = this.clipMetadata.gptDescriptionPrompt, tags = this.clipMetadata.tags, displayTags = this.displayTags, status = ClipStatus.fromString(this.status), commentCount = this.commentCount, upvoteCount = this.upvoteCount, downloadDisabledReason = this.downloadDisabledReason, duration = this.clipMetadata.duration?.toDuration(DurationUnit.SECONDS), playCount = this.playCount, canRemix = this.clipMetadata.canRemix, isRemix = this.clipMetadata.isRemix, remixTask = this.clipMetadata.task, optOutVideoCoverHook = this.clipMetadata.optOutVideoCoverHook, modelBadge = this.clipMetadata.modelBadge?.xAsModelBadgeData(), ) } fun SongListData.xAsLocalClipData(): LocalClipData = LocalClipData( clipId = this.id, mediaUrl = this.mediaUrl, videoCoverUrl = this.videoCoverUrl, previewUrl = this.previewUrl, artistName = this.artistName, handle = this.handle, albumImageUrl = this.displayImageUrl, artistAvatarUrl = this.artistAvatarUrl, artistUserId = this.artistUserId, nowPlayingTitle = this.title, caption = this.caption, captionMentions = this.captionMentions, mediaType = SunoMediaType.AUDIO, isPublic = this.isPublic, reaction = this.reaction, prompt = this.prompt, gptPrompt = this.gptPrompt, tags = this.tags, displayTags = this.displayTags, status = this.status, commentCount = this.commentCount, upvoteCount = this.upvoteCount, playCount = this.playCount, modelName = this.modelName, majorModelVersion = this.majorModelVersion, downloadDisabledReason = this.downloadDisabledReason, duration = this.duration, canRemix = this.canRemix, isRemix = this.isRemix, remixTask = this.remixTask, optOutVideoCoverHook = this.optOutVideoCoverHook, modelBadge = this.modelBadge, type = this.type?.name, ) fun ResultInner.xAsSongListDataOrNull(): SongListData? { return SongListData( id = this.id?.let { Id(it) } ?: return null, createdAt = this.createdAt?.xToDate() ?: Date(), status = ClipStatus.fromString(this.status), displayImageUrl = this.imageLargeUrl?.let { Url(it) }, mediaUrl = this.audioUrl?.let { Url(it) } ?: return null, videoCoverUrl = this.videoCoverUrl?.let { Url(it) }, previewUrl = this.previewUrl?.let { Url(it) }, artistName = this.displayName.orEmpty(), handle = this.handle?.let { UserHandle(it) }, artistUserId = this.userId?.let { Id(it) }, artistAvatarUrl = this.avatarImageUrl?.let { Url(it) }, title = this.title, tags = this.metadata?.tags, displayTags = this.displayTags, playCount = this.playCount, isPublic = this.isPublic ?: false, modelName = this.modelName, majorModelVersion = this.majorModelVersion, reaction = ReactionType.NOTHING, prompt = this.metadata?.prompt, gptPrompt = this.metadata?.gptDescriptionPrompt, commentCount = this.commentCount, upvoteCount = this.upvoteCount, caption = this.caption, captionMentions = CaptionMentionsEntity( userMentions = this.captionMentions?.userMentions?.map { it.xAsMentionEntity() }, ), downloadDisabledReason = this.downloadDisabledReason, duration = this.metadata?.duration?.toDuration(DurationUnit.SECONDS), canRemix = this.metadata?.canRemix, isRemix = this.metadata?.isRemix, remixTask = this.metadata?.task, optOutVideoCoverHook = this.metadata?.optOutVideoCoverHook, modelBadge = this.metadata?.modelBadge?.xAsModelBadgeData(), type = runCatching { ClipType.valueOf(this.metadata?.type!!) }.getOrNull(), ) } // todo add missing fields or refactor clips data models to be more consistent fun LocalClipData.xAsSongListDataOrNull(): SongListData? { return SongListData( id = this.clipId, createdAt = Date(), status = this.status, displayImageUrl = this.albumImageUrl ?: return null, mediaUrl = this.mediaUrl, videoCoverUrl = this.videoCoverUrl, previewUrl = this.previewUrl, artistName = this.artistName, artistUserId = this.artistUserId, handle = this.handle ?: return null, artistAvatarUrl = this.artistAvatarUrl ?: return null, title = this.nowPlayingTitle ?: "", tags = this.tags, displayTags = this.displayTags, playCount = null, isPublic = this.isPublic, modelName = this.modelName, majorModelVersion = this.majorModelVersion, reaction = this.reaction, prompt = this.prompt, gptPrompt = this.gptPrompt, commentCount = null, upvoteCount = null, caption = this.caption, captionMentions = this.captionMentions, downloadDisabledReason = this.downloadDisabledReason, duration = this.duration, canRemix = this.canRemix, isRemix = this.isRemix, remixTask = this.remixTask, optOutVideoCoverHook = this.optOutVideoCoverHook, modelBadge = this.modelBadge, type = this.type?.let { typeString -> ClipType.entries.firstOrNull { it.name.equals(typeString, ignoreCase = true) } }, ) } fun MediaItemRemoteEntity.xAsSongListDataOrNull(): SongListData? { val reaction = when (this.reaction?.reactionType) { "L" -> ReactionType.LIKE "D" -> ReactionType.DISLIKE else -> null } return SongListData( id = this.id?.let { Id(it) } ?: return null, createdAt = this.createdAt?.xToDate() ?: Date(), status = ClipStatus.fromString(this.status), displayImageUrl = this.imageUrl?.let { Url(it) }, mediaUrl = this.audioUrl?.let { Url(it) } ?: return null, videoCoverUrl = this.videoCoverUrl?.let { Url(it) }, previewUrl = this.previewUrl?.let { Url(it) }, artistName = this.displayName.orEmpty(), artistUserId = this.userId?.let { Id(it) }, handle = this.handle?.let { UserHandle(it) }, artistAvatarUrl = this.avatarImageUrl?.let { Url(it) }, title = this.title, tags = this.metadata?.tags, displayTags = this.displayTags, playCount = this.playCount, isPublic = this.isPublic == true, modelName = this.modelName, majorModelVersion = this.majorModelVersion, reaction = reaction, prompt = this.metadata?.prompt, gptPrompt = this.metadata?.gptDescriptionPrompt, commentCount = this.commentCount, upvoteCount = this.upvoteCount, caption = this.caption, captionMentions = CaptionMentionsEntity( userMentions = this.captionMentions?.userMentions?.map { it.xAsMentionEntity() }, ), downloadDisabledReason = this.downloadDisabledReason, duration = this.metadata?.duration?.toDuration(DurationUnit.SECONDS), canRemix = this.metadata?.canRemix, isRemix = this.metadata?.isRemix, remixTask = this.metadata?.task, optOutVideoCoverHook = this.metadata?.optOutVideoCoverHook, type = runCatching { ClipType.valueOf(this.metadata?.type!!) }.getOrNull(), ) } fun GeneratedClipSSRSchema.xAsLocalClipDataOrNull(): LocalClipData? { val reaction = when (reaction?.reactionType) { "L" -> ReactionType.LIKE "D" -> ReactionType.DISLIKE else -> null } return LocalClipData( clipId = Id(id), mediaType = SunoMediaType.AUDIO, mediaUrl = audioUrl?.let { Url(it) } ?: return null, videoCoverUrl = videoCoverUrl?.let { Url(it) }, previewUrl = previewUrl?.let { Url(it) }, artistName = displayName, artistUserId = userId?.let { Id(it) }, handle = handle?.let { UserHandle(it) }, artistAvatarUrl = avatarImageUrl?.let { Url(it) }, nowPlayingTitle = title, albumImageUrl = imageLargeUrl?.let { Url(it) }, caption = caption, captionMentions = captionMentions?.let { CaptionMentionsEntity(userMentions = it.userMentions?.map { mention -> mention.xAsMentionEntity() }) }, isPublic = isPublic ?: false, reaction = reaction, prompt = metadata.prompt, gptPrompt = metadata.gptDescriptionPrompt, tags = metadata.tags, displayTags = displayTags, status = ClipStatus.fromString(status), commentCount = commentCount, upvoteCount = upvoteCount, playCount = playCount, modelName = modelName, majorModelVersion = majorModelVersion, downloadDisabledReason = downloadDisabledReason, duration = metadata.duration?.toDuration(DurationUnit.SECONDS), allowComments = allowComments, canRemix = metadata.canRemix, isRemix = metadata.isRemix, remixTask = metadata.task, optOutVideoCoverHook = metadata.optOutVideoCoverHook, type = metadata.type, modelBadge = metadata.modelBadge?.xAsModelBadgeData(), ) }