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

GenreParser.kt

package dev.stave.domain.model

/**
 * Parse a genre string into individual genre tokens.
 *
 * UPnP/DIDL servers emit multi-genre tags with inconsistent separators across
 * implementations and across the source files' own metadata conventions:
 *   MinimServer          "Rock; Pop"
 *   iTunes / ID3v2.4     "Rock/Pop" or "Rock\Pop"
 *   Foobar2000           "Rock // Pop"
 *   Plex / BubbleUPnP    "Rock, Pop"
 *   Doubled variants     "Rock\\Pop"   (escaped-backslash or literal double-backslash)
 *
 * We collapse `//` and `\\` to single delimiters first, then split on any of
 * `,`, `;`, `/`, `\`. Tokens are trimmed; blanks are discarded. Order preserved.
 *
 * Returns an empty list for null/blank input. Doesn't lowercase — callers that need
 * case-insensitive comparison should do that themselves.
 */
fun parseGenres(raw: String?): List<String> {
    if (raw.isNullOrBlank()) return emptyList()
    // Collapse doubled separators first. Without this, "Rock // Pop" split on `/` would
    // yield ["Rock ", " ", " Pop"], and the middle empty token would pass through trim()
    // as "" and get filtered out — slow but correct. Doing this first is cheaper and
    // also handles the literal-double-backslash case "Rock\\Pop" which would otherwise
    // split to ["Rock", "", "Pop"].
    val normalized = raw
        .replace("//", "/")
        .replace("\\\\", "\\")
    return normalized
        .split(',', ';', '/', '\\')
        .map { it.trim() }
        .filter { it.isNotEmpty() }
}