fix: show error messages when tracks fail to download

This commit is contained in:
Lei Nelissen
2023-07-12 23:28:00 +02:00
parent 0b43c0749e
commit 2bd9cf9950
6 changed files with 61 additions and 32 deletions

View File

@@ -55,7 +55,7 @@ function DownloadManager () {
}, [queued, dispatch, activeDownloads]); }, [queued, dispatch, activeDownloads]);
useEffect(() => { useEffect(() => {
// GUARD: We only run this functino once // GUARD: We only run this function once
if (hasRehydratedOrphans) { if (hasRehydratedOrphans) {
return; return;
} }
@@ -99,7 +99,6 @@ function DownloadManager () {
setHasRehydratedOrphans(true); setHasRehydratedOrphans(true);
}, [rehydrated, ids, hasRehydratedOrphans, dispatch]); }, [rehydrated, ids, hasRehydratedOrphans, dispatch]);
return null; return null;
} }

View File

@@ -17,6 +17,7 @@ import FastImage from 'react-native-fast-image';
import { useGetImage } from '@/utility/JellyfinApi'; import { useGetImage } from '@/utility/JellyfinApi';
import { ShadowWrapper } from '@/components/Shadow'; import { ShadowWrapper } from '@/components/Shadow';
import { SafeFlatList } from '@/components/SafeNavigatorView'; import { SafeFlatList } from '@/components/SafeNavigatorView';
import { THEME_COLOR } from '@/CONSTANTS';
const DownloadedTrack = styled.View` const DownloadedTrack = styled.View`
flex: 1 0 auto; flex: 1 0 auto;
@@ -32,6 +33,10 @@ const AlbumImage = styled(FastImage)`
border-radius: 4px; border-radius: 4px;
`; `;
const ErrorWrapper = styled.View`
padding: 2px 16px 8px 16px;
`;
function Downloads() { function Downloads() {
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@@ -106,35 +111,42 @@ function Downloads() {
), [totalDownloadSize, defaultStyles, failedIds.length, handleRetryFailed, handleDeleteAllTracks, ids.length]); ), [totalDownloadSize, defaultStyles, failedIds.length, handleRetryFailed, handleDeleteAllTracks, ids.length]);
const renderItem = useCallback<NonNullable<FlatListProps<EntityId>['renderItem']>>(({ item }) => ( const renderItem = useCallback<NonNullable<FlatListProps<EntityId>['renderItem']>>(({ item }) => (
<DownloadedTrack> <>
<View style={{ marginRight: 12 }}> <DownloadedTrack>
<ShadowWrapper size="small">
<AlbumImage source={{ uri: getImage(item as string) }} style={defaultStyles.imageBackground} />
</ShadowWrapper>
</View>
<View style={{ flexShrink: 1, marginRight: 8 }}>
<Text style={[{ fontSize: 16, marginBottom: 4 }, defaultStyles.text]} numberOfLines={1}>
{tracks[item]?.Name}
</Text>
<Text style={[{ flexShrink: 1, fontSize: 11 }, defaultStyles.textHalfOpacity]} numberOfLines={1}>
{tracks[item]?.AlbumArtist} {tracks[item]?.Album ? `${tracks[item]?.Album}` : ''}
</Text>
</View>
<View style={{ marginLeft: 'auto', flexDirection: 'row', alignItems: 'center' }}>
{entities[item]?.isComplete && entities[item]?.size ? (
<Text style={[defaultStyles.textQuarterOpacity, { marginRight: 12, fontSize: 12 }]}>
{formatBytes(entities[item]?.size || 0)}
</Text>
) : null}
<View style={{ marginRight: 12 }}> <View style={{ marginRight: 12 }}>
<DownloadIcon trackId={item} /> <ShadowWrapper size="small">
<AlbumImage source={{ uri: getImage(item as string) }} style={defaultStyles.imageBackground} />
</ShadowWrapper>
</View> </View>
<Button onPress={() => handleDelete(item)} size="small" icon={TrashIcon} testID={`delete-track-${item}`} /> <View style={{ flexShrink: 1, marginRight: 8 }}>
{!entities[item]?.isComplete && ( <Text style={[{ fontSize: 16, marginBottom: 4 }, defaultStyles.text]} numberOfLines={1}>
<Button onPress={() => retryTrack(item)} size="small" icon={ArrowClockwise} style={{ marginLeft: 4 }} /> {tracks[item]?.Name}
)} </Text>
</View> <Text style={[{ flexShrink: 1, fontSize: 11 }, defaultStyles.textHalfOpacity]} numberOfLines={1}>
</DownloadedTrack> {tracks[item]?.AlbumArtist} {tracks[item]?.Album ? `${tracks[item]?.Album}` : ''}
</Text>
</View>
<View style={{ marginLeft: 'auto', flexDirection: 'row', alignItems: 'center' }}>
{entities[item]?.isComplete && entities[item]?.size ? (
<Text style={[defaultStyles.textQuarterOpacity, { marginRight: 12, fontSize: 12 }]}>
{formatBytes(entities[item]?.size || 0)}
</Text>
) : null}
<View style={{ marginRight: 12 }}>
<DownloadIcon trackId={item} />
</View>
<Button onPress={() => handleDelete(item)} size="small" icon={TrashIcon} testID={`delete-track-${item}`} />
{!entities[item]?.isComplete && (
<Button onPress={() => retryTrack(item)} size="small" icon={ArrowClockwise} style={{ marginLeft: 4 }} />
)}
</View>
</DownloadedTrack>
{entities[item]?.error && (
<ErrorWrapper>
<Text style={{ color: THEME_COLOR }}>{entities[item]?.error}</Text>
</ErrorWrapper>
)}
</>
), [entities, retryTrack, handleDelete, defaultStyles, tracks, getImage]); ), [entities, retryTrack, handleDelete, defaultStyles, tracks, getImage]);
// If no tracks have been downloaded, show a short message describing this // If no tracks have been downloaded, show a short message describing this

View File

@@ -33,7 +33,7 @@ export const downloadTrack = createAsyncThunk(
// Then convert the MIME-type to an extension // Then convert the MIME-type to an extension
const extension = MimeTypes[contentType as keyof typeof MimeTypes]; const extension = MimeTypes[contentType as keyof typeof MimeTypes];
if (!extension) { if (!extension) {
throw new Error('Jellyfin returned an unrecognized MIME-type'); throw new Error(`Unsupported MIME-type ${contentType}`);
} }
// Then generate the proper location // Then generate the proper location
@@ -74,7 +74,7 @@ export const removeDownloadedTrack = createAsyncThunk(
} }
// Then unlink the file, if it exists // Then unlink the file, if it exists
if (await exists(download.location)) { if (download.location && await exists(download.location)) {
return unlink(download.location); return unlink(download.location);
} }
} }

View File

@@ -2,6 +2,7 @@ import { createSlice, Dictionary, EntityId } from '@reduxjs/toolkit';
import { import {
completeDownload, completeDownload,
downloadAdapter, downloadAdapter,
downloadTrack,
failDownload, failDownload,
initializeDownload, initializeDownload,
progressDownload, progressDownload,
@@ -49,6 +50,7 @@ const downloads = createSlice({
...action.payload, ...action.payload,
isFailed: false, isFailed: false,
isComplete: true, isComplete: true,
error: undefined,
} }
}); });
@@ -67,6 +69,20 @@ const downloads = createSlice({
} }
}); });
}); });
builder.addCase(downloadTrack.rejected, (state, action) => {
downloadAdapter.upsertOne(state, {
id: action.meta.arg,
isComplete: false,
isFailed: true,
progress: 0,
error: action.error.message,
});
// Remove the item from the queue
const newSet = new Set(state.queued);
newSet.delete(action.meta.arg);
state.queued = Array.from(newSet);
});
builder.addCase(removeDownloadedTrack.fulfilled, (state, action) => { builder.addCase(removeDownloadedTrack.fulfilled, (state, action) => {
// Remove the download if it exists // Remove the download if it exists
downloadAdapter.removeOne(state, action.meta.arg); downloadAdapter.removeOne(state, action.meta.arg);

View File

@@ -6,6 +6,7 @@ export interface DownloadEntity {
isFailed: boolean; isFailed: boolean;
isComplete: boolean; isComplete: boolean;
size?: number; size?: number;
location: string; location?: string;
jobId?: number; jobId?: number;
error?: string;
} }

View File

@@ -9,6 +9,7 @@ const MimeTypes = {
'audio/dsp': '.dsp', 'audio/dsp': '.dsp',
'audio/flac': '.flac', 'audio/flac': '.flac',
'audio/m4b': '.m4b', 'audio/m4b': '.m4b',
'audio/mp4': '.m4a',
'audio/mpeg': '.mp3', 'audio/mpeg': '.mp3',
'audio/vorbis': '.vorbis', 'audio/vorbis': '.vorbis',
'audio/x-ape': '.ape', 'audio/x-ape': '.ape',