[web] Create package for sharing code between photos and cast (#1470)
This commit is contained in:
commit
626321b6d1
|
@ -3,11 +3,11 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@/media": "*",
|
||||
"@/next": "*",
|
||||
"@ente/accounts": "*",
|
||||
"@ente/eslint-config": "*",
|
||||
"@ente/shared": "*",
|
||||
"jszip": "3.10.1",
|
||||
"mime-types": "^2.1.35"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import JSZip from "jszip";
|
||||
import { EnteFile } from "types/file";
|
||||
import {
|
||||
getFileExtensionWithDot,
|
||||
getFileNameWithoutExtension,
|
||||
} from "utils/file";
|
||||
|
||||
class LivePhoto {
|
||||
image: Uint8Array;
|
||||
video: Uint8Array;
|
||||
imageNameTitle: string;
|
||||
videoNameTitle: string;
|
||||
}
|
||||
|
||||
export const decodeLivePhoto = async (file: EnteFile, zipBlob: Blob) => {
|
||||
const originalName = getFileNameWithoutExtension(file.metadata.title);
|
||||
const zip = await JSZip.loadAsync(zipBlob, { createFolders: true });
|
||||
|
||||
const livePhoto = new LivePhoto();
|
||||
for (const zipFilename in zip.files) {
|
||||
if (zipFilename.startsWith("image")) {
|
||||
livePhoto.imageNameTitle =
|
||||
originalName + getFileExtensionWithDot(zipFilename);
|
||||
livePhoto.image = await zip.files[zipFilename].async("uint8array");
|
||||
} else if (zipFilename.startsWith("video")) {
|
||||
livePhoto.videoNameTitle =
|
||||
originalName + getFileExtensionWithDot(zipFilename);
|
||||
livePhoto.video = await zip.files[zipFilename].async("uint8array");
|
||||
}
|
||||
}
|
||||
return livePhoto;
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { decodeLivePhoto } from "@/media/live-photo";
|
||||
import log from "@/next/log";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { FILE_TYPE, RAW_FORMATS } from "constants/file";
|
||||
import CastDownloadManager from "services/castDownloadManager";
|
||||
import { decodeLivePhoto } from "services/livePhotoService";
|
||||
import { getFileType } from "services/typeDetectionService";
|
||||
import {
|
||||
EncryptedEnteFile,
|
||||
|
@ -85,18 +85,6 @@ export async function decryptFile(
|
|||
}
|
||||
}
|
||||
|
||||
export function getFileNameWithoutExtension(filename: string) {
|
||||
const lastDotPosition = filename.lastIndexOf(".");
|
||||
if (lastDotPosition === -1) return filename;
|
||||
else return filename.slice(0, lastDotPosition);
|
||||
}
|
||||
|
||||
export function getFileExtensionWithDot(filename: string) {
|
||||
const lastDotPosition = filename.lastIndexOf(".");
|
||||
if (lastDotPosition === -1) return "";
|
||||
else return filename.slice(lastDotPosition);
|
||||
}
|
||||
|
||||
export function generateStreamFromArrayBuffer(data: Uint8Array) {
|
||||
return new ReadableStream({
|
||||
async start(controller: ReadableStreamDefaultController) {
|
||||
|
@ -115,6 +103,18 @@ export function isRawFileFromFileName(fileName: string) {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Note: File name for local EnteFile objects]
|
||||
*
|
||||
* The title property in a file's metadata is the original file's name. The
|
||||
* metadata of a file cannot be edited. So if later on the file's name is
|
||||
* changed, then the edit is stored in the `editedName` property of the public
|
||||
* metadata of the file.
|
||||
*
|
||||
* This function merges these edits onto the file object that we use locally.
|
||||
* Effectively, post this step, the file's metadata.title can be used in lieu of
|
||||
* its filename.
|
||||
*/
|
||||
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
|
||||
return files.map((file) => {
|
||||
if (file.pubMagicMetadata?.data.editedTime) {
|
||||
|
@ -137,8 +137,11 @@ export const getPreviewableImage = async (
|
|||
await CastDownloadManager.downloadFile(castToken, file),
|
||||
).blob();
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
fileBlob = new Blob([livePhoto.image]);
|
||||
const { imageData } = await decodeLivePhoto(
|
||||
file.metadata.title,
|
||||
fileBlob,
|
||||
);
|
||||
fileBlob = new Blob([imageData]);
|
||||
}
|
||||
const fileType = await getFileType(
|
||||
new File([fileBlob], file.metadata.title),
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@/media": "*",
|
||||
"@/next": "*",
|
||||
"@date-io/date-fns": "^2.14.0",
|
||||
"@ente/accounts": "*",
|
||||
|
@ -25,7 +26,6 @@
|
|||
"hdbscan": "0.0.1-alpha.5",
|
||||
"heic-convert": "^2.0.0",
|
||||
"idb": "^7.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-defaulticon-compatibility": "^0.1.1",
|
||||
"localforage": "^1.9.0",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { nameAndExtension } from "@/next/file";
|
||||
import log from "@/next/log";
|
||||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
import PhotoOutlined from "@mui/icons-material/PhotoOutlined";
|
||||
|
@ -7,11 +8,7 @@ import { FILE_TYPE } from "constants/file";
|
|||
import { useEffect, useState } from "react";
|
||||
import { EnteFile } from "types/file";
|
||||
import { makeHumanReadableStorage } from "utils/billing";
|
||||
import {
|
||||
changeFileName,
|
||||
splitFilenameAndExtension,
|
||||
updateExistingFilePubMetadata,
|
||||
} from "utils/file";
|
||||
import { changeFileName, updateExistingFilePubMetadata } from "utils/file";
|
||||
import { FileNameEditDialog } from "./FileNameEditDialog";
|
||||
import InfoItem from "./InfoItem";
|
||||
|
||||
|
@ -65,9 +62,7 @@ export function RenderFileName({
|
|||
const [extension, setExtension] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const [filename, extension] = splitFilenameAndExtension(
|
||||
file.metadata.title,
|
||||
);
|
||||
const [filename, extension] = nameAndExtension(file.metadata.title);
|
||||
setFilename(filename);
|
||||
setExtension(extension);
|
||||
}, [file]);
|
||||
|
|
|
@ -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 "../livePhotoService";
|
||||
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<any>,
|
||||
file: EnteFile,
|
||||
) {
|
||||
const electron = ensureElectron();
|
||||
const fs = ensureElectron().fs;
|
||||
const fileBlob = await new Response(fileStream).blob();
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
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) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { decodeLivePhoto } from "@/media/live-photo";
|
||||
import { ensureElectron } from "@/next/electron";
|
||||
import log from "@/next/log";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
|
@ -7,7 +8,6 @@ import { FILE_TYPE } from "constants/file";
|
|||
import { getLocalCollections } from "services/collectionService";
|
||||
import downloadManager from "services/download";
|
||||
import { getAllLocalFiles } from "services/fileService";
|
||||
import { decodeLivePhoto } from "services/livePhotoService";
|
||||
import { Collection } from "types/collection";
|
||||
import {
|
||||
CollectionExportNames,
|
||||
|
@ -21,11 +21,11 @@ import {
|
|||
} from "types/export";
|
||||
import { EnteFile } from "types/file";
|
||||
import { getNonEmptyPersonalCollections } from "utils/collection";
|
||||
import { splitFilenameAndExtension } from "utils/ffmpeg";
|
||||
import {
|
||||
getIDBasedSortedFiles,
|
||||
getPersonalFiles,
|
||||
mergeMetadata,
|
||||
splitFilenameAndExtension,
|
||||
} from "utils/file";
|
||||
import {
|
||||
safeDirectoryName,
|
||||
|
@ -318,15 +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(file, fileBlob);
|
||||
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(
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import JSZip from "jszip";
|
||||
import { EnteFile } from "types/file";
|
||||
import {
|
||||
getFileExtensionWithDot,
|
||||
getFileNameWithoutExtension,
|
||||
} from "utils/file";
|
||||
|
||||
class LivePhoto {
|
||||
image: Uint8Array;
|
||||
video: Uint8Array;
|
||||
imageNameTitle: string;
|
||||
videoNameTitle: string;
|
||||
}
|
||||
|
||||
export const decodeLivePhoto = async (file: EnteFile, zipBlob: Blob) => {
|
||||
const originalName = getFileNameWithoutExtension(file.metadata.title);
|
||||
const zip = await JSZip.loadAsync(zipBlob, { createFolders: true });
|
||||
|
||||
const livePhoto = new LivePhoto();
|
||||
for (const zipFilename in zip.files) {
|
||||
if (zipFilename.startsWith("image")) {
|
||||
livePhoto.imageNameTitle =
|
||||
originalName + getFileExtensionWithDot(zipFilename);
|
||||
livePhoto.image = await zip.files[zipFilename].async("uint8array");
|
||||
} else if (zipFilename.startsWith("video")) {
|
||||
livePhoto.videoNameTitle =
|
||||
originalName + getFileExtensionWithDot(zipFilename);
|
||||
livePhoto.video = await zip.files[zipFilename].async("uint8array");
|
||||
}
|
||||
}
|
||||
return livePhoto;
|
||||
};
|
||||
|
||||
export const encodeLivePhoto = async (livePhoto: LivePhoto) => {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"image" + getFileExtensionWithDot(livePhoto.imageNameTitle),
|
||||
livePhoto.image,
|
||||
);
|
||||
zip.file(
|
||||
"video" + getFileExtensionWithDot(livePhoto.videoNameTitle),
|
||||
livePhoto.video,
|
||||
);
|
||||
return await zip.generateAsync({ type: "uint8array" });
|
||||
};
|
|
@ -1,10 +1,10 @@
|
|||
import { encodeLivePhoto } from "@/media/live-photo";
|
||||
import log from "@/next/log";
|
||||
import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import { Remote } from "comlink";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import { LIVE_PHOTO_ASSET_SIZE_LIMIT } from "constants/upload";
|
||||
import { encodeLivePhoto } from "services/livePhotoService";
|
||||
import { getFileType } from "services/typeDetectionService";
|
||||
import {
|
||||
ElectronFile,
|
||||
|
@ -14,12 +14,6 @@ import {
|
|||
LivePhotoAssets,
|
||||
ParsedMetadataJSONMap,
|
||||
} from "types/upload";
|
||||
import {
|
||||
getFileExtensionWithDot,
|
||||
getFileNameWithoutExtension,
|
||||
isImageOrVideo,
|
||||
splitFilenameAndExtension,
|
||||
} from "utils/file";
|
||||
import { getFileTypeFromExtensionForLivePhotoClustering } from "utils/file/livePhoto";
|
||||
import { getUint8ArrayView } from "../readerService";
|
||||
import { extractFileMetadata } from "./fileService";
|
||||
|
@ -107,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,
|
||||
|
@ -304,3 +298,28 @@ function removePotentialLivePhotoSuffix(
|
|||
return filenameWithoutExtension;
|
||||
}
|
||||
}
|
||||
|
||||
function getFileNameWithoutExtension(filename: string) {
|
||||
const lastDotPosition = filename.lastIndexOf(".");
|
||||
if (lastDotPosition === -1) return filename;
|
||||
else return filename.slice(0, lastDotPosition);
|
||||
}
|
||||
|
||||
function getFileExtensionWithDot(filename: string) {
|
||||
const lastDotPosition = filename.lastIndexOf(".");
|
||||
if (lastDotPosition === -1) return "";
|
||||
else return filename.slice(lastDotPosition);
|
||||
}
|
||||
|
||||
function splitFilenameAndExtension(filename: string): [string, string] {
|
||||
const lastDotPosition = filename.lastIndexOf(".");
|
||||
if (lastDotPosition === -1) return [filename, null];
|
||||
else
|
||||
return [
|
||||
filename.slice(0, lastDotPosition),
|
||||
filename.slice(lastDotPosition + 1),
|
||||
];
|
||||
}
|
||||
|
||||
const isImageOrVideo = (fileType: FILE_TYPE) =>
|
||||
[FILE_TYPE.IMAGE, FILE_TYPE.VIDEO].includes(fileType);
|
||||
|
|
|
@ -24,6 +24,11 @@ export function isDataStream(object: any): object is DataStream {
|
|||
export type Logger = (message: string) => void;
|
||||
|
||||
export interface Metadata {
|
||||
/**
|
||||
* The file name.
|
||||
*
|
||||
* See: [Note: File name for local EnteFile objects]
|
||||
*/
|
||||
title: string;
|
||||
creationTime: number;
|
||||
modificationTime: number;
|
||||
|
|
|
@ -65,13 +65,3 @@ function parseCreationTime(creationTime: string) {
|
|||
}
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
export function splitFilenameAndExtension(filename: string): [string, string] {
|
||||
const lastDotPosition = filename.lastIndexOf(".");
|
||||
if (lastDotPosition === -1) return [filename, null];
|
||||
else
|
||||
return [
|
||||
filename.slice(0, lastDotPosition),
|
||||
filename.slice(lastDotPosition + 1),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { decodeLivePhoto } from "@/media/live-photo";
|
||||
import { convertBytesToHumanReadable } from "@/next/file";
|
||||
import log from "@/next/log";
|
||||
import type { Electron } from "@/next/types/ipc";
|
||||
|
@ -32,7 +33,6 @@ import {
|
|||
updateFilePublicMagicMetadata,
|
||||
} from "services/fileService";
|
||||
import heicConversionService from "services/heicConversionService";
|
||||
import { decodeLivePhoto } from "services/livePhotoService";
|
||||
import { getFileType } from "services/typeDetectionService";
|
||||
import { updateFileCreationDateInEXIF } from "services/upload/exifService";
|
||||
import {
|
||||
|
@ -97,19 +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, 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),
|
||||
|
@ -247,18 +248,6 @@ export async function decryptFile(
|
|||
}
|
||||
}
|
||||
|
||||
export function getFileNameWithoutExtension(filename: string) {
|
||||
const lastDotPosition = filename.lastIndexOf(".");
|
||||
if (lastDotPosition === -1) return filename;
|
||||
else return filename.slice(0, lastDotPosition);
|
||||
}
|
||||
|
||||
export function getFileExtensionWithDot(filename: string) {
|
||||
const lastDotPosition = filename.lastIndexOf(".");
|
||||
if (lastDotPosition === -1) return "";
|
||||
else return filename.slice(lastDotPosition);
|
||||
}
|
||||
|
||||
export function splitFilenameAndExtension(filename: string): [string, string] {
|
||||
const lastDotPosition = filename.lastIndexOf(".");
|
||||
if (lastDotPosition === -1) return [filename, null];
|
||||
|
@ -355,13 +344,13 @@ async function getRenderableLivePhotoURL(
|
|||
fileBlob: Blob,
|
||||
forceConvert: boolean,
|
||||
): Promise<LivePhotoSourceURL> {
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const livePhoto = await decodeLivePhoto(file.metadata.title, fileBlob);
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
|
@ -374,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,
|
||||
|
@ -813,21 +801,22 @@ async function downloadFileDesktop(
|
|||
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const fileBlob = await new Response(updatedStream).blob();
|
||||
const livePhoto = await decodeLivePhoto(file, fileBlob);
|
||||
const { imageFileName, imageData, videoFileName, videoData } =
|
||||
await decodeLivePhoto(file.metadata.title, fileBlob);
|
||||
const imageExportName = await safeFileName(
|
||||
downloadDir,
|
||||
livePhoto.imageNameTitle,
|
||||
imageFileName,
|
||||
fs.exists,
|
||||
);
|
||||
const imageStream = generateStreamFromArrayBuffer(livePhoto.image);
|
||||
const imageStream = generateStreamFromArrayBuffer(imageData);
|
||||
await writeStream(`${downloadDir}/${imageExportName}`, imageStream);
|
||||
try {
|
||||
const videoExportName = await safeFileName(
|
||||
downloadDir,
|
||||
livePhoto.videoNameTitle,
|
||||
videoFileName,
|
||||
fs.exists,
|
||||
);
|
||||
const videoStream = generateStreamFromArrayBuffer(livePhoto.video);
|
||||
const videoStream = generateStreamFromArrayBuffer(videoData);
|
||||
await writeStream(`${downloadDir}/${videoExportName}`, videoStream);
|
||||
} catch (e) {
|
||||
await fs.rm(`${downloadDir}/${imageExportName}`);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { decodeLivePhoto } from "@/media/live-photo";
|
||||
import log from "@/next/log";
|
||||
import { FILE_TYPE } from "constants/file";
|
||||
import PQueue from "p-queue";
|
||||
import DownloadManager from "services/download";
|
||||
import { getLocalFiles } from "services/fileService";
|
||||
import { decodeLivePhoto } from "services/livePhotoService";
|
||||
import { EnteFile } from "types/file";
|
||||
import { Dimensions } from "types/image";
|
||||
import {
|
||||
|
@ -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, 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]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ with Next.js.
|
|||
|
||||
For more details, see [translations.md](translations.md).
|
||||
|
||||
## Meta Frameworks
|
||||
## Meta frameworks
|
||||
|
||||
### Next.js
|
||||
|
||||
|
@ -131,7 +131,12 @@ It is more lower level than Next, but the bells and whistles it doesn't have are
|
|||
the bells and whistles (and the accompanying complexity) that we don't need in
|
||||
some cases.
|
||||
|
||||
## Photos
|
||||
## Media
|
||||
|
||||
- "jszip" is used for reading zip files in JavaScript. Live photos are zip
|
||||
files under the hood.
|
||||
|
||||
## Photos app specific
|
||||
|
||||
### Misc
|
||||
|
||||
|
|
3
web/packages/media/.eslintrc.js
Normal file
3
web/packages/media/.eslintrc.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ["@/build-config/eslintrc-next"],
|
||||
};
|
11
web/packages/media/README.md
Normal file
11
web/packages/media/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
## @/media
|
||||
|
||||
A package for sharing code between our apps that show media (photos, videos).
|
||||
|
||||
Specifically, this is the intersection of code required by both the photos and
|
||||
cast apps.
|
||||
|
||||
### Packaging
|
||||
|
||||
This (internal) package exports a React TypeScript library. We rely on the
|
||||
importing project to transpile and bundle it.
|
87
web/packages/media/live-photo.ts
Normal file
87
web/packages/media/live-photo.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { fileNameFromComponents, nameAndExtension } from "@/next/file";
|
||||
import JSZip from "jszip";
|
||||
|
||||
/**
|
||||
* An in-memory representation of a live photo.
|
||||
*/
|
||||
interface LivePhoto {
|
||||
imageFileName: string;
|
||||
imageData: Uint8Array;
|
||||
videoFileName: string;
|
||||
videoData: Uint8Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a binary serialized representation of a live photo to an in-memory
|
||||
* {@link LivePhoto}.
|
||||
*
|
||||
* A live photo is a zip file containing two files - an image and a video. This
|
||||
* functions reads that zip file (blob), and return separate bytes (and
|
||||
* filenames) for the image and video parts.
|
||||
*
|
||||
* @param fileName The name of the overall live photo. Both the image and video
|
||||
* parts of the decompressed live photo use this as their name, combined with
|
||||
* their original extensions.
|
||||
*
|
||||
* @param zipBlob A blob contained the zipped data (i.e. the binary serialized
|
||||
* live photo).
|
||||
*/
|
||||
export const decodeLivePhoto = async (
|
||||
fileName: string,
|
||||
zipBlob: Blob,
|
||||
): Promise<LivePhoto> => {
|
||||
let imageFileName, videoFileName: string | undefined;
|
||||
let imageData, videoData: Uint8Array | undefined;
|
||||
|
||||
const [name] = nameAndExtension(fileName);
|
||||
const zip = await JSZip.loadAsync(zipBlob, { createFolders: true });
|
||||
|
||||
for (const zipFileName in zip.files) {
|
||||
if (zipFileName.startsWith("image")) {
|
||||
const [, imageExt] = nameAndExtension(zipFileName);
|
||||
imageFileName = fileNameFromComponents([name, imageExt]);
|
||||
imageData = await zip.files[zipFileName]?.async("uint8array");
|
||||
} else if (zipFileName.startsWith("video")) {
|
||||
const [, videoExt] = nameAndExtension(zipFileName);
|
||||
videoFileName = fileNameFromComponents([name, videoExt]);
|
||||
videoData = await zip.files[zipFileName]?.async("uint8array");
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a binary serialized representation of a live photo.
|
||||
*
|
||||
* This function takes the (in-memory) image and video data from the
|
||||
* {@link livePhoto} object, writes them to a zip file (using the respective
|
||||
* filenames), and returns the {@link Uint8Array} that represent the bytes of
|
||||
* this zip file.
|
||||
*
|
||||
* @param livePhoto The in-mem photo to serialized.
|
||||
*/
|
||||
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]), imageData);
|
||||
zip.file(fileNameFromComponents(["video", videoExt]), videoData);
|
||||
return await zip.generateAsync({ type: "uint8array" });
|
||||
};
|
9
web/packages/media/package.json
Normal file
9
web/packages/media/package.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "@/media",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@/next": "*",
|
||||
"jszip": "^3.10"
|
||||
}
|
||||
}
|
5
web/packages/media/tsconfig.json
Normal file
5
web/packages/media/tsconfig.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@/build-config/tsconfig-typecheck.json",
|
||||
/* Typecheck all files with the given extensions (here or in subfolders) */
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
|
@ -1,19 +1,34 @@
|
|||
import type { ElectronFile } from "./types/file";
|
||||
|
||||
/**
|
||||
* The two parts of a file name - the name itself, and an (optional) extension.
|
||||
*
|
||||
* The extension does not include the dot.
|
||||
*/
|
||||
type FileNameComponents = [name: string, extension: string | undefined];
|
||||
|
||||
/**
|
||||
* Split a filename into its components - the name itself, and the extension (if
|
||||
* any) - returning both. The dot is not included in either.
|
||||
*
|
||||
* For example, `foo-bar.png` will be split into ["foo-bar", "png"].
|
||||
*
|
||||
* See {@link fileNameFromComponents} for the inverse operation.
|
||||
*/
|
||||
export const nameAndExtension = (
|
||||
fileName: string,
|
||||
): [string, string | undefined] => {
|
||||
export const nameAndExtension = (fileName: string): FileNameComponents => {
|
||||
const i = fileName.lastIndexOf(".");
|
||||
if (i == -1) return [fileName, undefined];
|
||||
else return [fileName.slice(0, i), fileName.slice(i + 1)];
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a file name from its components (name and extension).
|
||||
*
|
||||
* Inverse of {@link nameAndExtension}.
|
||||
*/
|
||||
export const fileNameFromComponents = (components: FileNameComponents) =>
|
||||
components.filter((x) => !!x).join(".");
|
||||
|
||||
export function getFileNameSize(file: File | ElectronFile) {
|
||||
return `${file.name}_${convertBytesToHumanReadable(file.size)}`;
|
||||
}
|
||||
|
|
|
@ -3252,7 +3252,7 @@ jssha@~3.3.1:
|
|||
object.assign "^4.1.4"
|
||||
object.values "^1.1.6"
|
||||
|
||||
jszip@3.10.1:
|
||||
jszip@^3.10:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
|
||||
integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
|
||||
|
|
Loading…
Reference in a new issue