diff --git a/web/apps/photos/src/services/upload/date.ts b/web/apps/photos/src/services/upload/date.ts new file mode 100644 index 000000000..89934e37c --- /dev/null +++ b/web/apps/photos/src/services/upload/date.ts @@ -0,0 +1,166 @@ +import log from "@/next/log"; +import { validateAndGetCreationUnixTimeInMicroSeconds } from "@ente/shared/time"; + +/** + * Try to extract a date (as epoch microseconds) from a file name by matching it + * against certain known patterns for media files. + * + * If it doesn't match a known pattern, or if there is some error during the + * parsing, return `undefined`. + */ +export const tryParseEpochMicrosecondsFromFileName = ( + fileName: string, +): number | undefined => { + try { + fileName = fileName.trim(); + let parsedDate: Date; + if (fileName.startsWith("IMG-") || fileName.startsWith("VID-")) { + // WhatsApp media files + // Sample name: IMG-20171218-WA0028.jpg + parsedDate = parseDateFromFusedDateString(fileName.split("-")[1]); + } else if (fileName.startsWith("Screenshot_")) { + // Screenshots on Android + // Sample name: Screenshot_20181227-152914.jpg + parsedDate = parseDateFromFusedDateString( + fileName.replaceAll("Screenshot_", ""), + ); + } else if (fileName.startsWith("signal-")) { + // Signal images + // Sample name: signal-2018-08-21-100217.jpg + const p = fileName.split("-"); + const dateString = `${p[1]}${p[2]}${p[3]}-${p[4]}`; + parsedDate = parseDateFromFusedDateString(dateString); + } + if (!parsedDate) { + parsedDate = tryToParseDateTime(fileName); + } + return validateAndGetCreationUnixTimeInMicroSeconds(parsedDate); + } catch (e) { + log.error(`Could not extract date from file name ${fileName}`, e); + return undefined; + } +}; + +interface DateComponent { + year: T; + month: T; + day: T; + hour: T; + minute: T; + second: T; +} + +const currentYear = new Date().getFullYear(); + +/* +generates data component for date in format YYYYMMDD-HHMMSS + */ +function parseDateFromFusedDateString(dateTime: string) { + const dateComponent: DateComponent = convertDateComponentToNumber({ + year: dateTime.slice(0, 4), + month: dateTime.slice(4, 6), + day: dateTime.slice(6, 8), + hour: dateTime.slice(9, 11), + minute: dateTime.slice(11, 13), + second: dateTime.slice(13, 15), + }); + return validateAndGetDateFromComponents(dateComponent); +} + +/* sample date format = 2018-08-19 12:34:45 + the date has six symbol separated number values + which we would extract and use to form the date + */ +export function tryToParseDateTime(dateTime: string): Date { + const dateComponent = getDateComponentsFromSymbolJoinedString(dateTime); + if (dateComponent.year?.length === 8 && dateComponent.month?.length === 6) { + // the filename has size 8 consecutive and then 6 consecutive digits + // high possibility that the it is a date in format YYYYMMDD-HHMMSS + const possibleDateTime = dateComponent.year + "-" + dateComponent.month; + return parseDateFromFusedDateString(possibleDateTime); + } + return validateAndGetDateFromComponents( + convertDateComponentToNumber(dateComponent), + ); +} + +function getDateComponentsFromSymbolJoinedString( + dateTime: string, +): DateComponent { + const [year, month, day, hour, minute, second] = + dateTime.match(/\d+/g) ?? []; + + return { year, month, day, hour, minute, second }; +} + +function validateAndGetDateFromComponents( + dateComponent: DateComponent, + options = { minYear: 1990, maxYear: currentYear + 1 }, +) { + let date = getDateFromComponents(dateComponent); + if (hasTimeValues(dateComponent) && !isTimePartValid(date, dateComponent)) { + // if the date has time values but they are not valid + // then we remove the time values and try to validate the date + date = getDateFromComponents(removeTimeValues(dateComponent)); + } + if (!isDatePartValid(date, dateComponent)) { + return null; + } + if ( + date.getFullYear() < options.minYear || + date.getFullYear() > options.maxYear + ) { + return null; + } + return date; +} + +function isTimePartValid(date: Date, dateComponent: DateComponent) { + return ( + date.getHours() === dateComponent.hour && + date.getMinutes() === dateComponent.minute && + date.getSeconds() === dateComponent.second + ); +} + +function isDatePartValid(date: Date, dateComponent: DateComponent) { + return ( + date.getFullYear() === dateComponent.year && + date.getMonth() === dateComponent.month && + date.getDate() === dateComponent.day + ); +} + +function convertDateComponentToNumber( + dateComponent: DateComponent, +): DateComponent { + return { + year: Number(dateComponent.year), + // https://stackoverflow.com/questions/2552483/why-does-the-month-argument-range-from-0-to-11-in-javascripts-date-constructor + month: Number(dateComponent.month) - 1, + day: Number(dateComponent.day), + hour: Number(dateComponent.hour), + minute: Number(dateComponent.minute), + second: Number(dateComponent.second), + }; +} + +function getDateFromComponents(dateComponent: DateComponent) { + const { year, month, day, hour, minute, second } = dateComponent; + if (hasTimeValues(dateComponent)) { + return new Date(year, month, day, hour, minute, second); + } else { + return new Date(year, month, day); + } +} + +function hasTimeValues(dateComponent: DateComponent) { + const { hour, minute, second } = dateComponent; + return !isNaN(hour) && !isNaN(minute) && !isNaN(second); +} + +function removeTimeValues( + dateComponent: DateComponent, +): DateComponent { + return { ...dateComponent, hour: 0, minute: 0, second: 0 }; +} diff --git a/web/apps/photos/src/services/upload/metadata.ts b/web/apps/photos/src/services/upload/metadata.ts index 37f1a7c35..6975513e8 100644 --- a/web/apps/photos/src/services/upload/metadata.ts +++ b/web/apps/photos/src/services/upload/metadata.ts @@ -4,11 +4,6 @@ import log from "@/next/log"; import { ElectronFile } from "@/next/types/file"; import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker"; import { CustomError } from "@ente/shared/error"; -import { - parseDateFromFusedDateString, - tryToParseDateTime, - validateAndGetCreationUnixTimeInMicroSeconds, -} from "@ente/shared/time"; import type { DataStream } from "@ente/shared/utils/data-stream"; import { Remote } from "comlink"; import { FILE_READER_CHUNK_SIZE, NULL_LOCATION } from "constants/upload"; @@ -22,6 +17,7 @@ import { type LivePhotoAssets2, type UploadAsset2, } from "types/upload"; +import { tryParseEpochMicrosecondsFromFileName } from "./date"; import { MAX_FILE_NAME_LENGTH_GOOGLE_EXPORT, getClippedMetadataJSONMapKeyForFile, @@ -154,7 +150,7 @@ async function extractMetadata( title: receivedFile.name, creationTime: extractedMetadata.creationTime ?? - tryExtractDateFromFileName(receivedFile.name) ?? + tryParseEpochMicrosecondsFromFileName(receivedFile.name) ?? receivedFile.lastModified * 1000, modificationTime: receivedFile.lastModified * 1000, latitude: extractedMetadata.location.latitude, @@ -190,43 +186,6 @@ async function getImageMetadata( } } -/** - * Try to extract a date from file name for certain known patterns. - * - * If it doesn't match a known pattern, or if there is some error during the - * extraction, return `undefined`. - */ -const tryExtractDateFromFileName = (fileName: string): number | undefined => { - try { - fileName = fileName.trim(); - let parsedDate: Date; - if (fileName.startsWith("IMG-") || fileName.startsWith("VID-")) { - // WhatsApp media files - // Sample name: IMG-20171218-WA0028.jpg - parsedDate = parseDateFromFusedDateString(fileName.split("-")[1]); - } else if (fileName.startsWith("Screenshot_")) { - // Screenshots on Android - // Sample name: Screenshot_20181227-152914.jpg - parsedDate = parseDateFromFusedDateString( - fileName.replaceAll("Screenshot_", ""), - ); - } else if (fileName.startsWith("signal-")) { - // Signal images - // Sample name: signal-2018-08-21-100217.jpg - const p = fileName.split("-"); - const dateString = `${p[1]}${p[2]}${p[3]}-${p[4]}`; - parsedDate = parseDateFromFusedDateString(dateString); - } - if (!parsedDate) { - parsedDate = tryToParseDateTime(fileName); - } - return validateAndGetCreationUnixTimeInMicroSeconds(parsedDate); - } catch (e) { - log.error(`Could not extract date from file name ${fileName}`, e); - return undefined; - } -}; - async function getVideoMetadata(file: File | ElectronFile) { let videoMetadata = NULL_EXTRACTED_METADATA; try { diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index 17143da09..74d3952a0 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -1,5 +1,5 @@ import { FILE_TYPE } from "@/media/file-type"; -import { tryToParseDateTime } from "@ente/shared/time"; +import { tryToParseDateTime } from "services/upload/date"; import { getLocalCollections } from "services/collectionService"; import { getLocalFiles } from "services/fileService"; import { diff --git a/web/packages/shared/time/index.ts b/web/packages/shared/time/index.ts index d98bc411b..7f03f44fc 100644 --- a/web/packages/shared/time/index.ts +++ b/web/packages/shared/time/index.ts @@ -5,16 +5,6 @@ export interface TimeDelta { years?: number; } -interface DateComponent { - year: T; - month: T; - day: T; - hour: T; - minute: T; - second: T; -} - -const currentYear = new Date().getFullYear(); export function getUnixTimeInMicroSecondsWithDelta(delta: TimeDelta): number { let currentDate = new Date(); @@ -71,116 +61,3 @@ function _addYears(date: Date, years: number) { result.setFullYear(date.getFullYear() + years); return result; } - -/* -generates data component for date in format YYYYMMDD-HHMMSS - */ -export function parseDateFromFusedDateString(dateTime: string) { - const dateComponent: DateComponent = convertDateComponentToNumber({ - year: dateTime.slice(0, 4), - month: dateTime.slice(4, 6), - day: dateTime.slice(6, 8), - hour: dateTime.slice(9, 11), - minute: dateTime.slice(11, 13), - second: dateTime.slice(13, 15), - }); - return validateAndGetDateFromComponents(dateComponent); -} - -/* sample date format = 2018-08-19 12:34:45 - the date has six symbol separated number values - which we would extract and use to form the date - */ -export function tryToParseDateTime(dateTime: string): Date { - const dateComponent = getDateComponentsFromSymbolJoinedString(dateTime); - if (dateComponent.year?.length === 8 && dateComponent.month?.length === 6) { - // the filename has size 8 consecutive and then 6 consecutive digits - // high possibility that the it is a date in format YYYYMMDD-HHMMSS - const possibleDateTime = dateComponent.year + "-" + dateComponent.month; - return parseDateFromFusedDateString(possibleDateTime); - } - return validateAndGetDateFromComponents( - convertDateComponentToNumber(dateComponent), - ); -} - -function getDateComponentsFromSymbolJoinedString( - dateTime: string, -): DateComponent { - const [year, month, day, hour, minute, second] = - dateTime.match(/\d+/g) ?? []; - - return { year, month, day, hour, minute, second }; -} - -function validateAndGetDateFromComponents( - dateComponent: DateComponent, - options = { minYear: 1990, maxYear: currentYear + 1 }, -) { - let date = getDateFromComponents(dateComponent); - if (hasTimeValues(dateComponent) && !isTimePartValid(date, dateComponent)) { - // if the date has time values but they are not valid - // then we remove the time values and try to validate the date - date = getDateFromComponents(removeTimeValues(dateComponent)); - } - if (!isDatePartValid(date, dateComponent)) { - return null; - } - if ( - date.getFullYear() < options.minYear || - date.getFullYear() > options.maxYear - ) { - return null; - } - return date; -} - -function isTimePartValid(date: Date, dateComponent: DateComponent) { - return ( - date.getHours() === dateComponent.hour && - date.getMinutes() === dateComponent.minute && - date.getSeconds() === dateComponent.second - ); -} - -function isDatePartValid(date: Date, dateComponent: DateComponent) { - return ( - date.getFullYear() === dateComponent.year && - date.getMonth() === dateComponent.month && - date.getDate() === dateComponent.day - ); -} - -function convertDateComponentToNumber( - dateComponent: DateComponent, -): DateComponent { - return { - year: Number(dateComponent.year), - // https://stackoverflow.com/questions/2552483/why-does-the-month-argument-range-from-0-to-11-in-javascripts-date-constructor - month: Number(dateComponent.month) - 1, - day: Number(dateComponent.day), - hour: Number(dateComponent.hour), - minute: Number(dateComponent.minute), - second: Number(dateComponent.second), - }; -} - -function getDateFromComponents(dateComponent: DateComponent) { - const { year, month, day, hour, minute, second } = dateComponent; - if (hasTimeValues(dateComponent)) { - return new Date(year, month, day, hour, minute, second); - } else { - return new Date(year, month, day); - } -} - -function hasTimeValues(dateComponent: DateComponent) { - const { hour, minute, second } = dateComponent; - return !isNaN(hour) && !isNaN(minute) && !isNaN(second); -} - -function removeTimeValues( - dateComponent: DateComponent, -): DateComponent { - return { ...dateComponent, hour: 0, minute: 0, second: 0 }; -}