diff --git a/src/components/artwork/audio-metadata/worker.js b/src/components/artwork/audio-metadata/worker.js index 15a50888..f2bc026f 100644 --- a/src/components/artwork/audio-metadata/worker.js +++ b/src/components/artwork/audio-metadata/worker.js @@ -1,3 +1,4 @@ +import * as URI from "fast-uri"; import { musicMetadataTags } from "~/components/metadata/common.js"; import { ostiary, rpc, workerProxy } from "~/common/worker.js"; @@ -29,7 +30,12 @@ export async function get({ data: track, ports }) { ? undefined : await input.resolve({ method: "HEAD", uri: track.uri }); + const trackUri = URI.parse(track.uri); + const trackPathParts = trackUri.path?.split("/"); + const filename = trackPathParts?.[trackPathParts.length - 1]; + const meta = await musicMetadataTags({ + filename, includeArtwork: true, stream: "stream" in resGet ? resGet.stream : undefined, mimeType: "stream" in resGet ? resGet.mimeType : undefined, diff --git a/src/components/metadata/audio-file/worker.js b/src/components/metadata/audio-file/worker.js index db3a262b..4bebda61 100644 --- a/src/components/metadata/audio-file/worker.js +++ b/src/components/metadata/audio-file/worker.js @@ -1,3 +1,4 @@ +import * as URI from "fast-uri"; import { ostiary, rpc, workerProxy } from "~/common/worker.js"; import { musicMetadataTags } from "~/components/metadata/common.js"; @@ -29,7 +30,12 @@ export async function patch({ data: track, ports }) { ? undefined : await input.resolve({ method: "HEAD", uri: track.uri }); + const trackUri = URI.parse(track.uri); + const trackPathParts = trackUri.path?.split("/"); + const filename = trackPathParts?.[trackPathParts.length - 1]; + const { stats, tags } = await musicMetadataTags({ + filename, stream: "stream" in resGet ? resGet.stream : undefined, mimeType: "stream" in resGet ? resGet.mimeType : undefined, urls: "url" in resGet diff --git a/src/components/metadata/common.js b/src/components/metadata/common.js index 3ff247dd..8fb5f6e4 100644 --- a/src/components/metadata/common.js +++ b/src/components/metadata/common.js @@ -27,6 +27,7 @@ const MIME_TO_EXT = { "audio/aacp": "aacp", "audio/mp4": "m4a", "audio/m4a": "m4a", + "audio/x-m4a": "m4a", "audio/ogg": "ogg", "audio/opus": "opus", "audio/speex": "spx", @@ -49,24 +50,34 @@ const MIME_TO_EXT = { }; /** - * @param {{ includeArtwork?: boolean; mimeType?: string; stream?: ReadableStream; urls?: Urls; }} _ + * @param {string | undefined} mimeType + * @returns {string | undefined} + */ +function mimeTypeToPath(mimeType) { + const ext = mimeType && MIME_TO_EXT[mimeType]; + return ext ? `file.${ext}` : undefined; +} + +/** + * @param {{ includeArtwork?: boolean; filename?: string; mimeType?: string; stream?: ReadableStream; urls?: Urls; }} _ * @returns {Promise} */ export async function musicMetadataTags({ includeArtwork, + filename, mimeType, stream, urls, }) { const uri = urls ? URI.parse(urls.get) : undefined; const pathParts = uri?.path?.split("/"); - const filename = pathParts?.[pathParts.length - 1]; + const urlFilename = pathParts?.[pathParts.length - 1]; let meta; if (urls?.get.startsWith("blob:")) { const blob = await fetch(urls.get).then((r) => r.blob()); - // Blob URLs carry no filename, so `filename` above is just the blob's UUID. + // Blob URLs carry no filename, so the URL path is just the blob's UUID. // Without a path or a recognised MIME-type music-metadata falls back to // content-sniffing, whose content-type matcher is broken in the browser // bundle (the `content-type` CJS interop leaves `default` undefined, so @@ -105,9 +116,14 @@ export async function musicMetadataTags({ // has size/mimeType — no path. Without a path or a recognised MIME-type, // music-metadata falls back to content-sniffing, which rejects some files // (e.g. `audio/x-m4a`, or servers returning `application/octet-stream`). - // Provide the filename so it can pick a parser by extension instead. - if (filename) { - tokenizer.fileInfo = { ...tokenizer.fileInfo, path: filename }; + // Provide the filename so it can pick a parser by extension instead. Fall + // back to the URL path, and then to a synthetic filename derived from the + // HEAD response MIME type (useful for temporary links like Dropbox's that + // don't preserve the original filename). + const path = filename || urlFilename || + mimeTypeToPath(tokenizer.fileInfo?.mimeType); + if (path) { + tokenizer.fileInfo = { ...tokenizer.fileInfo, path }; } meta = await parseFromTokenizer(tokenizer, { skipCovers: !includeArtwork }); } else if (stream) {