diff --git a/web/apps/cast/src/utils/file/index.ts b/web/apps/cast/src/utils/file/index.ts index 31ed5c577..60ec0e56e 100644 --- a/web/apps/cast/src/utils/file/index.ts +++ b/web/apps/cast/src/utils/file/index.ts @@ -137,11 +137,11 @@ export const getPreviewableImage = async ( await CastDownloadManager.downloadFile(castToken, file), ).blob(); if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - const livePhoto = await decodeLivePhoto( + const { imageData } = await decodeLivePhoto( file.metadata.title, fileBlob, ); - fileBlob = new Blob([livePhoto.image]); + fileBlob = new Blob([imageData]); } const fileType = await getFileType( new File([fileBlob], file.metadata.title), diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 37b228f60..882c36f9b 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -1,3 +1,4 @@ +import { decodeLivePhoto } from "@/media/live-photo"; import { ensureElectron } from "@/next/electron"; import log from "@/next/log"; import { CustomError } from "@ente/shared/error"; @@ -38,7 +39,6 @@ import { writeStream } from "utils/native-stream"; import { getAllLocalCollections } from "../collectionService"; import downloadManager from "../download"; import { getAllLocalFiles } from "../fileService"; -import { decodeLivePhoto } from "@/media/live-photo"; import { migrateExport } from "./migration"; /** Name of the JSON file in which we keep the state of the export. */ @@ -1015,18 +1015,18 @@ class ExportService { fileStream: ReadableStream, file: EnteFile, ) { - const electron = ensureElectron(); + const fs = ensureElectron().fs; const fileBlob = await new Response(fileStream).blob(); const livePhoto = await decodeLivePhoto(file.metadata.title, fileBlob); const imageExportName = await safeFileName( collectionExportPath, - livePhoto.imageNameTitle, - electron.fs.exists, + livePhoto.imageFileName, + fs.exists, ); const videoExportName = await safeFileName( collectionExportPath, - livePhoto.videoNameTitle, - electron.fs.exists, + livePhoto.videoFileName, + fs.exists, ); const livePhotoExportName = getLivePhotoExportName( imageExportName, @@ -1038,7 +1038,9 @@ class ExportService { livePhotoExportName, ); try { - const imageStream = generateStreamFromArrayBuffer(livePhoto.image); + const imageStream = generateStreamFromArrayBuffer( + livePhoto.imageData, + ); await this.saveMetadataFile( collectionExportPath, imageExportName, @@ -1049,7 +1051,9 @@ class ExportService { imageStream, ); - const videoStream = generateStreamFromArrayBuffer(livePhoto.video); + const videoStream = generateStreamFromArrayBuffer( + livePhoto.videoData, + ); await this.saveMetadataFile( collectionExportPath, videoExportName, @@ -1061,9 +1065,7 @@ class ExportService { videoStream, ); } catch (e) { - await electron.fs.rm( - `${collectionExportPath}/${imageExportName}`, - ); + await fs.rm(`${collectionExportPath}/${imageExportName}`); throw e; } } catch (e) { diff --git a/web/apps/photos/src/services/export/migration.ts b/web/apps/photos/src/services/export/migration.ts index 0403f93f7..3f471b539 100644 --- a/web/apps/photos/src/services/export/migration.ts +++ b/web/apps/photos/src/services/export/migration.ts @@ -318,18 +318,18 @@ async function getFileExportNamesFromExportedFiles( if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { const fileStream = await downloadManager.getFile(file); const fileBlob = await new Response(fileStream).blob(); - const livePhoto = await decodeLivePhoto( + const { imageFileName, videoFileName } = await decodeLivePhoto( file.metadata.title, fileBlob, ); const imageExportName = getUniqueFileExportNameForMigration( collectionPath, - livePhoto.imageNameTitle, + imageFileName, usedFilePaths, ); const videoExportName = getUniqueFileExportNameForMigration( collectionPath, - livePhoto.videoNameTitle, + videoFileName, usedFilePaths, ); fileExportName = getLivePhotoExportName( diff --git a/web/apps/photos/src/services/upload/livePhotoService.ts b/web/apps/photos/src/services/upload/livePhotoService.ts index cb0ac9318..c203c4d5f 100644 --- a/web/apps/photos/src/services/upload/livePhotoService.ts +++ b/web/apps/photos/src/services/upload/livePhotoService.ts @@ -101,16 +101,16 @@ export async function readLivePhoto( }, ); - const image = await getUint8ArrayView(livePhotoAssets.image); + const imageData = await getUint8ArrayView(livePhotoAssets.image); - const video = await getUint8ArrayView(livePhotoAssets.video); + const videoData = await getUint8ArrayView(livePhotoAssets.video); return { filedata: await encodeLivePhoto({ - image, - video, - imageNameTitle: livePhotoAssets.image.name, - videoNameTitle: livePhotoAssets.video.name, + imageFileName: livePhotoAssets.image.name, + imageData, + videoFileName: livePhotoAssets.video.name, + videoData, }), thumbnail, hasStaticThumbnail, diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index c1ee83448..d4c9b81d8 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -97,22 +97,20 @@ export async function downloadFile(file: EnteFile) { await DownloadManager.getFile(file), ).blob(); if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - const livePhoto = await decodeLivePhoto( - file.metadata.title, - fileBlob, - ); - const image = new File([livePhoto.image], livePhoto.imageNameTitle); + const { imageFileName, imageData, videoFileName, videoData } = + await decodeLivePhoto(file.metadata.title, fileBlob); + const image = new File([imageData], imageFileName); const imageType = await getFileType(image); const tempImageURL = URL.createObjectURL( - new Blob([livePhoto.image], { type: imageType.mimeType }), + new Blob([imageData], { type: imageType.mimeType }), ); - const video = new File([livePhoto.video], livePhoto.videoNameTitle); + const video = new File([videoData], videoFileName); const videoType = await getFileType(video); const tempVideoURL = URL.createObjectURL( - new Blob([livePhoto.video], { type: videoType.mimeType }), + new Blob([videoData], { type: videoType.mimeType }), ); - downloadUsingAnchor(tempImageURL, livePhoto.imageNameTitle); - downloadUsingAnchor(tempVideoURL, livePhoto.videoNameTitle); + downloadUsingAnchor(tempImageURL, imageFileName); + downloadUsingAnchor(tempVideoURL, videoFileName); } else { const fileType = await getFileType( new File([fileBlob], file.metadata.title), @@ -350,9 +348,9 @@ async function getRenderableLivePhotoURL( const getRenderableLivePhotoImageURL = async () => { try { - const imageBlob = new Blob([livePhoto.image]); + const imageBlob = new Blob([livePhoto.imageData]); const convertedImageBlob = await getRenderableImage( - livePhoto.imageNameTitle, + livePhoto.imageFileName, imageBlob, ); @@ -365,10 +363,9 @@ async function getRenderableLivePhotoURL( const getRenderableLivePhotoVideoURL = async () => { try { - const videoBlob = new Blob([livePhoto.video]); - + const videoBlob = new Blob([livePhoto.videoData]); const convertedVideoBlob = await getPlayableVideo( - livePhoto.videoNameTitle, + livePhoto.videoFileName, videoBlob, forceConvert, true, diff --git a/web/apps/photos/src/utils/machineLearning/index.ts b/web/apps/photos/src/utils/machineLearning/index.ts index 6a5984a4c..a89bccc4c 100644 --- a/web/apps/photos/src/utils/machineLearning/index.ts +++ b/web/apps/photos/src/utils/machineLearning/index.ts @@ -134,11 +134,11 @@ async function getOriginalConvertedFile(file: EnteFile, queue?: PQueue) { if (file.metadata.fileType === FILE_TYPE.IMAGE) { return await getRenderableImage(file.metadata.title, fileBlob); } else { - const livePhoto = await decodeLivePhoto(file.metadata.title, fileBlob); - return await getRenderableImage( - livePhoto.imageNameTitle, - new Blob([livePhoto.image]), + const { imageFileName, imageData } = await decodeLivePhoto( + file.metadata.title, + fileBlob, ); + return await getRenderableImage(imageFileName, new Blob([imageData])); } } diff --git a/web/packages/media/live-photo.ts b/web/packages/media/live-photo.ts index 96be4f613..16143ca13 100644 --- a/web/packages/media/live-photo.ts +++ b/web/packages/media/live-photo.ts @@ -1,11 +1,14 @@ import { fileNameFromComponents, nameAndExtension } from "@/next/file"; import JSZip from "jszip"; -class LivePhoto { - image: Uint8Array; - video: Uint8Array; - imageNameTitle: string; - videoNameTitle: string; +/** + * An in-memory representation of a live photo. + */ +interface LivePhoto { + imageFileName: string; + imageData: Uint8Array; + videoFileName: string; + videoData: Uint8Array; } /** @@ -23,23 +26,39 @@ class LivePhoto { * @param zipBlob A blob contained the zipped data (i.e. the binary serialized * live photo). */ -export const decodeLivePhoto = async (fileName: string, zipBlob: Blob) => { +export const decodeLivePhoto = async ( + fileName: string, + zipBlob: Blob, +): Promise => { + let imageFileName, videoFileName: string | undefined; + let imageData, videoData: Uint8Array | undefined; + const [name] = nameAndExtension(fileName); const zip = await JSZip.loadAsync(zipBlob, { createFolders: true }); - const livePhoto = new LivePhoto(); for (const zipFileName in zip.files) { if (zipFileName.startsWith("image")) { const [, imageExt] = nameAndExtension(zipFileName); - livePhoto.imageNameTitle = fileNameFromComponents([name, imageExt]); - livePhoto.image = await zip.files[zipFileName].async("uint8array"); + imageFileName = fileNameFromComponents([name, imageExt]); + imageData = await zip.files[zipFileName]?.async("uint8array"); } else if (zipFileName.startsWith("video")) { const [, videoExt] = nameAndExtension(zipFileName); - livePhoto.videoNameTitle = fileNameFromComponents([name, videoExt]); - livePhoto.video = await zip.files[zipFileName].async("uint8array"); + videoFileName = fileNameFromComponents([name, videoExt]); + videoData = await zip.files[zipFileName]?.async("uint8array"); } } - return livePhoto; + + if (!imageFileName || !imageData) + throw new Error( + `Decoded live photo ${fileName} does not have an image`, + ); + + if (!videoFileName || !videoData) + throw new Error( + `Decoded live photo ${fileName} does not have an image`, + ); + + return { imageFileName, imageData, videoFileName, videoData }; }; /** @@ -52,12 +71,17 @@ export const decodeLivePhoto = async (fileName: string, zipBlob: Blob) => { * * @param livePhoto The in-mem photo to serialized. */ -export const encodeLivePhoto = async (livePhoto: LivePhoto) => { - const [, imageExt] = nameAndExtension(livePhoto.imageNameTitle); - const [, videoExt] = nameAndExtension(livePhoto.videoNameTitle); +export const encodeLivePhoto = async ({ + imageFileName, + imageData, + videoFileName, + videoData, +}: LivePhoto) => { + const [, imageExt] = nameAndExtension(imageFileName); + const [, videoExt] = nameAndExtension(videoFileName); const zip = new JSZip(); - zip.file(fileNameFromComponents(["image", imageExt]), livePhoto.image); - zip.file(fileNameFromComponents(["video", videoExt]), livePhoto.video); + zip.file(fileNameFromComponents(["image", imageExt]), imageData); + zip.file(fileNameFromComponents(["video", videoExt]), videoData); return await zip.generateAsync({ type: "uint8array" }); };