mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
downloads improvements
This commit is contained in:
parent
54f85e9689
commit
41ba4ac12c
7 changed files with 230 additions and 83 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -78,3 +78,4 @@ fix-android-scroll-lag-summary.md
|
|||
server/cache-server
|
||||
carousal.md
|
||||
node_modules
|
||||
expofs.md
|
||||
|
|
|
|||
|
|
@ -290,6 +290,8 @@ PODS:
|
|||
- ExpoModulesCore
|
||||
- ExpoSystemUI (6.0.7):
|
||||
- ExpoModulesCore
|
||||
- ExpoUI (0.2.0-beta.7):
|
||||
- ExpoModulesCore
|
||||
- ExpoWebBrowser (15.0.8):
|
||||
- ExpoModulesCore
|
||||
- EXStructuredHeaders (5.0.0)
|
||||
|
|
@ -2741,6 +2743,7 @@ DEPENDENCIES:
|
|||
- ExpoScreenOrientation (from `../node_modules/expo-screen-orientation/ios`)
|
||||
- ExpoSharing (from `../node_modules/expo-sharing/ios`)
|
||||
- ExpoSystemUI (from `../node_modules/expo-system-ui/ios`)
|
||||
- "ExpoUI (from `../node_modules/@expo/ui/ios`)"
|
||||
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
||||
- EXStructuredHeaders (from `../node_modules/expo-structured-headers/ios`)
|
||||
- EXUpdates (from `../node_modules/expo-updates/ios`)
|
||||
|
|
@ -2919,6 +2922,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/expo-sharing/ios"
|
||||
ExpoSystemUI:
|
||||
:path: "../node_modules/expo-system-ui/ios"
|
||||
ExpoUI:
|
||||
:path: "../node_modules/@expo/ui/ios"
|
||||
ExpoWebBrowser:
|
||||
:path: "../node_modules/expo-web-browser/ios"
|
||||
EXStructuredHeaders:
|
||||
|
|
@ -3161,6 +3166,7 @@ SPEC CHECKSUMS:
|
|||
ExpoScreenOrientation: ef9ab3fb85c8a8ff57d52aa169b750aca03f0f4c
|
||||
ExpoSharing: 032c01bb034319e2374badf082ae935be866d2e9
|
||||
ExpoSystemUI: 6cd74248a2282adf6dec488a75fa532d69dee314
|
||||
ExpoUI: b99a1d1ef5352a60bebf4f4fd3a50d2f896ae804
|
||||
ExpoWebBrowser: d04a0d6247a0bea4519fbc2ea816610019ad83e0
|
||||
EXStructuredHeaders: c951e77f2d936f88637421e9588c976da5827368
|
||||
EXUpdates: ef83273afc231a627b170358c90689ac30a4429d
|
||||
|
|
|
|||
15
package-lock.json
generated
15
package-lock.json
generated
|
|
@ -15,7 +15,6 @@
|
|||
"@d11/react-native-fast-image": "^8.8.0",
|
||||
"@expo/env": "^2.0.7",
|
||||
"@expo/metro-runtime": "~6.1.2",
|
||||
"@expo/ui": "~0.2.0-beta.7",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@legendapp/list": "^2.0.13",
|
||||
|
|
@ -2265,20 +2264,6 @@
|
|||
"integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@expo/ui": {
|
||||
"version": "0.2.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@expo/ui/-/ui-0.2.0-beta.7.tgz",
|
||||
"integrity": "sha512-oz2HEpwll+yMFUKbryZ84IgxjLx7RPxxMDVKpCEsK0OhETrLF5NxHlpCkKjdLuQL3QiVSvj5kn6hBFknco3aCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sf-symbols-typescript": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/vector-icons": {
|
||||
"version": "15.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
"@d11/react-native-fast-image": "^8.8.0",
|
||||
"@expo/env": "^2.0.7",
|
||||
"@expo/metro-runtime": "~6.1.2",
|
||||
"@expo/ui": "~0.2.0-beta.7",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@gorhom/bottom-sheet": "^5.2.6",
|
||||
"@legendapp/list": "^2.0.13",
|
||||
|
|
|
|||
|
|
@ -646,18 +646,24 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
snapToAlignment="start"
|
||||
>
|
||||
{trailers[selectedCategory].map((trailer, index) => (
|
||||
<TouchableOpacity
|
||||
<View
|
||||
key={trailer.id}
|
||||
style={[
|
||||
styles.trailerCard,
|
||||
{
|
||||
width: trailerCardWidth,
|
||||
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
|
||||
}
|
||||
styles.trailerCardContainer,
|
||||
{ width: trailerCardWidth }
|
||||
]}
|
||||
onPress={() => handleTrailerPress(trailer)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.trailerCard,
|
||||
{
|
||||
width: trailerCardWidth,
|
||||
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
|
||||
}
|
||||
]}
|
||||
onPress={() => handleTrailerPress(trailer)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
{/* Thumbnail with Gradient Overlay */}
|
||||
<View style={styles.thumbnailWrapper}>
|
||||
<FastImage
|
||||
|
|
@ -665,8 +671,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
style={[
|
||||
styles.thumbnail,
|
||||
{
|
||||
borderTopLeftRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16,
|
||||
borderTopRightRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
|
||||
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
|
||||
}
|
||||
]}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
|
|
@ -675,19 +680,14 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
<View style={[
|
||||
styles.thumbnailGradient,
|
||||
{
|
||||
borderTopLeftRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16,
|
||||
borderTopRightRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
|
||||
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
|
||||
}
|
||||
]} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Trailer Info */}
|
||||
<View style={[
|
||||
styles.trailerInfo,
|
||||
{
|
||||
padding: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
|
||||
}
|
||||
]}>
|
||||
{/* Trailer Info Below Card */}
|
||||
<View style={styles.trailerInfoBelow}>
|
||||
<Text
|
||||
style={[
|
||||
styles.trailerTitle,
|
||||
|
|
@ -695,7 +695,8 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
color: currentTheme.colors.highEmphasis,
|
||||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12,
|
||||
lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 16,
|
||||
marginBottom: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4
|
||||
marginTop: isTV ? 10 : isLargeTablet ? 9 : isTablet ? 8 : 8,
|
||||
marginBottom: isTV ? 4 : isLargeTablet ? 3 : isTablet ? 2 : 2
|
||||
}
|
||||
]}
|
||||
numberOfLines={2}
|
||||
|
|
@ -712,7 +713,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
{new Date(trailer.published_at).getFullYear()}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
{/* Scroll Indicator - shows when there are more items to scroll */}
|
||||
{trailers[selectedCategory].length > (isTV ? 5 : isLargeTablet ? 4 : isTablet ? 4 : 3) && (
|
||||
|
|
@ -886,7 +887,12 @@ const styles = StyleSheet.create({
|
|||
paddingRight: 20, // Extra padding at end for scroll indicator
|
||||
},
|
||||
|
||||
// Enhanced Trailer Card Styles
|
||||
// Trailer Card Container (wraps card + info)
|
||||
trailerCardContainer: {
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
|
||||
// Enhanced Trailer Card Styles (thumbnail only)
|
||||
trailerCard: {
|
||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||
borderRadius: 16,
|
||||
|
|
@ -904,12 +910,12 @@ const styles = StyleSheet.create({
|
|||
thumbnailWrapper: {
|
||||
position: 'relative',
|
||||
aspectRatio: 16 / 9,
|
||||
width: '100%',
|
||||
},
|
||||
thumbnail: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
borderRadius: 16,
|
||||
},
|
||||
thumbnailGradient: {
|
||||
position: 'absolute',
|
||||
|
|
@ -918,21 +924,18 @@ const styles = StyleSheet.create({
|
|||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
borderRadius: 16,
|
||||
},
|
||||
|
||||
|
||||
|
||||
// Trailer Info Styles
|
||||
trailerInfo: {
|
||||
padding: 12,
|
||||
// Trailer Info Below Card
|
||||
trailerInfoBelow: {
|
||||
width: '100%',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
trailerTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
lineHeight: 16,
|
||||
marginBottom: 4,
|
||||
},
|
||||
trailerMeta: {
|
||||
fontSize: 10,
|
||||
|
|
|
|||
|
|
@ -152,7 +152,8 @@ export const CustomSubtitles: React.FC<CustomSubtitlesProps> = ({
|
|||
>
|
||||
{(() => {
|
||||
// Determine alignment and anchor for RTL or LTR
|
||||
const isRTL = lineRTLStatus[0] || lineRTLStatus.some(status => status);
|
||||
// Only apply RTL styling if ALL lines are RTL, not just some
|
||||
const isRTL = lineRTLStatus.every(status => status);
|
||||
let anchor: 'start' | 'middle' | 'end';
|
||||
let x: number;
|
||||
|
||||
|
|
@ -231,7 +232,8 @@ export const CustomSubtitles: React.FC<CustomSubtitlesProps> = ({
|
|||
})
|
||||
) : (
|
||||
(() => {
|
||||
const isRTL = lineRTLStatus.some(status => status);
|
||||
// Only apply RTL styling if ALL lines are RTL, not just some
|
||||
const isRTL = lineRTLStatus.every(status => status);
|
||||
// For RTL, use a very small negative letter spacing to stretch words slightly
|
||||
// This helps with proper diacritic spacing while maintaining ligatures
|
||||
const effectiveLetterSpacing = isRTL ? (subtitleSize * inverseScale * -0.02) : letterSpacing;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ export interface DownloadItem {
|
|||
// Additional metadata for progress tracking
|
||||
imdbId?: string; // IMDb ID for better tracking
|
||||
tmdbId?: number; // TMDB ID if available
|
||||
// CRITICAL: Resume data for proper pause/resume across sessions
|
||||
resumeData?: string; // The string which allows the API to resume a paused download
|
||||
}
|
||||
|
||||
type StartDownloadInput = {
|
||||
|
|
@ -174,6 +176,8 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
// Restore metadata for progress tracking
|
||||
imdbId: (d as any).imdbId ? String((d as any).imdbId) : undefined,
|
||||
tmdbId: typeof (d as any).tmdbId === 'number' ? (d as any).tmdbId : undefined,
|
||||
// CRITICAL: Restore resumeData for proper resume across sessions
|
||||
resumeData: (d as any).resumeData ? String((d as any).resumeData) : undefined,
|
||||
};
|
||||
return safe;
|
||||
});
|
||||
|
|
@ -243,7 +247,14 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
// No need to recreate it
|
||||
} else {
|
||||
console.log(`[DownloadsContext] Creating new resumable for download: ${id}`);
|
||||
// Only create new resumable if none exists (should be rare for resume operations)
|
||||
|
||||
// Use the exact same file URI that was used initially
|
||||
const fileUri = item.fileUri;
|
||||
if (!fileUri) {
|
||||
console.error(`[DownloadsContext] No fileUri found for download: ${id}`);
|
||||
updateDownload(id, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||
return;
|
||||
}
|
||||
|
||||
const progressCallback = (data: any) => {
|
||||
const { totalBytesWritten, totalBytesExpectedToWrite } = data;
|
||||
|
|
@ -274,19 +285,13 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
}
|
||||
};
|
||||
|
||||
// Use the exact same file URI that was used initially
|
||||
const fileUri = item.fileUri;
|
||||
if (!fileUri) {
|
||||
console.error(`[DownloadsContext] No fileUri found for download: ${id}`);
|
||||
updateDownload(id, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Create resumable with resumeData (5th parameter) for proper resume
|
||||
resumable = FileSystem.createDownloadResumable(
|
||||
item.sourceUrl,
|
||||
fileUri,
|
||||
{ headers: item.headers || {} },
|
||||
progressCallback
|
||||
progressCallback,
|
||||
item.resumeData // This is the critical parameter that was missing!
|
||||
);
|
||||
resumablesRef.current.set(id, resumable);
|
||||
lastBytesRef.current.set(id, { bytes: item.downloadedBytes, time: Date.now() });
|
||||
|
|
@ -311,18 +316,43 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
// Validate the downloaded file
|
||||
try {
|
||||
const fileInfo = await FileSystem.getInfoAsync(result.uri);
|
||||
if (!fileInfo.exists || fileInfo.size === 0) {
|
||||
throw new Error('Downloaded file is empty or missing');
|
||||
if (!fileInfo.exists) {
|
||||
throw new Error('Downloaded file does not exist');
|
||||
}
|
||||
if (fileInfo.size === 0) {
|
||||
throw new Error('Downloaded file is empty (0 bytes)');
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Check if file size matches expected size (if known)
|
||||
const currentItem = downloadsRef.current.find(d => d.id === id);
|
||||
if (currentItem && currentItem.totalBytes > 0) {
|
||||
const sizeDifference = Math.abs(fileInfo.size - currentItem.totalBytes);
|
||||
const percentDifference = (sizeDifference / currentItem.totalBytes) * 100;
|
||||
|
||||
// Allow up to 1% difference to account for potential header/metadata variations
|
||||
if (percentDifference > 1) {
|
||||
throw new Error(
|
||||
`File size mismatch: expected ${currentItem.totalBytes} bytes, got ${fileInfo.size} bytes (${percentDifference.toFixed(2)}% difference)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[DownloadsContext] File validation passed: ${result.uri} (${fileInfo.size} bytes)`);
|
||||
} catch (validationError) {
|
||||
console.error(`[DownloadsContext] File validation failed: ${validationError}`);
|
||||
throw new Error('Downloaded file validation failed');
|
||||
// Delete the corrupted file
|
||||
try {
|
||||
await FileSystem.deleteAsync(result.uri, { idempotent: true });
|
||||
console.log(`[DownloadsContext] Deleted corrupted file: ${result.uri}`);
|
||||
} catch (deleteError) {
|
||||
console.error(`[DownloadsContext] Failed to delete corrupted file: ${deleteError}`);
|
||||
}
|
||||
throw new Error(`Downloaded file validation failed: ${validationError}`);
|
||||
}
|
||||
|
||||
// Ensure we use the correct file URI from the result
|
||||
const finalFileUri = result.uri;
|
||||
updateDownload(id, (d) => ({ ...d, status: 'completed', progress: 100, updatedAt: Date.now(), fileUri: finalFileUri }));
|
||||
updateDownload(id, (d) => ({ ...d, status: 'completed', progress: 100, updatedAt: Date.now(), fileUri: finalFileUri, resumeData: undefined }));
|
||||
|
||||
const done = downloadsRef.current.find(x => x.id === id);
|
||||
if (done) notifyCompleted({ ...done, status: 'completed', progress: 100, fileUri: finalFileUri } as DownloadItem);
|
||||
|
|
@ -343,15 +373,37 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
|
||||
// Only mark as error and clean up if it's a real error (not pause-related)
|
||||
console.log(`[DownloadsContext] Marking download as error: ${id}`);
|
||||
// Don't clean up resumable for validation errors - allow retry
|
||||
if (e.message.includes('validation failed')) {
|
||||
console.log(`[DownloadsContext] Keeping resumable for potential retry: ${id}`);
|
||||
updateDownload(id, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||
} else {
|
||||
// Clean up for other errors
|
||||
updateDownload(id, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||
|
||||
// For validation errors, clear resumeData and allow fresh restart
|
||||
if (e.message && e.message.includes('validation failed')) {
|
||||
console.log(`[DownloadsContext] Validation error - clearing resume data for fresh start: ${id}`);
|
||||
updateDownload(id, (d) => ({
|
||||
...d,
|
||||
status: 'error',
|
||||
resumeData: undefined, // Clear corrupted resume data
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
// Clean up resumable to force fresh download on retry
|
||||
resumablesRef.current.delete(id);
|
||||
lastBytesRef.current.delete(id);
|
||||
} else if (e.message && (e.message.includes('size mismatch') || e.message.includes('empty'))) {
|
||||
// File corruption detected - clear everything for fresh start
|
||||
console.log(`[DownloadsContext] File corruption detected - clearing for fresh start: ${id}`);
|
||||
updateDownload(id, (d) => ({
|
||||
...d,
|
||||
status: 'error',
|
||||
downloadedBytes: 0,
|
||||
progress: 0,
|
||||
resumeData: undefined, // Clear corrupted resume data
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
resumablesRef.current.delete(id);
|
||||
lastBytesRef.current.delete(id);
|
||||
} else {
|
||||
// Network or other errors - keep resume data for retry
|
||||
console.log(`[DownloadsContext] Network/other error - keeping resume data for retry: ${id}`);
|
||||
updateDownload(id, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||
// Keep resumable for potential retry
|
||||
}
|
||||
}
|
||||
}, [updateDownload, maybeNotifyProgress, notifyCompleted]);
|
||||
|
|
@ -420,6 +472,8 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
// Store metadata for progress tracking
|
||||
imdbId: input.imdbId,
|
||||
tmdbId: input.tmdbId,
|
||||
// Initialize resumeData as undefined
|
||||
resumeData: undefined,
|
||||
};
|
||||
|
||||
setDownloads(prev => [newItem, ...prev]);
|
||||
|
|
@ -468,6 +522,17 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
const currentItem = downloadsRef.current.find(d => d.id === compoundId);
|
||||
if (currentItem && currentItem.status === 'paused') {
|
||||
console.log(`[DownloadsContext] Download was paused during initial download, keeping paused state: ${compoundId}`);
|
||||
// CRITICAL FIX: Save resumeData when paused
|
||||
try {
|
||||
const savableState = resumable.savable();
|
||||
updateDownload(compoundId, (d) => ({
|
||||
...d,
|
||||
resumeData: savableState.resumeData,
|
||||
updatedAt: Date.now(),
|
||||
}));
|
||||
} catch (savableError) {
|
||||
console.log(`[DownloadsContext] Could not get savable state after pause: ${compoundId}`, savableError);
|
||||
}
|
||||
// Don't delete resumable - keep it for resume
|
||||
return;
|
||||
}
|
||||
|
|
@ -477,16 +542,41 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
// Validate the downloaded file
|
||||
try {
|
||||
const fileInfo = await FileSystem.getInfoAsync(result.uri);
|
||||
if (!fileInfo.exists || fileInfo.size === 0) {
|
||||
throw new Error('Downloaded file is empty or missing');
|
||||
if (!fileInfo.exists) {
|
||||
throw new Error('Downloaded file does not exist');
|
||||
}
|
||||
if (fileInfo.size === 0) {
|
||||
throw new Error('Downloaded file is empty (0 bytes)');
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Check if file size matches expected size (if known)
|
||||
const currentItem = downloadsRef.current.find(d => d.id === compoundId);
|
||||
if (currentItem && currentItem.totalBytes > 0) {
|
||||
const sizeDifference = Math.abs(fileInfo.size - currentItem.totalBytes);
|
||||
const percentDifference = (sizeDifference / currentItem.totalBytes) * 100;
|
||||
|
||||
// Allow up to 1% difference to account for potential header/metadata variations
|
||||
if (percentDifference > 1) {
|
||||
throw new Error(
|
||||
`File size mismatch: expected ${currentItem.totalBytes} bytes, got ${fileInfo.size} bytes (${percentDifference.toFixed(2)}% difference)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[DownloadsContext] File validation passed: ${result.uri} (${fileInfo.size} bytes)`);
|
||||
} catch (validationError) {
|
||||
console.error(`[DownloadsContext] File validation failed: ${validationError}`);
|
||||
throw new Error('Downloaded file validation failed');
|
||||
// Delete the corrupted file
|
||||
try {
|
||||
await FileSystem.deleteAsync(result.uri, { idempotent: true });
|
||||
console.log(`[DownloadsContext] Deleted corrupted file: ${result.uri}`);
|
||||
} catch (deleteError) {
|
||||
console.error(`[DownloadsContext] Failed to delete corrupted file: ${deleteError}`);
|
||||
}
|
||||
throw new Error(`Downloaded file validation failed: ${validationError}`);
|
||||
}
|
||||
|
||||
updateDownload(compoundId, (d) => ({ ...d, status: 'completed', progress: 100, updatedAt: Date.now(), fileUri: result.uri }));
|
||||
updateDownload(compoundId, (d) => ({ ...d, status: 'completed', progress: 100, updatedAt: Date.now(), fileUri: result.uri, resumeData: undefined }));
|
||||
const done = downloadsRef.current.find(x => x.id === compoundId);
|
||||
if (done) notifyCompleted({ ...done, status: 'completed', progress: 100, fileUri: result.uri } as DownloadItem);
|
||||
resumablesRef.current.delete(compoundId);
|
||||
|
|
@ -496,20 +586,53 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
const current = downloadsRef.current.find(d => d.id === compoundId);
|
||||
if (current && current.status === 'paused') {
|
||||
console.log(`[DownloadsContext] Error was due to pause during initial download, keeping paused state and resumable: ${compoundId}`);
|
||||
// CRITICAL FIX: Save resumeData when paused
|
||||
try {
|
||||
const savableState = resumable.savable();
|
||||
updateDownload(compoundId, (d) => ({
|
||||
...d,
|
||||
resumeData: savableState.resumeData,
|
||||
updatedAt: Date.now(),
|
||||
}));
|
||||
} catch (savableError) {
|
||||
console.log(`[DownloadsContext] Could not get savable state after pause error: ${compoundId}`, savableError);
|
||||
}
|
||||
// Don't delete resumable - keep it for resume
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[DownloadsContext] Marking initial download as error: ${compoundId}`);
|
||||
// Don't clean up resumable for validation errors - allow retry
|
||||
if (e.message.includes('validation failed')) {
|
||||
console.log(`[DownloadsContext] Keeping resumable for potential retry: ${compoundId}`);
|
||||
updateDownload(compoundId, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||
} else {
|
||||
// Clean up for other errors
|
||||
updateDownload(compoundId, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||
|
||||
// For validation errors, clear resumeData and allow fresh restart
|
||||
if (e.message && e.message.includes('validation failed')) {
|
||||
console.log(`[DownloadsContext] Validation error - clearing resume data for fresh start: ${compoundId}`);
|
||||
updateDownload(compoundId, (d) => ({
|
||||
...d,
|
||||
status: 'error',
|
||||
resumeData: undefined, // Clear corrupted resume data
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
// Clean up resumable to force fresh download on retry
|
||||
resumablesRef.current.delete(compoundId);
|
||||
lastBytesRef.current.delete(compoundId);
|
||||
} else if (e.message && (e.message.includes('size mismatch') || e.message.includes('empty'))) {
|
||||
// File corruption detected - clear everything for fresh start
|
||||
console.log(`[DownloadsContext] File corruption detected - clearing for fresh start: ${compoundId}`);
|
||||
updateDownload(compoundId, (d) => ({
|
||||
...d,
|
||||
status: 'error',
|
||||
downloadedBytes: 0,
|
||||
progress: 0,
|
||||
resumeData: undefined, // Clear corrupted resume data
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
resumablesRef.current.delete(compoundId);
|
||||
lastBytesRef.current.delete(compoundId);
|
||||
} else {
|
||||
// Network or other errors - keep resume data for retry
|
||||
console.log(`[DownloadsContext] Network/other error - keeping resume data for retry: ${compoundId}`);
|
||||
updateDownload(compoundId, (d) => ({ ...d, status: 'error', updatedAt: Date.now() }));
|
||||
// Keep resumable for potential retry
|
||||
}
|
||||
}
|
||||
}, [updateDownload, resumeDownload]);
|
||||
|
|
@ -524,12 +647,40 @@ export const DownloadsProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||
const resumable = resumablesRef.current.get(id);
|
||||
if (resumable) {
|
||||
try {
|
||||
await resumable.pauseAsync();
|
||||
// CRITICAL FIX: Get the pause state which contains resumeData
|
||||
const pauseResult = await resumable.pauseAsync();
|
||||
console.log(`[DownloadsContext] Successfully paused download: ${id}`);
|
||||
|
||||
// CRITICAL FIX: Save the resumeData from pauseAsync result or savable()
|
||||
// The pauseAsync returns a DownloadPauseState object with resumeData
|
||||
const savableState = resumable.savable();
|
||||
|
||||
// Update the download item with the critical resumeData for future resume
|
||||
updateDownload(id, (d) => ({
|
||||
...d,
|
||||
status: 'paused',
|
||||
resumeData: savableState.resumeData || pauseResult.resumeData, // Store resume data
|
||||
updatedAt: Date.now(),
|
||||
}));
|
||||
|
||||
console.log(`[DownloadsContext] Saved resume data for download: ${id}`);
|
||||
|
||||
// Keep the resumable in memory for resume - DO NOT DELETE
|
||||
} catch (error) {
|
||||
console.log(`[DownloadsContext] Pause async failed (this is normal if already paused): ${id}`, error);
|
||||
// Keep resumable even if pause fails - we still want to be able to resume
|
||||
// Try to get savable state even if pause failed
|
||||
try {
|
||||
const savableState = resumable.savable();
|
||||
updateDownload(id, (d) => ({
|
||||
...d,
|
||||
status: 'paused',
|
||||
resumeData: savableState.resumeData,
|
||||
updatedAt: Date.now(),
|
||||
}));
|
||||
} catch (savableError) {
|
||||
console.log(`[DownloadsContext] Could not get savable state: ${id}`, savableError);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`[DownloadsContext] No resumable found for download: ${id}, just marked as paused`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue