/* * Copyright (c) Double Symmetry GmbH * Commercial use requires a license. See https://rntp.dev/pricing */ package com.doublesymmetry.trackplayer.models import android.content.Context import android.net.Uri import android.os.Bundle import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import com.facebook.react.bridge.ReadableArray import com.doublesymmetry.trackplayer.TrackPlayerModule.Companion.PLAYER_PREFS_NAME import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @Serializable data class BrowseCategory( val mediaId: String, val title: String, val items: List, ) @Serializable data class BrowseMediaItem( val mediaId: String, val url: String? = null, val title: String? = null, val artist: String? = null, val albumTitle: String? = null, val artworkUrl: String? = null, val duration: Double? = null, val isLive: Boolean? = null, val extras: JsonObject? = null, val children: List? = null, ) { val isPlayable: Boolean get() = url != null val isBrowsable: Boolean get() = children != null && url == null fun toMediaItem(context: Context? = null): MediaItem { val metadataExtras = Bundle() duration?.let { metadataExtras.putDouble("duration", it) } isLive?.let { metadataExtras.putBoolean("isLive", it) } extras?.let { MediaItemExtras.attachToMetadataExtras(metadataExtras, MediaItemExtras.jsonToBundle(it)) } val resolvedArtwork = artworkUrl?.let { aw -> context?.let { TrackPlayerMediaItem.resolveAssetUrl(it, aw) } ?: aw } val resolvedUrl = url?.let { u -> context?.let { TrackPlayerMediaItem.resolveAssetUrl(it, u) } ?: u } val metadata = MediaMetadata.Builder() .setTitle(title) .setArtist(artist) .setAlbumTitle(albumTitle) .setArtworkUri(resolvedArtwork?.let { Uri.parse(it) }) .setIsBrowsable(isBrowsable) .setIsPlayable(isPlayable) .setMediaType(if (isBrowsable) MediaMetadata.MEDIA_TYPE_FOLDER_MIXED else MediaMetadata.MEDIA_TYPE_MUSIC) .setExtras(metadataExtras) .build() val builder = MediaItem.Builder() .setMediaId(mediaId) .setMediaMetadata(metadata) if (resolvedUrl != null) { builder.setUri(resolvedUrl) } if (isLive == true) { builder.setLiveConfiguration(MediaItem.LiveConfiguration.Builder().build()) } return builder.build() } } @Serializable data class BrowseTree( val categories: List = emptyList(), ) { fun store(context: Context) { val prefs = context.getSharedPreferences(PLAYER_PREFS_NAME, Context.MODE_PRIVATE) prefs.edit().putString(BROWSE_TREE_KEY, Json.encodeToString(this)).apply() } fun findCategory(mediaId: String): BrowseCategory? = categories.find { it.mediaId == mediaId } fun findItem(mediaId: String): BrowseMediaItem? = categories.flatMap { findItemRecursive(it.items, mediaId) }.firstOrNull() fun findPlayableSiblings(mediaId: String): Pair, Int>? { for (category in categories) { findPlayableSiblingsInList(category.items, mediaId)?.let { return it } } return null } companion object { private const val BROWSE_TREE_KEY = "browse_tree" fun load(context: Context): BrowseTree { val prefs = context.getSharedPreferences(PLAYER_PREFS_NAME, Context.MODE_PRIVATE) val json = prefs.getString(BROWSE_TREE_KEY, null) ?: return BrowseTree() return try { Json.decodeFromString(json) } catch (_: Exception) { BrowseTree() } } fun fromReadableArray(data: ReadableArray): BrowseTree { val categories = mutableListOf() for (i in 0 until data.size()) { val catMap = data.getMap(i) ?: continue val mediaId = catMap.getString("mediaId") ?: continue val title = catMap.getString("title") ?: continue val itemsArray = catMap.getArray("items") ?: continue val items = parseItems(itemsArray) categories.add(BrowseCategory(mediaId = mediaId, title = title, items = items)) } return BrowseTree(categories) } private fun parseItems(data: ReadableArray): List { val items = mutableListOf() for (i in 0 until data.size()) { val itemMap = data.getMap(i) ?: continue val mediaId = itemMap.getString("mediaId") ?: continue val children = if (itemMap.hasKey("children")) { itemMap.getArray("children")?.let { parseItems(it) } } else null items.add(BrowseMediaItem( mediaId = mediaId, url = if (itemMap.hasKey("url")) itemMap.getString("url") else null, title = if (itemMap.hasKey("title")) itemMap.getString("title") else null, artist = if (itemMap.hasKey("artist")) itemMap.getString("artist") else null, albumTitle = if (itemMap.hasKey("albumTitle")) itemMap.getString("albumTitle") else null, artworkUrl = if (itemMap.hasKey("artworkUrl")) itemMap.getString("artworkUrl") else null, duration = if (itemMap.hasKey("duration")) itemMap.getDouble("duration") else null, isLive = if (itemMap.hasKey("isLive")) itemMap.getBoolean("isLive") else null, extras = MediaItemExtras.parseJson(itemMap), children = children, )) } return items } private fun findItemRecursive(items: List, mediaId: String): List { for (item in items) { if (item.mediaId == mediaId) return listOf(item) val children = item.children if (children != null) { val found = findItemRecursive(children, mediaId) if (found.isNotEmpty()) return found } } return emptyList() } private fun findPlayableSiblingsInList( items: List, mediaId: String, ): Pair, Int>? { val playableSiblings = items.filter { it.isPlayable } val startIndex = playableSiblings.indexOfFirst { it.mediaId == mediaId } if (startIndex >= 0) { return playableSiblings to startIndex } for (item in items) { val children = item.children ?: continue findPlayableSiblingsInList(children, mediaId)?.let { return it } } return null } } }