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:
@@ -161,6 +161,57 @@
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</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">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
@@ -352,17 +403,11 @@ async function save() {
|
|||||||
// Add if not in catalog
|
// Add if not in catalog
|
||||||
if (!itemId) {
|
if (!itemId) {
|
||||||
const ext = props.filename.split('.').pop()?.toLowerCase() || ''
|
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 } }>({
|
const addRes = await rpcClient.call<{ item: { id: string } }>({
|
||||||
method: 'content.add',
|
method: 'content.add',
|
||||||
params: {
|
params: {
|
||||||
filename: (props.filepath || props.filename).replace(/^\/+/, ''),
|
filename: (props.filepath || props.filename).replace(/^\/+/, ''),
|
||||||
mime_type: mimeMap[ext] || 'application/octet-stream',
|
mime_type: SHARE_MIME_MAP[ext] || 'application/octet-stream',
|
||||||
description: '',
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user