mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 17:03:26 +00:00
feat: background task for mp4 downloads
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
import type { DownloadTask } from "@kesha-antonov/react-native-background-downloader";
|
||||
import type { Asset } from "expo-media-library";
|
||||
import type { ReactNode } from "react";
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import * as MediaLibrary from "expo-media-library";
|
||||
import {
|
||||
checkForExistingDownloads,
|
||||
completeHandler,
|
||||
download,
|
||||
setConfig,
|
||||
} from "@kesha-antonov/react-native-background-downloader";
|
||||
import VideoManager from "@salihgun/react-native-video-processor";
|
||||
import { useToastController } from "@tamagui/toast";
|
||||
|
||||
@@ -25,9 +32,15 @@ export interface DownloadItem {
|
||||
asset?: Asset;
|
||||
isHLS?: boolean;
|
||||
media: ScrapeMedia;
|
||||
downloadResumable?: FileSystem.DownloadResumable;
|
||||
downloadTask?: DownloadTask;
|
||||
}
|
||||
|
||||
// @ts-expect-error - types are not up to date
|
||||
setConfig({
|
||||
isLogsEnabled: false,
|
||||
progressInterval: 250,
|
||||
});
|
||||
|
||||
interface DownloadManagerContextType {
|
||||
downloads: DownloadItem[];
|
||||
startDownload: (
|
||||
@@ -37,7 +50,7 @@ interface DownloadManagerContextType {
|
||||
headers?: Record<string, string>,
|
||||
) => Promise<Asset | void>;
|
||||
removeDownload: (id: string) => void;
|
||||
cancelDownload: (id: string) => Promise<void>;
|
||||
cancelDownload: (id: string) => void;
|
||||
}
|
||||
|
||||
const DownloadManagerContext = createContext<
|
||||
@@ -85,11 +98,11 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
|
||||
return cancellationFlags[downloadId] ?? false;
|
||||
};
|
||||
|
||||
const cancelDownload = async (downloadId: string): Promise<void> => {
|
||||
const cancelDownload = (downloadId: string) => {
|
||||
setCancellationFlag(downloadId, true);
|
||||
const downloadItem = downloads.find((d) => d.id === downloadId);
|
||||
if (downloadItem?.downloadResumable) {
|
||||
await downloadItem.downloadResumable.cancelAsync();
|
||||
if (downloadItem?.downloadTask) {
|
||||
downloadItem.downloadTask.stop();
|
||||
}
|
||||
toastController.show("Download cancelled", {
|
||||
burntOptions: { preset: "done" },
|
||||
@@ -98,6 +111,35 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
|
||||
});
|
||||
};
|
||||
|
||||
// const initializeDownloader = async () => {
|
||||
// setConfig({ isLogsEnabled: true }); // Set any global configs here
|
||||
|
||||
// const existingTasks = await checkForExistingDownloads();
|
||||
// existingTasks.forEach(task => {
|
||||
// // Reattach event listeners to existing tasks
|
||||
// processTask(task);
|
||||
// });
|
||||
// };
|
||||
|
||||
const _processTask = (task: DownloadTask) => {
|
||||
task
|
||||
.progress(({ bytesDownloaded, bytesTotal }) => {
|
||||
const progress = bytesDownloaded / bytesTotal;
|
||||
updateDownloadItem(task.id, { progress });
|
||||
})
|
||||
.done(() => {
|
||||
completeHandler(task.id);
|
||||
})
|
||||
.error(({ error, errorCode }) => {
|
||||
console.error(`Download error: ${errorCode} - ${error}`);
|
||||
});
|
||||
|
||||
const downloadItem = downloads.find((d) => d.id === task.id);
|
||||
if (downloadItem) {
|
||||
updateDownloadItem(task.id, { downloadTask: task });
|
||||
}
|
||||
};
|
||||
|
||||
const startDownload = async (
|
||||
url: string,
|
||||
type: "mp4" | "hls",
|
||||
@@ -143,66 +185,80 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const downloadMP4 = async (
|
||||
interface DownloadProgress {
|
||||
bytesDownloaded: number;
|
||||
bytesTotal: number;
|
||||
}
|
||||
|
||||
const downloadMP4 = (
|
||||
url: string,
|
||||
downloadId: string,
|
||||
headers: Record<string, string>,
|
||||
) => {
|
||||
let lastBytesWritten = 0;
|
||||
let lastTimestamp = Date.now();
|
||||
): Promise<Asset> => {
|
||||
return new Promise<Asset>((resolve, reject) => {
|
||||
let lastBytesWritten = 0;
|
||||
let lastTimestamp = Date.now();
|
||||
|
||||
const callback = (downloadProgress: FileSystem.DownloadProgressData) => {
|
||||
const currentTime = Date.now();
|
||||
const timeElapsed = (currentTime - lastTimestamp) / 1000;
|
||||
const updateProgress = (downloadProgress: DownloadProgress) => {
|
||||
const currentTime = Date.now();
|
||||
const timeElapsed = (currentTime - lastTimestamp) / 1000;
|
||||
|
||||
if (timeElapsed === 0) return;
|
||||
if (timeElapsed === 0) return;
|
||||
|
||||
const bytesWritten = downloadProgress.totalBytesWritten;
|
||||
const newBytes = bytesWritten - lastBytesWritten;
|
||||
const speed = newBytes / timeElapsed / 1024;
|
||||
const progress =
|
||||
bytesWritten / downloadProgress.totalBytesExpectedToWrite;
|
||||
const newBytes = downloadProgress.bytesDownloaded - lastBytesWritten;
|
||||
const speed = newBytes / timeElapsed / 1024;
|
||||
const progress =
|
||||
downloadProgress.bytesDownloaded / downloadProgress.bytesTotal;
|
||||
|
||||
updateDownloadItem(downloadId, {
|
||||
progress,
|
||||
speed,
|
||||
fileSize: downloadProgress.totalBytesExpectedToWrite,
|
||||
downloaded: bytesWritten,
|
||||
});
|
||||
updateDownloadItem(downloadId, {
|
||||
progress,
|
||||
speed,
|
||||
fileSize: downloadProgress.bytesTotal,
|
||||
downloaded: downloadProgress.bytesDownloaded,
|
||||
});
|
||||
|
||||
lastBytesWritten = bytesWritten;
|
||||
lastTimestamp = currentTime;
|
||||
};
|
||||
lastBytesWritten = downloadProgress.bytesDownloaded;
|
||||
lastTimestamp = currentTime;
|
||||
};
|
||||
|
||||
const fileUri = FileSystem.cacheDirectory
|
||||
? FileSystem.cacheDirectory + url.split("/").pop()
|
||||
: null;
|
||||
if (!fileUri) {
|
||||
console.error("Cache directory is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadResumable = FileSystem.createDownloadResumable(
|
||||
url,
|
||||
fileUri,
|
||||
{ headers },
|
||||
callback,
|
||||
);
|
||||
updateDownloadItem(downloadId, { downloadResumable });
|
||||
|
||||
try {
|
||||
const result = await downloadResumable.downloadAsync();
|
||||
if (result) {
|
||||
console.log("Finished downloading to ", result.uri);
|
||||
const asset = await saveFileToMediaLibraryAndDeleteOriginal(
|
||||
result.uri,
|
||||
downloadId,
|
||||
);
|
||||
return asset;
|
||||
const fileUri = FileSystem.cacheDirectory
|
||||
? FileSystem.cacheDirectory + url.split("/").pop()
|
||||
: null;
|
||||
if (!fileUri) {
|
||||
console.error("Cache directory is unavailable");
|
||||
reject(new Error("Cache directory is unavailable"));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
const downloadTask = download({
|
||||
id: downloadId,
|
||||
url,
|
||||
destination: fileUri,
|
||||
headers,
|
||||
isNotificationVisible: true,
|
||||
})
|
||||
.begin(() => {
|
||||
updateDownloadItem(downloadId, { downloadTask });
|
||||
})
|
||||
.progress(({ bytesDownloaded, bytesTotal }) => {
|
||||
updateProgress({ bytesDownloaded, bytesTotal });
|
||||
})
|
||||
.done(() => {
|
||||
saveFileToMediaLibraryAndDeleteOriginal(fileUri, downloadId)
|
||||
.then((asset) => {
|
||||
if (asset) {
|
||||
resolve(asset);
|
||||
} else {
|
||||
reject(new Error("No asset returned"));
|
||||
}
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
})
|
||||
.error(({ error, errorCode }) => {
|
||||
console.error(`Download error: ${errorCode} - ${error}`);
|
||||
reject(new Error(`Download error: ${errorCode} - ${error}`));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const downloadHLS = async (
|
||||
|
48
apps/expo/src/plugins/withRNBackgroundDownloader.js
Normal file
48
apps/expo/src/plugins/withRNBackgroundDownloader.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const { withAppDelegate } = require("@expo/config-plugins");
|
||||
|
||||
function withRNBackgroundDownloader(expoConfig) {
|
||||
return withAppDelegate(expoConfig, async (appDelegateConfig) => {
|
||||
const { modResults: appDelegate } = appDelegateConfig;
|
||||
const appDelegateLines = appDelegate.contents.split("\n");
|
||||
|
||||
// Define the code to be added to AppDelegate.mm
|
||||
const backgroundDownloaderImport =
|
||||
"#import <RNBackgroundDownloader.h> // Required by react-native-background-downloader. Generated by expoPlugins/withRNBackgroundDownloader.js";
|
||||
const backgroundDownloaderDelegate = `\n// Delegate method required by react-native-background-downloader. Generated by expoPlugins/withRNBackgroundDownloader.js
|
||||
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
|
||||
{
|
||||
[RNBackgroundDownloader setCompletionHandlerWithIdentifier:identifier completionHandler:completionHandler];
|
||||
}`;
|
||||
|
||||
// Find the index of the AppDelegate import statement
|
||||
const importIndex = appDelegateLines.findIndex((line) =>
|
||||
/^#import "AppDelegate.h"/.test(line),
|
||||
);
|
||||
|
||||
// Find the index of the last line before the @end statement
|
||||
const endStatementIndex = appDelegateLines.findIndex((line) =>
|
||||
/@end/.test(line),
|
||||
);
|
||||
|
||||
// Insert the import statement if it's not already present
|
||||
if (!appDelegate.contents.includes(backgroundDownloaderImport)) {
|
||||
appDelegateLines.splice(importIndex + 1, 0, backgroundDownloaderImport);
|
||||
}
|
||||
|
||||
// Insert the delegate method above the @end statement
|
||||
if (!appDelegate.contents.includes(backgroundDownloaderDelegate)) {
|
||||
appDelegateLines.splice(
|
||||
endStatementIndex,
|
||||
0,
|
||||
backgroundDownloaderDelegate,
|
||||
);
|
||||
}
|
||||
|
||||
// Update the contents of the AppDelegate file
|
||||
appDelegate.contents = appDelegateLines.join("\n");
|
||||
|
||||
return appDelegateConfig;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = withRNBackgroundDownloader;
|
@@ -0,0 +1,11 @@
|
||||
const withEntitlementsPlist =
|
||||
require("@expo/config-plugins").withEntitlementsPlist;
|
||||
|
||||
const withRemoveiOSNotificationEntitlement = (config) => {
|
||||
return withEntitlementsPlist(config, (mod) => {
|
||||
delete mod.modResults["aps-environment"];
|
||||
return mod;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = withRemoveiOSNotificationEntitlement;
|
Reference in New Issue
Block a user