Skip to content

Commit

Permalink
feat: Allow FLAC playback
Browse files Browse the repository at this point in the history
  • Loading branch information
leinelissen committed Aug 13, 2022
1 parent 8a9c14e commit 5b54760
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 9 deletions.
39 changes: 33 additions & 6 deletions src/store/downloads/actions.ts
Original file line number Diff line number Diff line change
@@ -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<DownloadEntity>({
selectId: (entity) => entity.id,
});

export const queueTrackForDownload = createAction<EntityId>('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');
Expand All @@ -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({
Expand All @@ -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
Expand All @@ -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);
}
}
);

2 changes: 1 addition & 1 deletion src/store/downloads/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export interface DownloadEntity {
isFailed: boolean;
isComplete: boolean;
size?: number;
location?: string;
location: string;
jobId?: number;
}
3 changes: 1 addition & 2 deletions src/utility/JellyfinApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ const baseTrackOptions: Record<string, string> = {
// 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',
};

Expand Down
18 changes: 18 additions & 0 deletions src/utility/MimeTypes.ts
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 5b54760

Please sign in to comment.