GenreParser.kt
Each line, with the release that last changed it.
| 0.5.0 | 1 | package dev.stave.domain.model |
| 2 | ||
| 3 | /** | |
| 4 | * Parse a genre string into individual genre tokens. | |
| 5 | * | |
| 6 | * UPnP/DIDL servers emit multi-genre tags with inconsistent separators across | |
| 7 | * implementations and across the source files' own metadata conventions: | |
| 8 | * MinimServer "Rock; Pop" | |
| 9 | * iTunes / ID3v2.4 "Rock/Pop" or "Rock\Pop" | |
| 10 | * Foobar2000 "Rock // Pop" | |
| 11 | * Plex / BubbleUPnP "Rock, Pop" | |
| 12 | * Doubled variants "Rock\\Pop" (escaped-backslash or literal double-backslash) | |
| 13 | * | |
| 14 | * We collapse `//` and `\\` to single delimiters first, then split on any of | |
| 15 | * `,`, `;`, `/`, `\`. Tokens are trimmed; blanks are discarded. Order preserved. | |
| 16 | * | |
| 17 | * Returns an empty list for null/blank input. Doesn't lowercase — callers that need | |
| 18 | * case-insensitive comparison should do that themselves. | |
| 19 | */ | |
| 20 | fun parseGenres(raw: String?): List<String> { | |
| 21 | if (raw.isNullOrBlank()) return emptyList() | |
| 22 | // Collapse doubled separators first. Without this, "Rock // Pop" split on `/` would | |
| 23 | // yield ["Rock ", " ", " Pop"], and the middle empty token would pass through trim() | |
| 24 | // as "" and get filtered out — slow but correct. Doing this first is cheaper and | |
| 25 | // also handles the literal-double-backslash case "Rock\\Pop" which would otherwise | |
| 26 | // split to ["Rock", "", "Pop"]. | |
| 27 | val normalized = raw | |
| 28 | .replace("//", "/") | |
| 29 | .replace("\\\\", "\\") | |
| 30 | return normalized | |
| 31 | .split(',', ';', '/', '\\') | |
| 32 | .map { it.trim() } | |
| 33 | .filter { it.isNotEmpty() } | |
| 34 | } |


