diff --git a/src/store/downloads/actions.ts b/src/store/downloads/actions.ts index 1b8a5f6f..fffd9925 100644 --- a/src/store/downloads/actions.ts +++ b/src/store/downloads/actions.ts @@ -1,15 +1,16 @@ import { createAction, createAsyncThunk, createEntityAdapter, EntityId } from '@reduxjs/toolkit'; import { AppState } from 'store'; import { generateTrackUrl } from 'utility/JellyfinApi'; -import { downloadFile, unlink, DocumentDirectoryPath } from 'react-native-fs'; +import { downloadFile, unlink, DocumentDirectoryPath, exists } from 'react-native-fs'; import { DownloadEntity } from './types'; +import MimeTypes from 'utility/MimeTypes'; export const downloadAdapter = createEntityAdapter({ selectId: (entity) => entity.id, }); export const queueTrackForDownload = createAction('download/queue'); -export const initializeDownload = createAction<{ id: EntityId, size?: number, jobId?: number }>('download/initialize'); +export const initializeDownload = createAction<{ id: EntityId, size?: number, jobId?: number, location: string }>('download/initialize'); export const progressDownload = createAction<{ id: EntityId, progress: number, jobId?: number }>('download/progress'); export const completeDownload = createAction<{ id: EntityId, location: string, size?: number }>('download/complete'); export const failDownload = createAction<{ id: EntityId }>('download/fail'); @@ -22,7 +23,21 @@ export const downloadTrack = createAsyncThunk( // Generate the URL we can use to download the file const url = generateTrackUrl(id as string, credentials); - const location = `${DocumentDirectoryPath}/${id}.mp3`; + + // Get the content-type from the URL by doing a HEAD-only request + const contentType = (await fetch(url, { method: 'HEAD' })).headers.get('Content-Type'); + if (!contentType) { + throw new Error('Jellyfin did not return a Content-Type for a streaming URL.'); + } + + // Then convert the MIME-type to an extension + const extension = MimeTypes[contentType as keyof typeof MimeTypes]; + if (!extension) { + throw new Error('Jellyfin returned an unrecognized MIME-type'); + } + + // Then generate the proper location + const location = `${DocumentDirectoryPath}/${id}${extension}`; // Actually kick off the download const { promise } = await downloadFile({ @@ -31,7 +46,7 @@ export const downloadTrack = createAsyncThunk( background: true, begin: ({ jobId, contentLength }) => { // Dispatch the initialization - dispatch(initializeDownload({ id, jobId, size: contentLength })); + dispatch(initializeDownload({ id, jobId, size: contentLength, location })); }, progress: (result) => { // Dispatch a progress update @@ -48,8 +63,20 @@ export const downloadTrack = createAsyncThunk( export const removeDownloadedTrack = createAsyncThunk( '/downloads/remove/track', - async(id: EntityId) => { - return unlink(`${DocumentDirectoryPath}/${id}.mp3`); + async(id: EntityId, { getState }) => { + // Retrieve the state + const { downloads: { entities }} = getState() as AppState; + + // Attempt to retrieve the entity from the state + const download = entities[id]; + if (!download) { + throw new Error('Attempted to remove unknown downloaded track.'); + } + + // Then unlink the file, if it exists + if (await exists(download.location)) { + return unlink(download.location); + } } ); diff --git a/src/store/downloads/types.ts b/src/store/downloads/types.ts index b3697e59..437d4f69 100644 --- a/src/store/downloads/types.ts +++ b/src/store/downloads/types.ts @@ -6,6 +6,6 @@ export interface DownloadEntity { isFailed: boolean; isComplete: boolean; size?: number; - location?: string; + location: string; jobId?: number; } diff --git a/src/utility/JellyfinApi.ts b/src/utility/JellyfinApi.ts index 27a1e0fc..85412cc3 100644 --- a/src/utility/JellyfinApi.ts +++ b/src/utility/JellyfinApi.ts @@ -22,8 +22,7 @@ const baseTrackOptions: Record = { // This must be set to support client seeking TranscodingProtocol: 'http', TranscodingContainer: 'aac', - Container: 'mp3,aac,m4a,m4b|aac,alac,m4a,m4b|alac,flac|ogg', - AudioCodec: 'aac', + Container: 'mp3,aac,m4a,m4b|aac,flac,alac,m4a,m4b|alac,flac|ogg', static: 'true', }; diff --git a/src/utility/MimeTypes.ts b/src/utility/MimeTypes.ts new file mode 100644 index 00000000..6ae1c330 --- /dev/null +++ b/src/utility/MimeTypes.ts @@ -0,0 +1,18 @@ +/** + * The filetypes this application will most probably receive. + * Adapted from: https://github.com/jellyfin/jellyfin/blob/63d943aab92a4b5f69e625a269eb830bcbfb4d22/MediaBrowser.Model/Net/MimeTypes.cs#L107 + */ +const MimeTypes = { + 'audio/aac': '.aac', + 'audio/ac3': '.ac3', + 'audio/dsf': '.dsf', + 'audio/dsp': '.dsp', + 'audio/flac': '.flac', + 'audio/m4b': '.m4b', + 'audio/vorbis': '.vorbis', + 'audio/x-ape': '.ape', + 'audio/xsp': '.xsp', + 'audio/x-wavpack': '.wv', +}; + +export default MimeTypes;