diff --git a/package-lock.json b/package-lock.json index 5d1d31d..559235c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "fuse.js": "^7.0.0", "i18n-js": "^4.3.2", "lodash": "^4.17.21", - "mime-db": "^1.53.0", + "mime": "^4.0.6", "react": "^18.2.0", "react-airplay": "^1.2.0", "react-native": "0.74.3", @@ -59,7 +59,6 @@ "@sentry/react-native": "^5.26.0", "@types/i18n-js": "^3.8.9", "@types/lodash": "^4.14.202", - "@types/mime-db": "^1.43.5", "@types/node": "^20.11.17", "@types/react": "^18.2.55", "@typescript-eslint/eslint-plugin": "^6.21.0", @@ -2158,6 +2157,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@react-native-community/cli-tools/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/@react-native-community/cli-tools/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3785,13 +3796,6 @@ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", "dev": true }, - "node_modules/@types/mime-db": { - "version": "1.43.5", - "resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.5.tgz", - "integrity": "sha512-/bfTiIUTNPUBnwnYvUxXAre5MhD88jgagLEQiQtIASjU+bwxd8kS/ASDA4a8ufd8m0Lheu6eeMJHEUpLHoJ28A==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "20.11.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", @@ -8414,14 +8418,18 @@ } }, "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.6.tgz", + "integrity": "sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4.0.0" + "node": ">=16" } }, "node_modules/mime-db": { diff --git a/package.json b/package.json index 7390b1f..a611495 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "fuse.js": "^7.0.0", "i18n-js": "^4.3.2", "lodash": "^4.17.21", - "mime-db": "^1.53.0", + "mime": "^4.0.6", "react": "^18.2.0", "react-airplay": "^1.2.0", "react-native": "0.74.3", @@ -62,7 +62,6 @@ "@sentry/react-native": "^5.26.0", "@types/i18n-js": "^3.8.9", "@types/lodash": "^4.14.202", - "@types/mime-db": "^1.43.5", "@types/node": "^20.11.17", "@types/react": "^18.2.55", "@typescript-eslint/eslint-plugin": "^6.21.0", diff --git a/src/store/downloads/actions.ts b/src/store/downloads/actions.ts index f69995f..89893d3 100644 --- a/src/store/downloads/actions.ts +++ b/src/store/downloads/actions.ts @@ -10,7 +10,7 @@ import { getExtensionForUrl } from '@/utility/mimeType'; export const downloadAdapter = createEntityAdapter(); export const queueTrackForDownload = createAction('download/queue'); -export const initializeDownload = createAction<{ id: string, size?: number, jobId?: number, location: string, image: string }>('download/initialize'); +export const initializeDownload = createAction<{ id: string, size?: number, jobId?: number, location: string, image?: string }>('download/initialize'); export const progressDownload = createAction<{ id: string, progress: number, jobId?: number }>('download/progress'); export const completeDownload = createAction<{ id: string, location: string, size?: number, image?: string }>('download/complete'); export const failDownload = createAction<{ id: string }>('download/fail'); @@ -25,12 +25,13 @@ export const downloadTrack = createAsyncThunk( // Get the content-type from the URL by doing a HEAD-only request const [audioExt, imageExt] = await Promise.all([ getExtensionForUrl(audioUrl), - getExtensionForUrl(imageUrl) + // Image files may be absent + getExtensionForUrl(imageUrl).catch(() => null) ]); // Then generate the proper location const audioLocation = `${DocumentDirectoryPath}/${id}.${audioExt}`; - const imageLocation = `${DocumentDirectoryPath}/${id}.${imageExt}`; + const imageLocation = imageExt ? `${DocumentDirectoryPath}/${id}.${imageExt}` : undefined; // Actually kick off the download const { promise: audioPromise } = downloadFile({ @@ -48,24 +49,26 @@ export const downloadTrack = createAsyncThunk( toFile: audioLocation, }); - const { promise: imagePromise } = downloadFile({ - fromUrl: imageUrl, - toFile: imageLocation, - background: true, - }); + const { promise: imagePromise } = imageExt && imageLocation + ? downloadFile({ + fromUrl: imageUrl, + toFile: imageLocation, + background: true, + }) + : { promise: Promise.resolve(null) }; // Await job completion const [audioResult, imageResult] = await Promise.all([audioPromise, imagePromise]); - const totalSize = audioResult.bytesWritten + imageResult.bytesWritten; + const totalSize = audioResult.bytesWritten + (imageResult?.bytesWritten || 0); dispatch(completeDownload({ id, location: audioLocation, size: totalSize, image: imageLocation })); }, ); export const removeDownloadedTrack = createAsyncThunk( '/downloads/remove/track', - async(id: string, { getState }) => { + async (id: string, { getState }) => { // Retrieve the state - const { downloads: { entities }} = getState() as AppState; + const { downloads: { entities } } = getState() as AppState; // Attempt to retrieve the entity from the state const download = entities[id]; diff --git a/src/utility/mimeType.ts b/src/utility/mimeType.ts index 7bc44e5..37b09ed 100644 --- a/src/utility/mimeType.ts +++ b/src/utility/mimeType.ts @@ -1,8 +1,9 @@ -import db from 'mime-db'; +import mime from 'mime'; const MIME_OVERRIDES: Record = { 'audio/mpeg': 'mp3', - 'audio/ogg': '.ogg' + 'audio/ogg': '.ogg', + 'audio/flac': '.flac', }; /** @@ -12,6 +13,11 @@ export async function getExtensionForUrl(url: string) { const response = await fetch(url, { method: 'HEAD' }); const contentType = response.headers.get('Content-Type'); + // GUARD: Check that the request actually returned something + if (!response.ok) { + throw new Error('Failed to retrieve extension for URL: ' + response.statusText); + } + // GUARD: Check that we received a content type if (!contentType) { throw new Error('Jellyfin did not return a Content-Type for a streaming URL.'); @@ -23,21 +29,20 @@ export async function getExtensionForUrl(url: string) { } // Alternatively, retrieve it from mime-db - const extensions = db[contentType]?.extensions; - + const extension = mime.getExtension(contentType); + // GUARD: Check that we received an extension - if (!extensions?.length) { + if (!extension) { + console.error({ contentType, extension, url }); throw new Error(`Unsupported MIME-type ${contentType}`); } - return extensions[0]; + return extension; } /** * Find a mime type by its extension */ export function getMimeTypeForExtension(extension: string) { - return Object.keys(db).find((type) => { - return db[type].extensions?.includes(extension); - }); + return mime.getType(extension); } \ No newline at end of file