This commit is contained in:
Manav Rathi 2024-04-24 11:14:53 +05:30
parent bded3c6706
commit 3b0433c4ab
No known key found for this signature in database
2 changed files with 169 additions and 170 deletions

View file

@ -1,4 +1,4 @@
import { getFileNameSize, nameAndExtension } from "@/next/file";
import { getFileNameSize } from "@/next/file";
import log from "@/next/log";
import { ElectronFile } from "@/next/types/file";
import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker";
@ -21,12 +21,9 @@ import {
Metadata,
ParsedExtractedMetadata,
type DataStream,
type FileWithCollection,
type FileWithCollection2,
type LivePhotoAssets2,
type UploadAsset2,
} from "types/upload";
import { getFileTypeFromExtensionForLivePhotoClustering } from "utils/file/livePhoto";
import { getEXIFLocation, getEXIFTime, getParsedExifData } from "./exifService";
import {
MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT,
@ -295,171 +292,6 @@ async function extractLivePhotoMetadata(
};
}
export function getLivePhotoSize(livePhotoAssets: LivePhotoAssets) {
return livePhotoAssets.image.size + livePhotoAssets.video.size;
}
/**
* Go through the given files, combining any sibling image + video assets into a
* single live photo when appropriate.
*/
export const clusterLivePhotos = (mediaFiles: FileWithCollection2[]) => {
const result: FileWithCollection2[] = [];
mediaFiles
.sort((f, g) =>
nameAndExtension(getFileName(f.file))[0].localeCompare(
nameAndExtension(getFileName(g.file))[0],
),
)
.sort((f, g) => f.collectionID - g.collectionID);
let index = 0;
while (index < mediaFiles.length - 1) {
const f = mediaFiles[index];
const g = mediaFiles[index + 1];
const fFileType = getFileTypeFromExtensionForLivePhotoClustering(
getFileName(f.file),
);
const gFileType = getFileTypeFromExtensionForLivePhotoClustering(
getFileName(g.file),
);
const fa: PotentialLivePhotoAsset = {
collectionID: f.collectionID,
fileType: fFileType,
fileName: getFileName(f.file),
/* TODO(MR): ElectronFile changes */
size: (f as FileWithCollection).file.size,
};
const ga: PotentialLivePhotoAsset = {
collectionID: g.collectionID,
fileType: gFileType,
fileName: getFileName(g.file),
/* TODO(MR): ElectronFile changes */
size: (g as FileWithCollection).file.size,
};
if (areLivePhotoAssets(fa, ga)) {
let imageFile: File | ElectronFile | string;
let videoFile: File | ElectronFile | string;
if (
fFileType === FILE_TYPE.IMAGE &&
gFileType === FILE_TYPE.VIDEO
) {
imageFile = f.file;
videoFile = g.file;
} else {
videoFile = f.file;
imageFile = g.file;
}
const livePhotoLocalID = f.localID;
result.push({
localID: livePhotoLocalID,
collectionID: f.collectionID,
isLivePhoto: true,
livePhotoAssets: {
image: imageFile,
video: videoFile,
},
});
index += 2;
} else {
result.push({
...f,
isLivePhoto: false,
});
index += 1;
}
}
if (index === mediaFiles.length - 1) {
result.push({
...mediaFiles[index],
isLivePhoto: false,
});
}
return result;
};
interface PotentialLivePhotoAsset {
collectionID: number;
fileType: FILE_TYPE;
fileName: string;
size: number;
}
const areLivePhotoAssets = (
f: PotentialLivePhotoAsset,
g: PotentialLivePhotoAsset,
) => {
if (f.collectionID != g.collectionID) return false;
const [fName, fExt] = nameAndExtension(f.fileName);
const [gName, gExt] = nameAndExtension(g.fileName);
let fPrunedName: string;
let gPrunedName: string;
if (f.fileType == FILE_TYPE.IMAGE && g.fileType == FILE_TYPE.VIDEO) {
fPrunedName = removePotentialLivePhotoSuffix(
fName,
// A Google Live Photo image file can have video extension appended
// as suffix, so we pass that to removePotentialLivePhotoSuffix to
// remove it.
//
// Example: IMG_20210630_0001.mp4.jpg (Google Live Photo image file)
gExt ? `.${gExt}` : undefined,
);
gPrunedName = removePotentialLivePhotoSuffix(gName);
} else if (f.fileType == FILE_TYPE.VIDEO && g.fileType == FILE_TYPE.IMAGE) {
fPrunedName = removePotentialLivePhotoSuffix(fName);
gPrunedName = removePotentialLivePhotoSuffix(
gName,
fExt ? `.${fExt}` : undefined,
);
} else {
return false;
}
if (fPrunedName != gPrunedName) return false;
// Also check that the size of an individual Live Photo asset is less than
// an (arbitrary) limit. This should be true in practice as the videos for a
// live photo are a few seconds long. Further on, the zipping library that
// we use doesn't support stream as a input.
const maxAssetSize = 20 * 1024 * 1024; /* 20MB */
if (f.size > maxAssetSize || g.size > maxAssetSize) {
log.info(
`Not classifying assets with too large sizes ${[f.size, g.size]} as a live photo`,
);
return false;
}
return true;
};
const removePotentialLivePhotoSuffix = (name: string, suffix?: string) => {
const suffix_3 = "_3";
// The icloud-photos-downloader library appends _HVEC to the end of the
// filename in case of live photos.
//
// https://github.com/icloud-photos-downloader/icloud_photos_downloader
const suffix_hvec = "_HVEC";
let foundSuffix: string | undefined;
if (name.endsWith(suffix_3)) {
foundSuffix = suffix_3;
} else if (
name.endsWith(suffix_hvec) ||
name.endsWith(suffix_hvec.toLowerCase())
) {
foundSuffix = suffix_hvec;
} else if (suffix) {
if (name.endsWith(suffix) || name.endsWith(suffix.toLowerCase())) {
foundSuffix = suffix;
}
}
return foundSuffix ? name.slice(0, foundSuffix.length * -1) : name;
};
async function getFileHash(
worker: Remote<DedicatedCryptoWorker>,
file: File | ElectronFile,
@ -499,3 +331,7 @@ async function getFileHash(
log.info(`file hashing failed ${getFileNameSize(file)} ,${e.message} `);
}
}
export function getLivePhotoSize(livePhotoAssets: LivePhotoAssets) {
return livePhotoAssets.image.size + livePhotoAssets.video.size;
}

