EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

DidlItem.kt

package dev.stave.domain.model

import kotlin.time.Duration

/**
 * A single entry returned by a ContentDirectory Browse() call.
 * Either a Container (navigable) or a Track (playable) — images/playlists come later.
 *
 * The [extras] map preserves non-standard properties MinimServer exposes via
 * upnp:userAnnotation — Work, Group, DiscSubtitle, etc. — so the UI can render them
 * without the parser having to know the full list ahead of time.
 */
sealed class DidlItem {
    abstract val id: String
    abstract val parentId: String
    abstract val title: String
    abstract val extras: Map<String, String>

    data class Container(
        override val id: String,
        override val parentId: String,
        override val title: String,
        val childCount: Int?,
        val albumArtUri: String?,
        val upnpClass: String,
        /** Album artist / creator for album containers — null for generic folders. */
        val artist: String?,
        /** True for MinimServer's synthetic entries: `>> Show All`, `[folder view]`, disc headings. */
        val isSynthetic: Boolean,
        override val extras: Map<String, String> = emptyMap(),
    ) : DidlItem() {
        val isAlbum: Boolean get() = upnpClass.contains("musicAlbum", ignoreCase = true)

        /** True for genre containers (`object.container.genre.musicGenre` and
         *  friends). Gates the "Play 42 random <Genre> tracks" context action;
         *  mirrors the Android `DidlItem.Container.isGenre` helper. */
        val isGenre: Boolean get() = upnpClass.contains("genre", ignoreCase = true)
    }

    data class Track(
        override val id: String,
        override val parentId: String,
        override val title: String,
        val artist: String?,
        val albumArtist: String?,
        val album: String?,
        val composer: String?,
        val genre: String?,
        val date: String?,
        val trackNumber: Int?,
        val discNumber: Int?,
        val duration: Duration?,
        val albumArtUri: String?,
        val resourceUrl: String,
        val mimeType: String?,
        val sampleFrequency: Int?,
        val bitsPerSample: Int?,
        val channels: Int?,
        val bitrate: Int?,
        val size: Long?,
        override val extras: Map<String, String> = emptyMap(),
    ) : DidlItem() {
        val isLossless: Boolean get() = mimeType?.let {
            it.contains("flac") || it.contains("wav") || it.contains("alac") || it.contains("aiff")
        } ?: false
    }
}