refactor(chat): unify media types and viewer (#1328, #1329) (#1353)

This commit is contained in:
m57 authored and GitHub committed 2026-09-28 12:25:57 +03:00
1 parent af0c508c1f
commit 15bc0e21c7
4 files changed
+64 -64

No files matched your search

@@ -114,15 +114,12 @@ object ImageBytesResolver {
}.getOrElse { e -> Result.Error(e.message ?: "Could not download image") }
}
/** Image-only view of the shared MIME table: anything that isn't `image/` saves as `.img`. */
fun extensionForMime(mime: String): String =
when (mime.substringBefore(';').trim().lowercase()) {
"image/jpeg", "image/jpg" -> "jpg"
"image/png" -> "png"
"image/gif" -> "gif"
"image/webp" -> "webp"
"image/bmp" -> "bmp"
"image/heic", "image/heif" -> "heic"
"image/svg+xml" -> "svg"
else -> "img"
if (mime.trim().startsWith("image/", ignoreCase = true)) {
com.m57.hermescontrol.ui.chat
.extensionForMime(mime)
} else {
"img"
}
}
@@ -140,67 +140,41 @@ fun normalizeFrameworkMime(mime: String?): String =
?.takeIf { it.isNotBlank() }
?: "application/octet-stream"
/**
* MIME aliases and preferred extensions that [MEDIA_BY_EXT] can't express
* (it's keyed by extension, so `image/jpeg` would otherwise resolve to its
* first entry, `jpeg`). HEIC is save-only here: it isn't classified as inline media.
*/
private val EXT_BY_MIME_ALIAS: Map<String, String> =
mapOf(
"image/jpeg" to "jpg",
"image/jpg" to "jpg",
"image/heic" to "heic",
"image/heif" to "heic",
"audio/mp3" to "mp3",
"audio/x-m4a" to "m4a",
"audio/x-wav" to "wav",
"audio/wave" to "wav",
"application/x-flac" to "flac",
"application/ogg" to "ogg",
)
/** Returns the canonical file extension for a given MIME type. */
fun extensionForMime(mime: String): String {
val clean = mime.substringBefore(';').trim().lowercase()
for ((ext, pair) in MEDIA_BY_EXT) {
if (pair.second
EXT_BY_MIME_ALIAS[clean]?.let { return it }
MEDIA_BY_EXT.entries
.firstOrNull { (_, pair) ->
pair.second
.substringBefore(';')
.trim()
.lowercase() == clean
) {
return ext
}
}
return when (clean) {
"image/jpeg", "image/jpg" -> {
"jpg"
}
"video/mp4" -> {
"mp4"
}
"video/x-matroska" -> {
"mkv"
}
"video/quicktime" -> {
"mov"
}
"audio/mpeg", "audio/mp3" -> {
"mp3"
}
"audio/mp4", "audio/x-m4a" -> {
"m4a"
}
"audio/wav", "audio/x-wav", "audio/wave" -> {
"wav"
}
"audio/flac", "application/x-flac" -> {
"flac"
}
"audio/ogg", "application/ogg" -> {
"ogg"
}
"audio/aac" -> {
"aac"
}
else -> {
when {
clean.startsWith("video/") -> "mp4"
clean.startsWith("audio/") -> "mp3"
clean.startsWith("image/") -> "img"
else -> "bin"
}
}
}?.let { return it.key }
return when {
clean.startsWith("video/") -> "mp4"
clean.startsWith("audio/") -> "mp3"
clean.startsWith("image/") -> "img"
else -> "bin"
}
}
@@ -35,6 +35,11 @@ class ImageBytesResolverTest {
assertEquals("heic", ImageBytesResolver.extensionForMime("image/heic"))
assertEquals("svg", ImageBytesResolver.extensionForMime("image/svg+xml"))
assertEquals("img", ImageBytesResolver.extensionForMime("image/unknown"))
assertEquals("heic", ImageBytesResolver.extensionForMime("image/heif"))
assertEquals("bmp", ImageBytesResolver.extensionForMime("image/bmp"))
// Non-image content types stay `.img`, not `.bin`/`.pdf`.
assertEquals("img", ImageBytesResolver.extensionForMime("application/pdf"))
assertEquals("img", ImageBytesResolver.extensionForMime("text/html"))
}
@Test
@@ -30,6 +30,30 @@ class MediaTypesTest {
assertEquals("application/octet-stream", mediaMimeForPath("/tmp/x.weird"))
}
@Test
fun `extension for mime uses one table plus aliases`() {
// Table entries, including params and case.
assertEquals("png", extensionForMime("image/png"))
assertEquals("png", extensionForMime(" IMAGE/PNG; charset=binary"))
assertEquals("ogg", extensionForMime("audio/ogg; codecs=opus"))
assertEquals("mkv", extensionForMime("video/x-matroska"))
assertEquals("pdf", extensionForMime("application/pdf"))
// Aliases win over first-table-match.
assertEquals("jpg", extensionForMime("image/jpeg"))
assertEquals("jpg", extensionForMime("image/jpg"))
assertEquals("heic", extensionForMime("image/heif"))
assertEquals("mp3", extensionForMime("audio/mp3"))
assertEquals("m4a", extensionForMime("audio/x-m4a"))
assertEquals("wav", extensionForMime("audio/wave"))
assertEquals("flac", extensionForMime("application/x-flac"))
assertEquals("ogg", extensionForMime("application/ogg"))
// Family fallbacks.
assertEquals("mp4", extensionForMime("video/unknown"))
assertEquals("mp3", extensionForMime("audio/unknown"))
assertEquals("img", extensionForMime("image/unknown"))
assertEquals("bin", extensionForMime("application/unknown"))
}
@Test
fun `name extraction handles urls and paths`() {
assertEquals("report.pdf", mediaNameFromPath("https://gw/api/files/download?path=%2Ftmp%2Freport.pdf"))