feat: add ICY metadata parser with artist/title extraction

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-10 01:18:51 -04:00
parent bb14a6af53
commit 45a946f829
3 changed files with 368 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
package xyz.cottongin.radio247.audio
data class IcyMetadata(
val raw: String,
val title: String?,
val artist: String?
)

View File

@@ -0,0 +1,91 @@
package xyz.cottongin.radio247.audio
import java.io.InputStream
class IcyParser(
private val input: InputStream,
private val metaint: Int?,
private val onAudioData: (ByteArray, Int, Int) -> Unit,
private val onMetadata: (IcyMetadata) -> Unit
) {
private val buffer = ByteArray(8192)
fun readAll() {
if (metaint == null) {
passthrough()
} else {
parseWithMetadata(metaint)
}
}
private fun passthrough() {
while (true) {
val n = input.read(buffer)
if (n <= 0) break
onAudioData(buffer, 0, n)
}
}
private fun parseWithMetadata(metaint: Int) {
val audioBuf = ByteArray(metaint)
while (true) {
val audioRead = readFully(audioBuf, 0, metaint)
if (audioRead <= 0) break
if (audioRead < metaint) {
onAudioData(audioBuf, 0, audioRead)
break
}
onAudioData(audioBuf, 0, metaint)
val lengthByte = input.read()
if (lengthByte < 0) break
val metadataSize = (lengthByte and 0xFF) * 16
if (metadataSize == 0) continue
val metaBytes = ByteArray(metadataSize)
val metaRead = readFully(metaBytes, 0, metadataSize)
if (metaRead < metadataSize) break
val raw = String(metaBytes, Charsets.UTF_8).trimEnd { it == '\u0000' }
val parsed = parseMetaString(raw)
onMetadata(parsed)
}
}
private fun readFully(buf: ByteArray, off: Int, len: Int): Int {
var total = 0
while (total < len) {
val n = input.read(buf, off + total, len - total)
if (n <= 0) return total
total += n
}
return total
}
private fun parseMetaString(raw: String): IcyMetadata {
val streamTitle = extractStreamTitle(raw)
if (streamTitle != null) {
val separatorIndex = streamTitle.indexOf(" - ")
if (separatorIndex >= 0) {
return IcyMetadata(
raw = raw,
artist = streamTitle.substring(0, separatorIndex).trim(),
title = streamTitle.substring(separatorIndex + 3).trim()
)
}
return IcyMetadata(raw = raw, title = streamTitle.trim(), artist = null)
}
return IcyMetadata(raw = raw, title = null, artist = null)
}
private fun extractStreamTitle(meta: String): String? {
val key = "StreamTitle='"
val start = meta.indexOf(key)
if (start < 0) return null
val valueStart = start + key.length
val end = meta.indexOf("';", valueStart)
if (end < 0) return null
return meta.substring(valueStart, end)
}
}