fix: show error messages when tracks fail to download
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user