mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 16:33:26 +00:00
Fix for infinite rerender while scraping 🍻
This commit is contained in:
@@ -124,97 +124,104 @@ export const useDownloadManager = () => {
|
|||||||
[setDownloads],
|
[setDownloads],
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveFileToMediaLibraryAndDeleteOriginal = async (
|
const saveFileToMediaLibraryAndDeleteOriginal = useCallback(
|
||||||
fileUri: string,
|
async (fileUri: string, download: Download): Promise<Asset | void> => {
|
||||||
download: Download,
|
console.log(
|
||||||
): Promise<Asset | void> => {
|
"Saving file to media library and deleting original",
|
||||||
console.log("Saving file to media library and deleting original", fileUri);
|
fileUri,
|
||||||
try {
|
);
|
||||||
updateDownloadItem(download.id, { status: "importing" });
|
try {
|
||||||
|
updateDownloadItem(download.id, { status: "importing" });
|
||||||
|
|
||||||
const asset = await MediaLibrary.createAssetAsync(fileUri);
|
const asset = await MediaLibrary.createAssetAsync(fileUri);
|
||||||
const { localUri } = await MediaLibrary.getAssetInfoAsync(asset);
|
const { localUri } = await MediaLibrary.getAssetInfoAsync(asset);
|
||||||
await FileSystem.deleteAsync(fileUri);
|
await FileSystem.deleteAsync(fileUri);
|
||||||
|
|
||||||
updateDownloadItem(download.id, {
|
updateDownloadItem(download.id, {
|
||||||
status: "finished",
|
status: "finished",
|
||||||
localPath: localUri,
|
localPath: localUri,
|
||||||
});
|
});
|
||||||
console.log("File saved to media library and original deleted");
|
console.log("File saved to media library and original deleted");
|
||||||
showToast("Download finished", {
|
showToast("Download finished", {
|
||||||
burntOptions: { preset: "done" },
|
burntOptions: { preset: "done" },
|
||||||
});
|
});
|
||||||
return asset;
|
return asset;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving file to media library:", error);
|
console.error("Error saving file to media library:", error);
|
||||||
showToast("Download failed", {
|
showToast("Download failed", {
|
||||||
burntOptions: { preset: "error" },
|
burntOptions: { preset: "error" },
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadMP4 = async (
|
|
||||||
url: string,
|
|
||||||
downloadItem: Download,
|
|
||||||
headers: Record<string, string>,
|
|
||||||
): Promise<Asset | void> => {
|
|
||||||
let lastBytesWritten = 0;
|
|
||||||
let lastTimestamp = Date.now();
|
|
||||||
|
|
||||||
const updateProgress = (downloadProgress: DownloadProgressData) => {
|
|
||||||
const currentTime = Date.now();
|
|
||||||
const timeElapsed = (currentTime - lastTimestamp) / 1000;
|
|
||||||
|
|
||||||
if (timeElapsed === 0) return;
|
|
||||||
|
|
||||||
const newBytes = downloadProgress.totalBytesWritten - lastBytesWritten;
|
|
||||||
const speed = newBytes / timeElapsed / 1024 / 1024;
|
|
||||||
const progress =
|
|
||||||
downloadProgress.totalBytesWritten /
|
|
||||||
downloadProgress.totalBytesExpectedToWrite;
|
|
||||||
|
|
||||||
updateDownloadItem(downloadItem.id, {
|
|
||||||
progress,
|
|
||||||
speed,
|
|
||||||
fileSize: downloadProgress.totalBytesExpectedToWrite,
|
|
||||||
downloaded: downloadProgress.totalBytesWritten,
|
|
||||||
});
|
|
||||||
|
|
||||||
lastBytesWritten = downloadProgress.totalBytesWritten;
|
|
||||||
lastTimestamp = currentTime;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileUri = `${FileSystem.cacheDirectory}movie-web${url.split("/").pop()}`;
|
|
||||||
if (
|
|
||||||
!(await FileSystem.getInfoAsync(`${FileSystem.cacheDirectory}movie-web`))
|
|
||||||
.exists
|
|
||||||
) {
|
|
||||||
console.error("Cache directory is unavailable");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadResumable = FileSystem.createDownloadResumable(
|
|
||||||
url,
|
|
||||||
fileUri,
|
|
||||||
{
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
updateProgress,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await downloadResumable.downloadAsync();
|
|
||||||
if (result) {
|
|
||||||
console.log("Finished downloading to ", result.uri);
|
|
||||||
return saveFileToMediaLibraryAndDeleteOriginal(
|
|
||||||
result.uri,
|
|
||||||
downloadItem,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
},
|
||||||
console.error(e);
|
[updateDownloadItem, showToast],
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
const downloadMP4 = useCallback(
|
||||||
|
async (
|
||||||
|
url: string,
|
||||||
|
downloadItem: Download,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
): Promise<Asset | void> => {
|
||||||
|
let lastBytesWritten = 0;
|
||||||
|
let lastTimestamp = Date.now();
|
||||||
|
|
||||||
|
const updateProgress = (downloadProgress: DownloadProgressData) => {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const timeElapsed = (currentTime - lastTimestamp) / 1000;
|
||||||
|
|
||||||
|
if (timeElapsed === 0) return;
|
||||||
|
|
||||||
|
const newBytes = downloadProgress.totalBytesWritten - lastBytesWritten;
|
||||||
|
const speed = newBytes / timeElapsed / 1024 / 1024;
|
||||||
|
const progress =
|
||||||
|
downloadProgress.totalBytesWritten /
|
||||||
|
downloadProgress.totalBytesExpectedToWrite;
|
||||||
|
|
||||||
|
updateDownloadItem(downloadItem.id, {
|
||||||
|
progress,
|
||||||
|
speed,
|
||||||
|
fileSize: downloadProgress.totalBytesExpectedToWrite,
|
||||||
|
downloaded: downloadProgress.totalBytesWritten,
|
||||||
|
});
|
||||||
|
|
||||||
|
lastBytesWritten = downloadProgress.totalBytesWritten;
|
||||||
|
lastTimestamp = currentTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileUri = `${FileSystem.cacheDirectory}movie-web${url.split("/").pop()}`;
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
await FileSystem.getInfoAsync(`${FileSystem.cacheDirectory}movie-web`)
|
||||||
|
).exists
|
||||||
|
) {
|
||||||
|
console.error("Cache directory is unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadResumable = FileSystem.createDownloadResumable(
|
||||||
|
url,
|
||||||
|
fileUri,
|
||||||
|
{
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
updateProgress,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await downloadResumable.downloadAsync();
|
||||||
|
if (result) {
|
||||||
|
console.log("Finished downloading to ", result.uri);
|
||||||
|
return saveFileToMediaLibraryAndDeleteOriginal(
|
||||||
|
result.uri,
|
||||||
|
downloadItem,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateDownloadItem, saveFileToMediaLibraryAndDeleteOriginal],
|
||||||
|
);
|
||||||
|
|
||||||
const cleanupDownload = useCallback(
|
const cleanupDownload = useCallback(
|
||||||
async (segmentDir: string, download: Download) => {
|
async (segmentDir: string, download: Download) => {
|
||||||
@@ -224,162 +231,177 @@ export const useDownloadManager = () => {
|
|||||||
[removeDownload],
|
[removeDownload],
|
||||||
);
|
);
|
||||||
|
|
||||||
const downloadHLS = async (
|
const downloadHLS = useCallback(
|
||||||
url: string,
|
async (
|
||||||
download: Download,
|
url: string,
|
||||||
headers: Record<string, string>,
|
download: Download,
|
||||||
) => {
|
headers: Record<string, string>,
|
||||||
const segments = await extractSegmentsFromHLS(url, headers);
|
) => {
|
||||||
|
const segments = await extractSegmentsFromHLS(url, headers);
|
||||||
|
|
||||||
if (!segments || segments.length === 0) {
|
if (!segments || segments.length === 0) {
|
||||||
return removeDownload(download);
|
return removeDownload(download);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalSegments = segments.length;
|
const totalSegments = segments.length;
|
||||||
let segmentsDownloaded = 0;
|
let segmentsDownloaded = 0;
|
||||||
|
|
||||||
const segmentDir = `${FileSystem.cacheDirectory}movie-web/segments/`;
|
const segmentDir = `${FileSystem.cacheDirectory}movie-web/segments/`;
|
||||||
await ensureDirExists(segmentDir);
|
await ensureDirExists(segmentDir);
|
||||||
|
|
||||||
const updateProgress = () => {
|
const updateProgress = () => {
|
||||||
const progress = segmentsDownloaded / totalSegments;
|
const progress = segmentsDownloaded / totalSegments;
|
||||||
updateDownloadItem(download.id, {
|
updateDownloadItem(download.id, {
|
||||||
progress,
|
progress,
|
||||||
downloaded: segmentsDownloaded,
|
downloaded: segmentsDownloaded,
|
||||||
fileSize: totalSegments,
|
fileSize: totalSegments,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const localSegmentPaths = [];
|
const localSegmentPaths = [];
|
||||||
|
|
||||||
|
for (const [index, segment] of segments.entries()) {
|
||||||
|
if (getCancellationFlag(download.id)) {
|
||||||
|
await cleanupDownload(segmentDir, download);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentFile = `${segmentDir}${index}.ts`;
|
||||||
|
localSegmentPaths.push(segmentFile);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadSegment(segment, segmentFile, headers);
|
||||||
|
|
||||||
|
if (getCancellationFlag(download.id)) {
|
||||||
|
await cleanupDownload(segmentDir, download);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentsDownloaded++;
|
||||||
|
updateProgress();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (getCancellationFlag(download.id)) {
|
||||||
|
await cleanupDownload(segmentDir, download);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [index, segment] of segments.entries()) {
|
|
||||||
if (getCancellationFlag(download.id)) {
|
if (getCancellationFlag(download.id)) {
|
||||||
await cleanupDownload(segmentDir, download);
|
return removeDownload(download);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDownloadItem(download.id, { status: "merging" });
|
||||||
|
const uri = await VideoManager.mergeVideos(
|
||||||
|
localSegmentPaths,
|
||||||
|
`${FileSystem.cacheDirectory}movie-web/output.mp4`,
|
||||||
|
);
|
||||||
|
const asset = await saveFileToMediaLibraryAndDeleteOriginal(
|
||||||
|
uri,
|
||||||
|
download,
|
||||||
|
);
|
||||||
|
return asset;
|
||||||
|
},
|
||||||
|
[
|
||||||
|
getCancellationFlag,
|
||||||
|
updateDownloadItem,
|
||||||
|
saveFileToMediaLibraryAndDeleteOriginal,
|
||||||
|
removeDownload,
|
||||||
|
cleanupDownload,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const startDownload = useCallback(
|
||||||
|
async (
|
||||||
|
url: string,
|
||||||
|
type: "mp4" | "hls",
|
||||||
|
media: ScrapeMedia,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<Asset | void> => {
|
||||||
|
const { allowMobileData } = useNetworkSettingsStore.getState();
|
||||||
|
|
||||||
|
const { type: networkType } = await Network.getNetworkStateAsync();
|
||||||
|
|
||||||
|
if (networkType === NetworkStateType.CELLULAR && !allowMobileData) {
|
||||||
|
showToast("Mobile data downloads are disabled", {
|
||||||
|
burntOptions: { preset: "error" },
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const segmentFile = `${segmentDir}${index}.ts`;
|
const { status } = await MediaLibrary.requestPermissionsAsync();
|
||||||
localSegmentPaths.push(segmentFile);
|
if (status !== MediaLibrary.PermissionStatus.GRANTED) {
|
||||||
|
showToast("Permission denied", {
|
||||||
try {
|
burntOptions: { preset: "error" },
|
||||||
await downloadSegment(segment, segmentFile, headers);
|
});
|
||||||
|
return;
|
||||||
if (getCancellationFlag(download.id)) {
|
|
||||||
await cleanupDownload(segmentDir, download);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentsDownloaded++;
|
|
||||||
updateProgress();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
if (getCancellationFlag(download.id)) {
|
|
||||||
await cleanupDownload(segmentDir, download);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (getCancellationFlag(download.id)) {
|
const existingDownload = downloads.find(
|
||||||
return removeDownload(download);
|
(d) => d.media.tmdbId === media.tmdbId,
|
||||||
}
|
|
||||||
|
|
||||||
updateDownloadItem(download.id, { status: "merging" });
|
|
||||||
const uri = await VideoManager.mergeVideos(
|
|
||||||
localSegmentPaths,
|
|
||||||
`${FileSystem.cacheDirectory}movie-web/output.mp4`,
|
|
||||||
);
|
|
||||||
const asset = await saveFileToMediaLibraryAndDeleteOriginal(uri, download);
|
|
||||||
return asset;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startDownload = async (
|
|
||||||
url: string,
|
|
||||||
type: "mp4" | "hls",
|
|
||||||
media: ScrapeMedia,
|
|
||||||
headers?: Record<string, string>,
|
|
||||||
): Promise<Asset | void> => {
|
|
||||||
const { allowMobileData } = useNetworkSettingsStore.getState();
|
|
||||||
|
|
||||||
const { type: networkType } = await Network.getNetworkStateAsync();
|
|
||||||
|
|
||||||
if (networkType === NetworkStateType.CELLULAR && !allowMobileData) {
|
|
||||||
showToast("Mobile data downloads are disabled", {
|
|
||||||
burntOptions: { preset: "error" },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status } = await MediaLibrary.requestPermissionsAsync();
|
|
||||||
if (status !== MediaLibrary.PermissionStatus.GRANTED) {
|
|
||||||
showToast("Permission denied", {
|
|
||||||
burntOptions: { preset: "error" },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingDownload = downloads.find(
|
|
||||||
(d) => d.media.tmdbId === media.tmdbId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingDownload && media.type === "movie") {
|
|
||||||
showToast("Download already exists", {
|
|
||||||
burntOptions: { preset: "error" },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingDownload && media.type === "show") {
|
|
||||||
const existingEpisode = existingDownload.downloads.find(
|
|
||||||
(d) =>
|
|
||||||
d.media.type === "show" &&
|
|
||||||
d.media.episode.tmdbId === media.episode.tmdbId,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingEpisode) {
|
if (existingDownload && media.type === "movie") {
|
||||||
showToast("Download already exists", {
|
showToast("Download already exists", {
|
||||||
burntOptions: { preset: "error" },
|
burntOptions: { preset: "error" },
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
showToast("Download started", {
|
|
||||||
burntOptions: { preset: "none" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const newDownload: Download = {
|
if (existingDownload && media.type === "show") {
|
||||||
id: `download-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
const existingEpisode = existingDownload.downloads.find(
|
||||||
progress: 0,
|
(d) =>
|
||||||
speed: 0,
|
d.media.type === "show" &&
|
||||||
fileSize: 0,
|
d.media.episode.tmdbId === media.episode.tmdbId,
|
||||||
downloaded: 0,
|
|
||||||
type,
|
|
||||||
url,
|
|
||||||
status: "downloading",
|
|
||||||
media,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (existingDownload) {
|
|
||||||
existingDownload.downloads.push(newDownload);
|
|
||||||
setDownloads((prev) => {
|
|
||||||
return prev.map((d) =>
|
|
||||||
d.media.tmdbId === media.tmdbId ? existingDownload : d,
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setDownloads((prev) => {
|
|
||||||
return [...prev, { media, downloads: [newDownload] }];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "mp4") {
|
if (existingEpisode) {
|
||||||
const asset = await downloadMP4(url, newDownload, headers ?? {});
|
showToast("Download already exists", {
|
||||||
return asset;
|
burntOptions: { preset: "error" },
|
||||||
} else if (type === "hls") {
|
});
|
||||||
const asset = await downloadHLS(url, newDownload, headers ?? {});
|
return;
|
||||||
return asset;
|
}
|
||||||
}
|
}
|
||||||
};
|
showToast("Download started", {
|
||||||
|
burntOptions: { preset: "none" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const newDownload: Download = {
|
||||||
|
id: `download-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
progress: 0,
|
||||||
|
speed: 0,
|
||||||
|
fileSize: 0,
|
||||||
|
downloaded: 0,
|
||||||
|
type,
|
||||||
|
url,
|
||||||
|
status: "downloading",
|
||||||
|
media,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingDownload) {
|
||||||
|
existingDownload.downloads.push(newDownload);
|
||||||
|
setDownloads((prev) => {
|
||||||
|
return prev.map((d) =>
|
||||||
|
d.media.tmdbId === media.tmdbId ? existingDownload : d,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setDownloads((prev) => {
|
||||||
|
return [...prev, { media, downloads: [newDownload] }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "mp4") {
|
||||||
|
const asset = await downloadMP4(url, newDownload, headers ?? {});
|
||||||
|
return asset;
|
||||||
|
} else if (type === "hls") {
|
||||||
|
const asset = await downloadHLS(url, newDownload, headers ?? {});
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[downloads, showToast, setDownloads, downloadMP4, downloadHLS],
|
||||||
|
);
|
||||||
|
|
||||||
const downloadSegment = async (
|
const downloadSegment = async (
|
||||||
segmentUrl: string,
|
segmentUrl: string,
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@movie-web/prettier-config",
|
"prettier": "@movie-web/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@movie-web/providers": "^2.2.9",
|
"@movie-web/providers": "^2.3.0",
|
||||||
"parse-hls": "^1.0.7",
|
"parse-hls": "^1.0.7",
|
||||||
"srt-webvtt": "^2.0.0",
|
"srt-webvtt": "^2.0.0",
|
||||||
"tmdb-ts": "^1.6.1"
|
"tmdb-ts": "^1.6.1"
|
||||||
|
590
pnpm-lock.yaml
generated
590
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user