Extract
This commit is contained in:
parent
56713325ed
commit
48bace50df
6 changed files with 169 additions and 165 deletions
|
@ -1,4 +1,3 @@
|
|||
import { ensureElectron } from "@/next/electron";
|
||||
import { getFileNameSize } from "@/next/file";
|
||||
import log from "@/next/log";
|
||||
import { ElectronFile } from "@/next/types/file";
|
||||
|
@ -19,11 +18,8 @@ import { FilePublicMagicMetadataProps } from "types/file";
|
|||
import {
|
||||
FileTypeInfo,
|
||||
LivePhotoAssets,
|
||||
Location,
|
||||
Metadata,
|
||||
ParsedExtractedMetadata,
|
||||
ParsedMetadataJSON,
|
||||
ParsedMetadataJSONMap,
|
||||
type DataStream,
|
||||
type FileWithCollection,
|
||||
type FileWithCollection2,
|
||||
|
@ -32,15 +28,15 @@ import {
|
|||
} from "types/upload";
|
||||
import { getFileTypeFromExtensionForLivePhotoClustering } from "utils/file/livePhoto";
|
||||
import { getEXIFLocation, getEXIFTime, getParsedExifData } from "./exifService";
|
||||
import {
|
||||
MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT,
|
||||
getClippedMetadataJSONMapKeyForFile,
|
||||
getMetadataJSONMapKeyForFile,
|
||||
type ParsedMetadataJSON,
|
||||
} from "./takeout";
|
||||
import uploadCancelService from "./uploadCancelService";
|
||||
import { getFileName } from "./uploadService";
|
||||
|
||||
const NULL_PARSED_METADATA_JSON: ParsedMetadataJSON = {
|
||||
creationTime: null,
|
||||
modificationTime: null,
|
||||
...NULL_LOCATION,
|
||||
};
|
||||
|
||||
const EXIF_TAGS_NEEDED = [
|
||||
"DateTimeOriginal",
|
||||
"CreateDate",
|
||||
|
@ -59,8 +55,6 @@ const EXIF_TAGS_NEEDED = [
|
|||
"MetadataDate",
|
||||
];
|
||||
|
||||
export const MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT = 46;
|
||||
|
||||
export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
|
||||
location: NULL_LOCATION,
|
||||
creationTime: null,
|
||||
|
@ -138,115 +132,6 @@ export async function getImageMetadata(
|
|||
return imageMetadata;
|
||||
}
|
||||
|
||||
export const getMetadataJSONMapKeyForJSON = (
|
||||
collectionID: number,
|
||||
jsonFileName: string,
|
||||
) => {
|
||||
let title = jsonFileName.slice(0, -1 * ".json".length);
|
||||
const endsWithNumberedSuffixWithBrackets = title.match(/\(\d+\)$/);
|
||||
if (endsWithNumberedSuffixWithBrackets) {
|
||||
title = title.slice(
|
||||
0,
|
||||
-1 * endsWithNumberedSuffixWithBrackets[0].length,
|
||||
);
|
||||
const [name, extension] = splitFilenameAndExtension(title);
|
||||
return `${collectionID}-${name}${endsWithNumberedSuffixWithBrackets[0]}.${extension}`;
|
||||
}
|
||||
return `${collectionID}-${title}`;
|
||||
};
|
||||
|
||||
// if the file name is greater than MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT(46) , then google photos clips the file name
|
||||
// so we need to use the clipped file name to get the metadataJSON file
|
||||
export const getClippedMetadataJSONMapKeyForFile = (
|
||||
collectionID: number,
|
||||
fileName: string,
|
||||
) => {
|
||||
return `${collectionID}-${fileName.slice(
|
||||
0,
|
||||
MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT,
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const getMetadataJSONMapKeyForFile = (
|
||||
collectionID: number,
|
||||
fileName: string,
|
||||
) => {
|
||||
return `${collectionID}-${getFileOriginalName(fileName)}`;
|
||||
};
|
||||
|
||||
export async function parseMetadataJSON(
|
||||
receivedFile: File | ElectronFile | string,
|
||||
) {
|
||||
try {
|
||||
let text: string;
|
||||
if (typeof receivedFile == "string") {
|
||||
text = await ensureElectron().fs.readTextFile(receivedFile);
|
||||
} else {
|
||||
if (!(receivedFile instanceof File)) {
|
||||
receivedFile = new File(
|
||||
[await receivedFile.blob()],
|
||||
receivedFile.name,
|
||||
);
|
||||
}
|
||||
text = await receivedFile.text();
|
||||
}
|
||||
|
||||
return parseMetadataJSONText(text);
|
||||
} catch (e) {
|
||||
log.error("parseMetadataJSON failed", e);
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseMetadataJSONText(text: string) {
|
||||
const metadataJSON: object = JSON.parse(text);
|
||||
|
||||
const parsedMetadataJSON: ParsedMetadataJSON = NULL_PARSED_METADATA_JSON;
|
||||
if (!metadataJSON) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
metadataJSON["photoTakenTime"] &&
|
||||
metadataJSON["photoTakenTime"]["timestamp"]
|
||||
) {
|
||||
parsedMetadataJSON.creationTime =
|
||||
metadataJSON["photoTakenTime"]["timestamp"] * 1000000;
|
||||
} else if (
|
||||
metadataJSON["creationTime"] &&
|
||||
metadataJSON["creationTime"]["timestamp"]
|
||||
) {
|
||||
parsedMetadataJSON.creationTime =
|
||||
metadataJSON["creationTime"]["timestamp"] * 1000000;
|
||||
}
|
||||
if (
|
||||
metadataJSON["modificationTime"] &&
|
||||
metadataJSON["modificationTime"]["timestamp"]
|
||||
) {
|
||||
parsedMetadataJSON.modificationTime =
|
||||
metadataJSON["modificationTime"]["timestamp"] * 1000000;
|
||||
}
|
||||
let locationData: Location = NULL_LOCATION;
|
||||
if (
|
||||
metadataJSON["geoData"] &&
|
||||
(metadataJSON["geoData"]["latitude"] !== 0.0 ||
|
||||
metadataJSON["geoData"]["longitude"] !== 0.0)
|
||||
) {
|
||||
locationData = metadataJSON["geoData"];
|
||||
} else if (
|
||||
metadataJSON["geoDataExif"] &&
|
||||
(metadataJSON["geoDataExif"]["latitude"] !== 0.0 ||
|
||||
metadataJSON["geoDataExif"]["longitude"] !== 0.0)
|
||||
) {
|
||||
locationData = metadataJSON["geoDataExif"];
|
||||
}
|
||||
if (locationData !== null) {
|
||||
parsedMetadataJSON.latitude = locationData.latitude;
|
||||
parsedMetadataJSON.longitude = locationData.longitude;
|
||||
}
|
||||
return parsedMetadataJSON;
|
||||
}
|
||||
|
||||
// tries to extract date from file name if available else returns null
|
||||
export function extractDateFromFileName(filename: string): number {
|
||||
try {
|
||||
|
@ -283,32 +168,6 @@ function convertSignalNameToFusedDateString(filename: string) {
|
|||
return `${dateStringParts[1]}${dateStringParts[2]}${dateStringParts[3]}-${dateStringParts[4]}`;
|
||||
}
|
||||
|
||||
const EDITED_FILE_SUFFIX = "-edited";
|
||||
|
||||
/*
|
||||
Get the original file name for edited file to associate it to original file's metadataJSON file
|
||||
as edited file doesn't have their own metadata file
|
||||
*/
|
||||
function getFileOriginalName(fileName: string) {
|
||||
let originalName: string = null;
|
||||
const [nameWithoutExtension, extension] =
|
||||
splitFilenameAndExtension(fileName);
|
||||
|
||||
const isEditedFile = nameWithoutExtension.endsWith(EDITED_FILE_SUFFIX);
|
||||
if (isEditedFile) {
|
||||
originalName = nameWithoutExtension.slice(
|
||||
0,
|
||||
-1 * EDITED_FILE_SUFFIX.length,
|
||||
);
|
||||
} else {
|
||||
originalName = nameWithoutExtension;
|
||||
}
|
||||
if (extension) {
|
||||
originalName += "." + extension;
|
||||
}
|
||||
return originalName;
|
||||
}
|
||||
|
||||
async function getVideoMetadata(file: File | ElectronFile) {
|
||||
let videoMetadata = NULL_EXTRACTED_METADATA;
|
||||
try {
|
||||
|
@ -356,7 +215,7 @@ export async function getLivePhotoFileType(
|
|||
|
||||
export const extractAssetMetadata = async (
|
||||
worker: Remote<DedicatedCryptoWorker>,
|
||||
parsedMetadataJSONMap: ParsedMetadataJSONMap,
|
||||
parsedMetadataJSONMap: Map<string, ParsedMetadataJSON>,
|
||||
{ isLivePhoto, file, livePhotoAssets }: UploadAsset2,
|
||||
collectionID: number,
|
||||
fileTypeInfo: FileTypeInfo,
|
||||
|
@ -380,7 +239,7 @@ export const extractAssetMetadata = async (
|
|||
|
||||
async function extractFileMetadata(
|
||||
worker: Remote<DedicatedCryptoWorker>,
|
||||
parsedMetadataJSONMap: ParsedMetadataJSONMap,
|
||||
parsedMetadataJSONMap: Map<string, ParsedMetadataJSON>,
|
||||
collectionID: number,
|
||||
fileTypeInfo: FileTypeInfo,
|
||||
rawFile: File | ElectronFile | string,
|
||||
|
@ -412,7 +271,7 @@ async function extractFileMetadata(
|
|||
|
||||
async function extractLivePhotoMetadata(
|
||||
worker: Remote<DedicatedCryptoWorker>,
|
||||
parsedMetadataJSONMap: ParsedMetadataJSONMap,
|
||||
parsedMetadataJSONMap: Map<string, ParsedMetadataJSON>,
|
||||
collectionID: number,
|
||||
fileTypeInfo: FileTypeInfo,
|
||||
livePhotoAssets: LivePhotoAssets2,
|
||||
|
|
155
web/apps/photos/src/services/upload/takeout.ts
Normal file
155
web/apps/photos/src/services/upload/takeout.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
/** @file Dealing with the JSON metadata in Google Takeouts */
|
||||
|
||||
import { ensureElectron } from "@/next/electron";
|
||||
import { nameAndExtension } from "@/next/file";
|
||||
import log from "@/next/log";
|
||||
import type { ElectronFile } from "@/next/types/file";
|
||||
import { NULL_LOCATION } from "constants/upload";
|
||||
import { type Location } from "types/upload";
|
||||
|
||||
export interface ParsedMetadataJSON {
|
||||
creationTime: number;
|
||||
modificationTime: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export const MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT = 46;
|
||||
|
||||
export const getMetadataJSONMapKeyForJSON = (
|
||||
collectionID: number,
|
||||
jsonFileName: string,
|
||||
) => {
|
||||
let title = jsonFileName.slice(0, -1 * ".json".length);
|
||||
const endsWithNumberedSuffixWithBrackets = title.match(/\(\d+\)$/);
|
||||
if (endsWithNumberedSuffixWithBrackets) {
|
||||
title = title.slice(
|
||||
0,
|
||||
-1 * endsWithNumberedSuffixWithBrackets[0].length,
|
||||
);
|
||||
const [name, extension] = nameAndExtension(title);
|
||||
return `${collectionID}-${name}${endsWithNumberedSuffixWithBrackets[0]}.${extension}`;
|
||||
}
|
||||
return `${collectionID}-${title}`;
|
||||
};
|
||||
|
||||
// if the file name is greater than MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT(46) , then google photos clips the file name
|
||||
// so we need to use the clipped file name to get the metadataJSON file
|
||||
export const getClippedMetadataJSONMapKeyForFile = (
|
||||
collectionID: number,
|
||||
fileName: string,
|
||||
) => {
|
||||
return `${collectionID}-${fileName.slice(
|
||||
0,
|
||||
MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT,
|
||||
)}`;
|
||||
};
|
||||
|
||||
export const getMetadataJSONMapKeyForFile = (
|
||||
collectionID: number,
|
||||
fileName: string,
|
||||
) => {
|
||||
return `${collectionID}-${getFileOriginalName(fileName)}`;
|
||||
};
|
||||
|
||||
const EDITED_FILE_SUFFIX = "-edited";
|
||||
|
||||
/*
|
||||
Get the original file name for edited file to associate it to original file's metadataJSON file
|
||||
as edited file doesn't have their own metadata file
|
||||
*/
|
||||
function getFileOriginalName(fileName: string) {
|
||||
let originalName: string = null;
|
||||
const [name, extension] = nameAndExtension(fileName);
|
||||
|
||||
const isEditedFile = name.endsWith(EDITED_FILE_SUFFIX);
|
||||
if (isEditedFile) {
|
||||
originalName = name.slice(0, -1 * EDITED_FILE_SUFFIX.length);
|
||||
} else {
|
||||
originalName = name;
|
||||
}
|
||||
if (extension) {
|
||||
originalName += "." + extension;
|
||||
}
|
||||
return originalName;
|
||||
}
|
||||
|
||||
export async function parseMetadataJSON(
|
||||
receivedFile: File | ElectronFile | string,
|
||||
) {
|
||||
try {
|
||||
let text: string;
|
||||
if (typeof receivedFile == "string") {
|
||||
text = await ensureElectron().fs.readTextFile(receivedFile);
|
||||
} else {
|
||||
if (!(receivedFile instanceof File)) {
|
||||
receivedFile = new File(
|
||||
[await receivedFile.blob()],
|
||||
receivedFile.name,
|
||||
);
|
||||
}
|
||||
text = await receivedFile.text();
|
||||
}
|
||||
|
||||
return parseMetadataJSONText(text);
|
||||
} catch (e) {
|
||||
log.error("parseMetadataJSON failed", e);
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const NULL_PARSED_METADATA_JSON: ParsedMetadataJSON = {
|
||||
creationTime: null,
|
||||
modificationTime: null,
|
||||
latitude: null, longitude: null
|
||||
...NULL_LOCATION,
|
||||
};
|
||||
|
||||
export async function parseMetadataJSONText(text: string) {
|
||||
const metadataJSON: object = JSON.parse(text);
|
||||
|
||||
const parsedMetadataJSON: ParsedMetadataJSON = NULL_PARSED_METADATA_JSON;
|
||||
if (!metadataJSON) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
metadataJSON["photoTakenTime"] &&
|
||||
metadataJSON["photoTakenTime"]["timestamp"]
|
||||
) {
|
||||
parsedMetadataJSON.creationTime =
|
||||
metadataJSON["photoTakenTime"]["timestamp"] * 1000000;
|
||||
} else if (
|
||||
metadataJSON["creationTime"] &&
|
||||
metadataJSON["creationTime"]["timestamp"]
|
||||
) {
|
||||
parsedMetadataJSON.creationTime =
|
||||
metadataJSON["creationTime"]["timestamp"] * 1000000;
|
||||
}
|
||||
if (
|
||||
metadataJSON["modificationTime"] &&
|
||||
metadataJSON["modificationTime"]["timestamp"]
|
||||
) {
|
||||
parsedMetadataJSON.modificationTime =
|
||||
metadataJSON["modificationTime"]["timestamp"] * 1000000;
|
||||
}
|
||||
let locationData: Location = NULL_LOCATION;
|
||||
if (
|
||||
metadataJSON["geoData"] &&
|
||||
(metadataJSON["geoData"]["latitude"] !== 0.0 ||
|
||||
metadataJSON["geoData"]["longitude"] !== 0.0)
|
||||
) {
|
||||
locationData = metadataJSON["geoData"];
|
||||
} else if (
|
||||
metadataJSON["geoDataExif"] &&
|
||||
(metadataJSON["geoDataExif"]["latitude"] !== 0.0 ||
|
||||
metadataJSON["geoDataExif"]["longitude"] !== 0.0)
|
||||
) {
|
||||
locationData = metadataJSON["geoDataExif"];
|
||||
}
|
||||
if (locationData !== null) {
|
||||
parsedMetadataJSON.latitude = locationData.latitude;
|
||||
parsedMetadataJSON.longitude = locationData.longitude;
|
||||
}
|
||||
return parsedMetadataJSON;
|
||||
}
|
|
@ -26,8 +26,6 @@ import { EncryptedEnteFile, EnteFile } from "types/file";
|
|||
import { SetFiles } from "types/gallery";
|
||||
import {
|
||||
FileWithCollection,
|
||||
ParsedMetadataJSON,
|
||||
ParsedMetadataJSONMap,
|
||||
PublicUploadProps,
|
||||
type FileWithCollection2,
|
||||
} from "types/upload";
|
||||
|
@ -50,6 +48,7 @@ import {
|
|||
getMetadataJSONMapKeyForJSON,
|
||||
parseMetadataJSON,
|
||||
} from "./metadataService";
|
||||
import type { ParsedMetadataJSON } from "./takeout";
|
||||
import uploadCancelService from "./uploadCancelService";
|
||||
import UploadService, {
|
||||
assetName,
|
||||
|
@ -264,7 +263,7 @@ class UploadManager {
|
|||
private cryptoWorkers = new Array<
|
||||
ComlinkWorker<typeof DedicatedCryptoWorker>
|
||||
>(MAX_CONCURRENT_UPLOADS);
|
||||
private parsedMetadataJSONMap: ParsedMetadataJSONMap;
|
||||
private parsedMetadataJSONMap: Map<string, ParsedMetadataJSON>;
|
||||
private filesToBeUploaded: FileWithCollection2[];
|
||||
private remainingFiles: FileWithCollection2[] = [];
|
||||
private failedFiles: FileWithCollection2[];
|
||||
|
|
|
@ -29,7 +29,6 @@ import {
|
|||
FileInMemory,
|
||||
FileTypeInfo,
|
||||
FileWithMetadata,
|
||||
ParsedMetadataJSONMap,
|
||||
ProcessedFile,
|
||||
PublicUploadProps,
|
||||
UploadAsset,
|
||||
|
@ -64,6 +63,7 @@ import {
|
|||
} from "./thumbnail";
|
||||
import uploadCancelService from "./uploadCancelService";
|
||||
import UploadHttpClient from "./uploadHttpClient";
|
||||
import type { ParsedMetadataJSON } from "./takeout";
|
||||
|
||||
/** Upload files to cloud storage */
|
||||
class UploadService {
|
||||
|
@ -169,7 +169,7 @@ export const uploader = async (
|
|||
worker: Remote<DedicatedCryptoWorker>,
|
||||
existingFiles: EnteFile[],
|
||||
fileWithCollection: FileWithCollection2,
|
||||
parsedMetadataJSONMap: ParsedMetadataJSONMap,
|
||||
parsedMetadataJSONMap: Map<string, ParsedMetadataJSON>,
|
||||
uploaderName: string,
|
||||
isCFUploadProxyDisabled: boolean,
|
||||
makeProgessTracker: MakeProgressTracker,
|
||||
|
|
|
@ -38,13 +38,6 @@ export interface Location {
|
|||
longitude: number;
|
||||
}
|
||||
|
||||
export interface ParsedMetadataJSON {
|
||||
creationTime: number;
|
||||
modificationTime: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface MultipartUploadURLs {
|
||||
objectKey: string;
|
||||
partURLs: string[];
|
||||
|
@ -93,8 +86,6 @@ export interface FileWithCollection2 extends UploadAsset2 {
|
|||
collectionID?: number;
|
||||
}
|
||||
|
||||
export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>;
|
||||
|
||||
export interface UploadURL {
|
||||
url: string;
|
||||
objectKey: string;
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
getClippedMetadataJSONMapKeyForFile,
|
||||
getMetadataJSONMapKeyForFile,
|
||||
getMetadataJSONMapKeyForJSON,
|
||||
} from "services/upload/metadataService";
|
||||
} from "services/upload/takeout";
|
||||
import { getUserDetailsV2 } from "services/userService";
|
||||
import { groupFilesBasedOnCollectionID } from "utils/file";
|
||||
|
||||
|
|
Loading…
Reference in a new issue