mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 16:33:26 +00:00
feat: background task for mp4 downloads
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import type { ExpoConfig } from "expo/config";
|
import type { ExpoConfig } from "expo/config";
|
||||||
|
|
||||||
import withRemoveiOSNotificationEntitlement from "./config-plugins/withRemoveiOSNotificationEntitlement";
|
|
||||||
import withRNBackgroundDownloader from "./config-plugins/withRNBackgroundDownloader";
|
|
||||||
import { version } from "./package.json";
|
import { version } from "./package.json";
|
||||||
|
import withRemoveiOSNotificationEntitlement from "./src/plugins/withRemoveiOSNotificationEntitlement";
|
||||||
|
import withRNBackgroundDownloader from "./src/plugins/withRNBackgroundDownloader";
|
||||||
|
|
||||||
const defineConfig = (): ExpoConfig => ({
|
const defineConfig = (): ExpoConfig => ({
|
||||||
name: "movie-web",
|
name: "movie-web",
|
||||||
|
@@ -1,8 +1,15 @@
|
|||||||
|
import type { DownloadTask } from "@kesha-antonov/react-native-background-downloader";
|
||||||
import type { Asset } from "expo-media-library";
|
import type { Asset } from "expo-media-library";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
import * as FileSystem from "expo-file-system";
|
import * as FileSystem from "expo-file-system";
|
||||||
import * as MediaLibrary from "expo-media-library";
|
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 VideoManager from "@salihgun/react-native-video-processor";
|
||||||
import { useToastController } from "@tamagui/toast";
|
import { useToastController } from "@tamagui/toast";
|
||||||
|
|
||||||
@@ -25,9 +32,15 @@ export interface DownloadItem {
|
|||||||
asset?: Asset;
|
asset?: Asset;
|
||||||
isHLS?: boolean;
|
isHLS?: boolean;
|
||||||
media: ScrapeMedia;
|
media: ScrapeMedia;
|
||||||
downloadResumable?: FileSystem.DownloadResumable;
|
downloadTask?: DownloadTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error - types are not up to date
|
||||||
|
setConfig({
|
||||||
|
isLogsEnabled: false,
|
||||||
|
progressInterval: 250,
|
||||||
|
});
|
||||||
|
|
||||||
interface DownloadManagerContextType {
|
interface DownloadManagerContextType {
|
||||||
downloads: DownloadItem[];
|
downloads: DownloadItem[];
|
||||||
startDownload: (
|
startDownload: (
|
||||||
@@ -37,7 +50,7 @@ interface DownloadManagerContextType {
|
|||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
) => Promise<Asset | void>;
|
) => Promise<Asset | void>;
|
||||||
removeDownload: (id: string) => void;
|
removeDownload: (id: string) => void;
|
||||||
cancelDownload: (id: string) => Promise<void>;
|
cancelDownload: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadManagerContext = createContext<
|
const DownloadManagerContext = createContext<
|
||||||
@@ -85,11 +98,11 @@ export const DownloadManagerProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
return cancellationFlags[downloadId] ?? false;
|
return cancellationFlags[downloadId] ?? false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelDownload = async (downloadId: string): Promise<void> => {
|
const cancelDownload = (downloadId: string) => {
|
||||||
setCancellationFlag(downloadId, true);
|
setCancellationFlag(downloadId, true);
|
||||||
const downloadItem = downloads.find((d) => d.id === downloadId);
|
const downloadItem = downloads.find((d) => d.id === downloadId);
|
||||||
if (downloadItem?.downloadResumable) {
|
if (downloadItem?.downloadTask) {
|
||||||
await downloadItem.downloadResumable.cancelAsync();
|
downloadItem.downloadTask.stop();
|
||||||
}
|
}
|
||||||
toastController.show("Download cancelled", {
|
toastController.show("Download cancelled", {
|
||||||
burntOptions: { preset: "done" },
|
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 (
|
const startDownload = async (
|
||||||
url: string,
|
url: string,
|
||||||
type: "mp4" | "hls",
|
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,
|
url: string,
|
||||||
downloadId: string,
|
downloadId: string,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
) => {
|
): Promise<Asset> => {
|
||||||
let lastBytesWritten = 0;
|
return new Promise<Asset>((resolve, reject) => {
|
||||||
let lastTimestamp = Date.now();
|
let lastBytesWritten = 0;
|
||||||
|
let lastTimestamp = Date.now();
|
||||||
|
|
||||||
const callback = (downloadProgress: FileSystem.DownloadProgressData) => {
|
const updateProgress = (downloadProgress: DownloadProgress) => {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
const timeElapsed = (currentTime - lastTimestamp) / 1000;
|
const timeElapsed = (currentTime - lastTimestamp) / 1000;
|
||||||
|
|
||||||
if (timeElapsed === 0) return;
|
if (timeElapsed === 0) return;
|
||||||
|
|
||||||
const bytesWritten = downloadProgress.totalBytesWritten;
|
const newBytes = downloadProgress.bytesDownloaded - lastBytesWritten;
|
||||||
const newBytes = bytesWritten - lastBytesWritten;
|
const speed = newBytes / timeElapsed / 1024;
|
||||||
const speed = newBytes / timeElapsed / 1024;
|
const progress =
|
||||||
const progress =
|
downloadProgress.bytesDownloaded / downloadProgress.bytesTotal;
|
||||||
bytesWritten / downloadProgress.totalBytesExpectedToWrite;
|
|
||||||
|
|
||||||
updateDownloadItem(downloadId, {
|
updateDownloadItem(downloadId, {
|
||||||
progress,
|
progress,
|
||||||
speed,
|
speed,
|
||||||
fileSize: downloadProgress.totalBytesExpectedToWrite,
|
fileSize: downloadProgress.bytesTotal,
|
||||||
downloaded: bytesWritten,
|
downloaded: downloadProgress.bytesDownloaded,
|
||||||
});
|
});
|
||||||
|
|
||||||
lastBytesWritten = bytesWritten;
|
lastBytesWritten = downloadProgress.bytesDownloaded;
|
||||||
lastTimestamp = currentTime;
|
lastTimestamp = currentTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileUri = FileSystem.cacheDirectory
|
const fileUri = FileSystem.cacheDirectory
|
||||||
? FileSystem.cacheDirectory + url.split("/").pop()
|
? FileSystem.cacheDirectory + url.split("/").pop()
|
||||||
: null;
|
: null;
|
||||||
if (!fileUri) {
|
if (!fileUri) {
|
||||||
console.error("Cache directory is unavailable");
|
console.error("Cache directory is unavailable");
|
||||||
return;
|
reject(new 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;
|
|
||||||
}
|
}
|
||||||
} 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 (
|
const downloadHLS = async (
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
"*.js",
|
"*.js",
|
||||||
".expo/types/**/*.ts",
|
".expo/types/**/*.ts",
|
||||||
"expo-env.d.ts",
|
"expo-env.d.ts",
|
||||||
"config-plugins/*",
|
"src/plugins/*",
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user