fix: missing images causing download errors
This commit is contained in:
36
package-lock.json
generated
36
package-lock.json
generated
@@ -25,7 +25,7 @@
|
|||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"i18n-js": "^4.3.2",
|
"i18n-js": "^4.3.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime-db": "^1.53.0",
|
"mime": "^4.0.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-airplay": "^1.2.0",
|
"react-airplay": "^1.2.0",
|
||||||
"react-native": "0.74.3",
|
"react-native": "0.74.3",
|
||||||
@@ -59,7 +59,6 @@
|
|||||||
"@sentry/react-native": "^5.26.0",
|
"@sentry/react-native": "^5.26.0",
|
||||||
"@types/i18n-js": "^3.8.9",
|
"@types/i18n-js": "^3.8.9",
|
||||||
"@types/lodash": "^4.14.202",
|
"@types/lodash": "^4.14.202",
|
||||||
"@types/mime-db": "^1.43.5",
|
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
@@ -2158,6 +2157,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/@react-native-community/cli-tools/node_modules/p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
@@ -3785,13 +3796,6 @@
|
|||||||
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
|
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.11.17",
|
"version": "20.11.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
|
||||||
@@ -8414,14 +8418,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime": {
|
"node_modules/mime": {
|
||||||
"version": "2.6.0",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.6.tgz",
|
||||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
"integrity": "sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"mime": "cli.js"
|
"mime": "bin/cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0.0"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"i18n-js": "^4.3.2",
|
"i18n-js": "^4.3.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime-db": "^1.53.0",
|
"mime": "^4.0.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-airplay": "^1.2.0",
|
"react-airplay": "^1.2.0",
|
||||||
"react-native": "0.74.3",
|
"react-native": "0.74.3",
|
||||||
@@ -62,7 +62,6 @@
|
|||||||
"@sentry/react-native": "^5.26.0",
|
"@sentry/react-native": "^5.26.0",
|
||||||
"@types/i18n-js": "^3.8.9",
|
"@types/i18n-js": "^3.8.9",
|
||||||
"@types/lodash": "^4.14.202",
|
"@types/lodash": "^4.14.202",
|
||||||
"@types/mime-db": "^1.43.5",
|
|
||||||
"@types/node": "^20.11.17",
|
"@types/node": "^20.11.17",
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { getExtensionForUrl } from '@/utility/mimeType';
|
|||||||
export const downloadAdapter = createEntityAdapter<DownloadEntity>();
|
export const downloadAdapter = createEntityAdapter<DownloadEntity>();
|
||||||
|
|
||||||
export const queueTrackForDownload = createAction<string>('download/queue');
|
export const queueTrackForDownload = createAction<string>('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 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 completeDownload = createAction<{ id: string, location: string, size?: number, image?: string }>('download/complete');
|
||||||
export const failDownload = createAction<{ id: string }>('download/fail');
|
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
|
// Get the content-type from the URL by doing a HEAD-only request
|
||||||
const [audioExt, imageExt] = await Promise.all([
|
const [audioExt, imageExt] = await Promise.all([
|
||||||
getExtensionForUrl(audioUrl),
|
getExtensionForUrl(audioUrl),
|
||||||
getExtensionForUrl(imageUrl)
|
// Image files may be absent
|
||||||
|
getExtensionForUrl(imageUrl).catch(() => null)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Then generate the proper location
|
// Then generate the proper location
|
||||||
const audioLocation = `${DocumentDirectoryPath}/${id}.${audioExt}`;
|
const audioLocation = `${DocumentDirectoryPath}/${id}.${audioExt}`;
|
||||||
const imageLocation = `${DocumentDirectoryPath}/${id}.${imageExt}`;
|
const imageLocation = imageExt ? `${DocumentDirectoryPath}/${id}.${imageExt}` : undefined;
|
||||||
|
|
||||||
// Actually kick off the download
|
// Actually kick off the download
|
||||||
const { promise: audioPromise } = downloadFile({
|
const { promise: audioPromise } = downloadFile({
|
||||||
@@ -48,24 +49,26 @@ export const downloadTrack = createAsyncThunk(
|
|||||||
toFile: audioLocation,
|
toFile: audioLocation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { promise: imagePromise } = downloadFile({
|
const { promise: imagePromise } = imageExt && imageLocation
|
||||||
fromUrl: imageUrl,
|
? downloadFile({
|
||||||
toFile: imageLocation,
|
fromUrl: imageUrl,
|
||||||
background: true,
|
toFile: imageLocation,
|
||||||
});
|
background: true,
|
||||||
|
})
|
||||||
|
: { promise: Promise.resolve(null) };
|
||||||
|
|
||||||
// Await job completion
|
// Await job completion
|
||||||
const [audioResult, imageResult] = await Promise.all([audioPromise, imagePromise]);
|
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 }));
|
dispatch(completeDownload({ id, location: audioLocation, size: totalSize, image: imageLocation }));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const removeDownloadedTrack = createAsyncThunk(
|
export const removeDownloadedTrack = createAsyncThunk(
|
||||||
'/downloads/remove/track',
|
'/downloads/remove/track',
|
||||||
async(id: string, { getState }) => {
|
async (id: string, { getState }) => {
|
||||||
// Retrieve the state
|
// Retrieve the state
|
||||||
const { downloads: { entities }} = getState() as AppState;
|
const { downloads: { entities } } = getState() as AppState;
|
||||||
|
|
||||||
// Attempt to retrieve the entity from the state
|
// Attempt to retrieve the entity from the state
|
||||||
const download = entities[id];
|
const download = entities[id];
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import db from 'mime-db';
|
import mime from 'mime';
|
||||||
|
|
||||||
const MIME_OVERRIDES: Record<string, string> = {
|
const MIME_OVERRIDES: Record<string, string> = {
|
||||||
'audio/mpeg': 'mp3',
|
'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 response = await fetch(url, { method: 'HEAD' });
|
||||||
const contentType = response.headers.get('Content-Type');
|
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
|
// GUARD: Check that we received a content type
|
||||||
if (!contentType) {
|
if (!contentType) {
|
||||||
throw new Error('Jellyfin did not return a Content-Type for a streaming URL.');
|
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
|
// Alternatively, retrieve it from mime-db
|
||||||
const extensions = db[contentType]?.extensions;
|
const extension = mime.getExtension(contentType);
|
||||||
|
|
||||||
// GUARD: Check that we received an extension
|
// GUARD: Check that we received an extension
|
||||||
if (!extensions?.length) {
|
if (!extension) {
|
||||||
|
console.error({ contentType, extension, url });
|
||||||
throw new Error(`Unsupported MIME-type ${contentType}`);
|
throw new Error(`Unsupported MIME-type ${contentType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions[0];
|
return extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a mime type by its extension
|
* Find a mime type by its extension
|
||||||
*/
|
*/
|
||||||
export function getMimeTypeForExtension(extension: string) {
|
export function getMimeTypeForExtension(extension: string) {
|
||||||
return Object.keys(db).find((type) => {
|
return mime.getType(extension);
|
||||||
return db[type].extensions?.includes(extension);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user