View file

@ -1,4 +1,5 @@
import { ensureElectron } from "@/next/electron";
import { nameAndExtension } from "@/next/file";
import log from "@/next/log";
import { ElectronFile } from "@/next/types/file";
import { ComlinkWorker } from "@/next/worker/comlink-worker";
@ -9,6 +10,7 @@ import { Events, eventBus } from "@ente/shared/events";
import { wait } from "@ente/shared/utils";
import { Canceler } from "axios";
import { Remote } from "comlink";
import { FILE_TYPE } from "constants/file";
import {
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT,
UPLOAD_RESULT,
@ -43,7 +45,6 @@ import {
segregateMetadataAndMediaFiles2,
} from "utils/upload";
import { getLocalFiles } from "../fileService";
import { clusterLivePhotoFiles, clusterLivePhotos } from "./metadataService";
import {
getMetadataJSONMapKeyForJSON,
tryParseTakeoutMetadataJSON,
@ -56,6 +57,7 @@ import UploadService, {
getFileName,
uploader,
} from "./uploadService";
import { getFileTypeFromExtensionForLivePhotoClustering } from "utils/file/livePhoto";
const MAX_CONCURRENT_UPLOADS = 4;
@ -800,3 +802,164 @@ const cancelRemainingUploads = async () => {
await electron.setPendingUploadFiles("zips", []);
await electron.setPendingUploadFiles("files", []);
};
/**
* Go through the given files, combining any sibling image + video assets into a
* single live photo when appropriate.
*/
const clusterLivePhotos = (mediaFiles: FileWithCollection2[]) => {
const result: FileWithCollection2[] = [];
mediaFiles
.sort((f, g) =>
nameAndExtension(getFileName(f.file))[0].localeCompare(
nameAndExtension(getFileName(g.file))[0],
),
)
.sort((f, g) => f.collectionID - g.collectionID);
let index = 0;
while (index < mediaFiles.length - 1) {
const f = mediaFiles[index];
const g = mediaFiles[index + 1];
const fFileType = getFileTypeFromExtensionForLivePhotoClustering(
getFileName(f.file),
);
const gFileType = getFileTypeFromExtensionForLivePhotoClustering(
getFileName(g.file),
);
const fa: PotentialLivePhotoAsset = {
collectionID: f.collectionID,
fileType: fFileType,
fileName: getFileName(f.file),
/* TODO(MR): ElectronFile changes */
size: (f as FileWithCollection).file.size,
};
const ga: PotentialLivePhotoAsset = {
collectionID: g.collectionID,
fileType: gFileType,
fileName: getFileName(g.file),
/* TODO(MR): ElectronFile changes */
size: (g as FileWithCollection).file.size,
};
if (areLivePhotoAssets(fa, ga)) {
let imageFile: File | ElectronFile | string;
let videoFile: File | ElectronFile | string;
if (
fFileType === FILE_TYPE.IMAGE &&
gFileType === FILE_TYPE.VIDEO
) {
imageFile = f.file;
videoFile = g.file;
} else {
videoFile = f.file;
imageFile = g.file;
}
const livePhotoLocalID = f.localID;
result.push({
localID: livePhotoLocalID,
collectionID: f.collectionID,
isLivePhoto: true,
livePhotoAssets: {
image: imageFile,
video: videoFile,
},
});
index += 2;
} else {
result.push({
...f,
isLivePhoto: false,
});
index += 1;
}
}
if (index === mediaFiles.length - 1) {
result.push({
...mediaFiles[index],
isLivePhoto: false,
});
}
return result;
};
interface PotentialLivePhotoAsset {
collectionID: number;
fileType: FILE_TYPE;
fileName: string;
size: number;
}
const areLivePhotoAssets = (
f: PotentialLivePhotoAsset,
g: PotentialLivePhotoAsset,
) => {
if (f.collectionID != g.collectionID) return false;
const [fName, fExt] = nameAndExtension(f.fileName);
const [gName, gExt] = nameAndExtension(g.fileName);
let fPrunedName: string;
let gPrunedName: string;
if (f.fileType == FILE_TYPE.IMAGE && g.fileType == FILE_TYPE.VIDEO) {
fPrunedName = removePotentialLivePhotoSuffix(
fName,
// A Google Live Photo image file can have video extension appended
// as suffix, so we pass that to removePotentialLivePhotoSuffix to
// remove it.
//
// Example: IMG_20210630_0001.mp4.jpg (Google Live Photo image file)
gExt ? `.${gExt}` : undefined,
);
gPrunedName = removePotentialLivePhotoSuffix(gName);
} else if (f.fileType == FILE_TYPE.VIDEO && g.fileType == FILE_TYPE.IMAGE) {
fPrunedName = removePotentialLivePhotoSuffix(fName);
gPrunedName = removePotentialLivePhotoSuffix(
gName,
fExt ? `.${fExt}` : undefined,
);
} else {
return false;
}
if (fPrunedName != gPrunedName) return false;
// Also check that the size of an individual Live Photo asset is less than
// an (arbitrary) limit. This should be true in practice as the videos for a
// live photo are a few seconds long. Further on, the zipping library that
// we use doesn't support stream as a input.
const maxAssetSize = 20 * 1024 * 1024; /* 20MB */
if (f.size > maxAssetSize || g.size > maxAssetSize) {
log.info(
`Not classifying assets with too large sizes ${[f.size, g.size]} as a live photo`,
);
return false;
}
return true;
};
const removePotentialLivePhotoSuffix = (name: string, suffix?: string) => {
const suffix_3 = "_3";
// The icloud-photos-downloader library appends _HVEC to the end of the
// filename in case of live photos.
//
// https://github.com/icloud-photos-downloader/icloud_photos_downloader
const suffix_hvec = "_HVEC";
let foundSuffix: string | undefined;
if (name.endsWith(suffix_3)) {
foundSuffix = suffix_3;
} else if (
name.endsWith(suffix_hvec) ||
name.endsWith(suffix_hvec.toLowerCase())
) {
foundSuffix = suffix_hvec;
} else if (suffix) {
if (name.endsWith(suffix) || name.endsWith(suffix.toLowerCase())) {
foundSuffix = suffix;
}
}
return foundSuffix ? name.slice(0, foundSuffix.length * -1) : name;
};