fix(13-11): ShareModal's MIME map stops filing m4a/aac/opus/wma as Documents

Adds the four missing audio extensions to ShareModal.vue's extension-to-
MIME map (m4a->audio/mp4, aac->audio/aac, opus->audio/opus, wma->audio/
x-ms-wma), extracted to an exported module-scope SHARE_MIME_MAP so it's
directly fixture-testable (useFileType.test.ts convention). All three
maps agree that these eight extensions are audio/*: SHARE_MIME_MAP,
archyContentAdapter.ts's classifyByMime (13-06), and content.rs's
auto-filing check, which is prefix-only (mime_type.starts_with("audio/"))
so any correct audio/* value here already satisfies it. Existing four
entries (mp3/flac/ogg/wav) and the generic-fallback behavior for unknown
extensions are unchanged. Whole neode-ui suite green (924/924).
This commit is contained in:
archipelago
2026-08-05 18:20:51 -04:00
parent 7bea8f6ba4
commit abe77ebe7e
2 changed files with 125 additions and 7 deletions
+52 -7
View File
@@ -161,6 +161,57 @@
</Teleport>
</template>
<script lang="ts">
/**
* Extension → MIME map used when sharing a file that isn't already in the
* catalog (`save()` below, `content.add`). Historically listed exactly four
* audio extensions (`mp3`/`flac`/`ogg`/`wav`) and left `m4a`/`aac`/`opus`/
* `wma` to fall through to the generic `application/octet-stream` fallback
* those four then never routed to the global audio player (which decides
* purely on `mime.startsWith('audio/')`, see `usePaidItemViewer.ts`) and
* auto-filed to Documents instead of Music (`content.rs`'s paid-download
* auto-filing, same `starts_with("audio/")` check) — 13-CONTEXT.md's
* landmine, T-13-72. This fix adds the four missing entries; it does not
* change the fallback strategy and does not guess at unknown extensions.
*
* Kept in sync with two other maps that must agree on every audio
* extension (13-11 Task 2's own acceptance criterion):
* - `archyContentAdapter.ts`'s `classifyByMime`/`AUDIO_EXT_FALLBACK` (13-06)
* - `content.rs`'s auto-filing check (`mime_type.starts_with("audio/")`) —
* that check is prefix-only, so any correct `audio/*` value here already
* agrees with it; the specific `audio/*` strings below are chosen to
* match `classifyByMime`'s own test fixtures exactly, so a byte-for-byte
* MIME string never diverges between the two even though the node-side
* check itself only cares about the prefix.
*
* Module-scope (not a local const inside `save()`) and exported so it is a
* table `archyContentAdapter.test.ts`-style fixture tests can pin directly,
* per this task's own `read_first` guidance to follow
* `useFileType.test.ts`'s fixture-table convention.
*/
export const SHARE_MIME_MAP: Record<string, string> = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
mp4: 'video/mp4',
webm: 'video/webm',
mkv: 'video/x-matroska',
mp3: 'audio/mpeg',
flac: 'audio/flac',
ogg: 'audio/ogg',
wav: 'audio/wav',
m4a: 'audio/mp4',
aac: 'audio/aac',
opus: 'audio/opus',
wma: 'audio/x-ms-wma',
pdf: 'application/pdf',
zip: 'application/zip',
txt: 'text/plain',
}
</script>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { rpcClient } from '@/api/rpc-client'
@@ -352,17 +403,11 @@ async function save() {
// Add if not in catalog
if (!itemId) {
const ext = props.filename.split('.').pop()?.toLowerCase() || ''
const mimeMap: Record<string, string> = {
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
webp: 'image/webp', mp4: 'video/mp4', webm: 'video/webm', mkv: 'video/x-matroska',
mp3: 'audio/mpeg', flac: 'audio/flac', ogg: 'audio/ogg', wav: 'audio/wav',
pdf: 'application/pdf', zip: 'application/zip', txt: 'text/plain',
}
const addRes = await rpcClient.call<{ item: { id: string } }>({
method: 'content.add',
params: {
filename: (props.filepath || props.filename).replace(/^\/+/, ''),
mime_type: mimeMap[ext] || 'application/octet-stream',
mime_type: SHARE_MIME_MAP[ext] || 'application/octet-stream',
description: '',
},
})
@@ -0,0 +1,73 @@
import { describe, it, expect } from 'vitest'
import { SHARE_MIME_MAP } from '../ShareModal.vue'
// 13-11 Task 2: ShareModal.vue's extension-to-MIME map historically listed
// exactly four audio extensions (mp3/flac/ogg/wav) and left
// m4a/aac/opus/wma to fall through to `application/octet-stream` — those
// four never routed to the global audio player and auto-filed to Documents
// instead of Music. This pins all eight audio extensions plus the existing
// non-audio entries, following useFileType.test.ts's fixture-table
// convention.
describe('ShareModal SHARE_MIME_MAP', () => {
it.each([
['mp3', 'audio/mpeg'],
['flac', 'audio/flac'],
['ogg', 'audio/ogg'],
['wav', 'audio/wav'],
['m4a', 'audio/mp4'],
['aac', 'audio/aac'],
['opus', 'audio/opus'],
['wma', 'audio/x-ms-wma'],
])('maps .%s to the audio MIME %s', (ext, expected) => {
expect(SHARE_MIME_MAP[ext]).toBe(expected)
})
it('every one of the eight audio extensions maps to a real audio/* MIME (not application/octet-stream)', () => {
const audioExts = ['mp3', 'flac', 'ogg', 'wav', 'm4a', 'aac', 'opus', 'wma']
for (const ext of audioExts) {
const mime = SHARE_MIME_MAP[ext]
expect(mime).toBeDefined()
expect(mime!.startsWith('audio/')).toBe(true)
}
})
it('existing non-audio entries are unchanged', () => {
expect(SHARE_MIME_MAP.jpg).toBe('image/jpeg')
expect(SHARE_MIME_MAP.mp4).toBe('video/mp4')
expect(SHARE_MIME_MAP.pdf).toBe('application/pdf')
expect(SHARE_MIME_MAP.zip).toBe('application/zip')
expect(SHARE_MIME_MAP.txt).toBe('text/plain')
})
it('an unknown extension is absent from the map — the fix adds coverage, it does not guess', () => {
expect(SHARE_MIME_MAP.xyz123).toBeUndefined()
})
it('agrees with archyContentAdapter.ts\'s classifyByMime on all eight audio extensions', async () => {
const { classifyByMime } = await import('../../../composables/archyContentAdapter')
const audioExts = ['mp3', 'flac', 'ogg', 'wav', 'm4a', 'aac', 'opus', 'wma']
for (const ext of audioExts) {
const mime = SHARE_MIME_MAP[ext]!
expect(classifyByMime({ mime_type: mime, filename: `track.${ext}` })).toBe('audio')
}
})
})
describe('an audio MIME never opens the lightbox', () => {
it('usePaidItemViewer routes every audio/* mime to the global player before its image/video lightbox branch is ever reached', () => {
// usePaidItemViewer.ts's routing order is: audio -> global bottom-bar
// player (return); image/video -> lightbox (return); anything else ->
// browser-tab fallback. The audio branch is checked FIRST and always
// returns, so no audio/* mime can ever reach the lightbox branch —
// pinned here structurally (mirrors the real predicate, avoids a heavy
// component mount) rather than re-implementing usePaidItemViewer's
// async RPC/blob flow in a unit test.
const audioExts = ['mp3', 'flac', 'ogg', 'wav', 'm4a', 'aac', 'opus', 'wma']
for (const ext of audioExts) {
const mime = SHARE_MIME_MAP[ext]!
const routesToAudioPlayerFirst = mime.startsWith('audio/')
expect(routesToAudioPlayerFirst).toBe(true)
}
})
})