diff --git a/.gitignore b/.gitignore index 6efad7e3..2e0fc4c6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ test/e2e/**/*.js test/e2e/**/*.js.map test/*.js test/*.js.map +test/tmp/* benchmark/**/*.js benchmark/**/*.js.map gulpfile.js diff --git a/src/backend/model/fileaccess/MetadataLoader.ts b/src/backend/model/fileaccess/MetadataLoader.ts index e0fcb50f..58b027c8 100644 --- a/src/backend/model/fileaccess/MetadataLoader.ts +++ b/src/backend/model/fileaccess/MetadataLoader.ts @@ -1,7 +1,6 @@ import * as fs from 'fs'; import { imageSize } from 'image-size'; import { Config } from '../../../common/config/private/Config'; -import { SideCar } from '../../../common/entities/MediaDTO'; import { FaceRegion, PhotoMetadata } from '../../../common/entities/PhotoDTO'; import { VideoMetadata } from '../../../common/entities/VideoDTO'; import { Logger } from '../../Logger'; @@ -142,57 +141,9 @@ export class MetadataLoader { for (const sidecarPath of sidecarPaths) { if (fs.existsSync(sidecarPath)) { - const sidecarData = await exifr.sidecar(sidecarPath); + const sidecarData: any = await exifr.sidecar(sidecarPath); if (sidecarData !== undefined) { - if ((sidecarData as SideCar).dc !== undefined) { - if ((sidecarData as SideCar).dc.subject !== undefined) { - if (metadata.keywords === undefined) { - metadata.keywords = []; - } - let keywords = (sidecarData as SideCar).dc.subject || []; - if (typeof keywords === 'string') { - keywords = [keywords]; - } - for (const kw of keywords) { - if (metadata.keywords.indexOf(kw) === -1) { - metadata.keywords.push(kw); - } - } - } - } - let hasPhotoshopDate = false; - if ((sidecarData as SideCar).photoshop !== undefined) { - if ((sidecarData as SideCar).photoshop.DateCreated !== undefined) { - const date = Utils.timestampToMS((sidecarData as SideCar).photoshop.DateCreated, null); - if (date) { - metadata.creationDate = date; - hasPhotoshopDate = true; - } - } - } - if (Object.hasOwn(sidecarData, 'xap')) { - (sidecarData as any)['xmp'] = (sidecarData as any)['xap']; - delete (sidecarData as any)['xap']; - } - if ((sidecarData as SideCar).xmp !== undefined) { - if ( - (sidecarData as SideCar).xmp.Rating !== undefined && - (sidecarData as SideCar).xmp.Rating > 0 - ) { - metadata.rating = (sidecarData as SideCar).xmp.Rating; - } - if ( - !hasPhotoshopDate && ( - (sidecarData as SideCar).xmp.CreateDate !== undefined || - (sidecarData as SideCar).xmp.ModifyDate !== undefined - ) - ) { - metadata.creationDate = - Utils.timestampToMS((sidecarData as SideCar).xmp.CreateDate, null) || - Utils.timestampToMS((sidecarData as SideCar).xmp.ModifyDate, null) || - metadata.creationDate; - } - } + MetadataLoader.mapMetadata(metadata, sidecarData); } } } @@ -205,6 +156,7 @@ export class MetadataLoader { Logger.silly(LOG_TAG, 'Error loading metadata for : ' + fullPath); Logger.silly(err); } + return metadata; } @@ -235,14 +187,6 @@ export class MetadataLoader { translateValues: false, //don't translate orientation from numbers to strings etc. mergeOutput: false //don't merge output, because things like Microsoft Rating (percent) and xmp.rating will be merged }; - - //Function to convert html code for special characters into their corresponding character (used in exif.photoshop-section) - const unescape = (tag: string) => { - return tag.replace(/&#([0-9]{1,3});/gi, function (match, numStr) { - return String.fromCharCode(parseInt(numStr, 10)); - }); - } - try { let bufferSize = Config.Media.photoMetadataSize; try { @@ -254,6 +198,15 @@ export class MetadataLoader { } catch (err) { // ignoring errors } + try { + //read the actual image size, don't rely on tags for this + const info = imageSize(fullPath); + metadata.size = { width: info.width, height: info.height }; + } catch (e) { + //in case of failure, set dimensions to 0 so they may be read via tags + metadata.size = { width: 0, height: 0 }; + } + const data = Buffer.allocUnsafe(bufferSize); fileHandle = await fs.promises.open(fullPath, 'r'); @@ -267,14 +220,6 @@ export class MetadataLoader { await fileHandle.close(); } try { - try { - //read the actual image size, don't rely on tags for this - const info = imageSize(fullPath); - metadata.size = { width: info.width, height: info.height }; - } catch (e) { - //in case of failure, set dimensions to 0 so they may be read via tags - metadata.size = { width: 0, height: 0 }; - } try { //Parse iptc data using the IptcParser, which works correctly for both UTF-8 and ASCII @@ -316,298 +261,12 @@ export class MetadataLoader { } try { - let orientation = 1; //Orientation 1 is normal const exif = await exifr.parse(data, exifrOptions); - //exif is structured in sections, we read the data by section - - //dc-section (subject is the only tag we want from dc) - if (exif.dc && - exif.dc.subject && - exif.dc.subject.length > 0) { - const subj = Array.isArray(exif.dc.subject) ? exif.dc.subject : [exif.dc.subject]; - if (metadata.keywords === undefined) { - metadata.keywords = []; - } - for (const kw of subj) { - if (metadata.keywords.indexOf(kw) === -1) { - metadata.keywords.push(kw); - } - } - } - - //ifd0 section - if (exif.ifd0) { - if (exif.ifd0.ImageWidth && metadata.size.width <= 0) { - metadata.size.width = exif.ifd0.ImageWidth; - } - if (exif.ifd0.ImageHeight && metadata.size.height <= 0) { - metadata.size.height = exif.ifd0.ImageHeight; - } - if (exif.ifd0.Orientation) { - orientation = parseInt( - exif.ifd0.Orientation as any, - 10 - ) as number; - } - if (exif.ifd0.Make && exif.ifd0.Make !== '') { - metadata.cameraData = metadata.cameraData || {}; - metadata.cameraData.make = '' + exif.ifd0.Make; - } - if (exif.ifd0.Model && exif.ifd0.Model !== '') { - metadata.cameraData = metadata.cameraData || {}; - metadata.cameraData.model = '' + exif.ifd0.Model; - } - //if (exif.ifd0.ModifyDate) {} //Deferred to the exif-section where the other timestamps are - } - - //exif section starting with the date sectino - if (exif.exif) { - //Preceedence of dates: exif.DateTimeOriginal, exif.CreateDate, ifd0.ModifyDate, ihdr["Creation Time"], xmp.MetadataDate, file system date - //Filesystem is the absolute last resort, and it's hard to write tests for, since file system dates are changed on e.g. git clone. - if (exif.exif.DateTimeOriginal) { - //DateTimeOriginal is when the camera shutter closed - let offset = exif.exif.OffsetTimeOriginal; //OffsetTimeOriginal is the corresponding offset - if (!offset) { //Find offset among other options if possible - offset = exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); - } - metadata.creationDate = Utils.timestampToMS(exif.exif.DateTimeOriginal, offset); - metadata.creationDateOffset = offset; - } else if (exif.exif.CreateDate) { //using else if here, because DateTimeOriginal has preceedence - //Create is when the camera wrote the file (typically within the same ms as shutter close) - let offset = exif.exif.OffsetTimeDigitized; //OffsetTimeDigitized is the corresponding offset - if (!offset) { //Find offset among other options if possible - offset = exif.exif.OffsetTimeOriginal || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); - } - metadata.creationDate = Utils.timestampToMS(exif.exif.CreateDate, offset); - metadata.creationDateOffset = offset; - } else if (exif.ifd0?.ModifyDate) { //using else if here, because DateTimeOriginal and CreatDate have preceedence - let offset = exif.exif.OffsetTime; //exif.Offsettime is the offset corresponding to ifd0.ModifyDate - if (!offset) { //Find offset among other options if possible - offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); - } - metadata.creationDate = Utils.timestampToMS(exif.ifd0.ModifyDate, offset); - metadata.creationDateOffset = offset - } else if (exif.ihdr && exif.ihdr["Creation Time"]) {// again else if (another fallback date if the good ones aren't there) { - const any_offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); - metadata.creationDate = Utils.timestampToMS(exif.ihdr["Creation Time"], any_offset); - metadata.creationDateOffset = any_offset; - } else if (exif.xmp?.MetadataDate) {// again else if (another fallback date if the good ones aren't there - metadata date is probably later than actual creation date, but much better than file time) { - const any_offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); - metadata.creationDate = Utils.timestampToMS(exif.xmp.MetadataDate, any_offset); - metadata.creationDateOffset = any_offset; - } - if (exif.exif.LensModel && exif.exif.LensModel !== '') { - metadata.cameraData = metadata.cameraData || {}; - metadata.cameraData.lens = '' + exif.exif.LensModel; - } - if (Utils.isUInt32(exif.exif.ISO)) { - metadata.cameraData = metadata.cameraData || {}; - metadata.cameraData.ISO = parseInt('' + exif.exif.ISO, 10); - } - if (Utils.isFloat32(exif.exif.FocalLength)) { - metadata.cameraData = metadata.cameraData || {}; - metadata.cameraData.focalLength = parseFloat( - '' + exif.exif.FocalLength - ); - } - if (Utils.isFloat32(exif.exif.ExposureTime)) { - metadata.cameraData = metadata.cameraData || {}; - metadata.cameraData.exposure = parseFloat( - parseFloat('' + exif.exif.ExposureTime).toFixed(6) - ); - } - if (Utils.isFloat32(exif.exif.FNumber)) { - metadata.cameraData = metadata.cameraData || {}; - metadata.cameraData.fStop = parseFloat( - parseFloat('' + exif.exif.FNumber).toFixed(2) - ); - } - if (exif.exif.ExifImageWidth && metadata.size.width <= 0) { - metadata.size.width = exif.exif.ExifImageWidth; - } - if (exif.exif.ExifImageHeight && metadata.size.height <= 0) { - metadata.size.height = exif.exif.ExifImageHeight; - } - } - - //gps section - if (exif.gps) { - metadata.positionData = metadata.positionData || {}; - metadata.positionData.GPSData = metadata.positionData.GPSData || {}; - - if (Utils.isFloat32(exif.gps.longitude)) { - metadata.positionData.GPSData.longitude = parseFloat( - exif.gps.longitude.toFixed(6) - ); - } - if (Utils.isFloat32(exif.gps.latitude)) { - metadata.positionData.GPSData.latitude = parseFloat( - exif.gps.latitude.toFixed(6) - ); - } - - if (metadata.positionData) { - if (!metadata.positionData.GPSData || - Object.keys(metadata.positionData.GPSData).length === 0) { - metadata.positionData.GPSData = undefined; - metadata.positionData = undefined; - } - } - } - //photoshop section (sometimes has City, Country and State) - if (exif.photoshop) { - if (!metadata.positionData?.country && exif.photoshop.Country) { - metadata.positionData = metadata.positionData || {}; - metadata.positionData.country = unescape(exif.photoshop.Country); - } - if (!metadata.positionData?.state && exif.photoshop.State) { - metadata.positionData = metadata.positionData || {}; - metadata.positionData.state = unescape(exif.photoshop.State); - } - if (!metadata.positionData?.city && exif.photoshop.City) { - metadata.positionData = metadata.positionData || {}; - metadata.positionData.city = unescape(exif.photoshop.City); - } - } - - /////////////////////////////////////// - metadata.size.height = Math.max(metadata.size.height, 1); //ensure height dimension is positive - metadata.size.width = Math.max(metadata.size.width, 1); //ensure width dimension is positive - - //Before moving on to the XMP section (particularly the regions (mwg-rs)) - //we need to switch width and height for images that are rotated sideways - if (4 < orientation) { //Orientation is sideways (rotated 90% or 270%) - // noinspection JSSuspiciousNameCombination - const height = metadata.size.width; - // noinspection JSSuspiciousNameCombination - metadata.size.width = metadata.size.height; - metadata.size.height = height; - } - /////////////////////////////////////// - - //xmp section - if (exif.xmp && exif.xmp.Rating) { - metadata.rating = exif.xmp.Rating; - if (metadata.rating < 0) { - metadata.rating = 0; - } - } - //xmp."mwg-rs" section - if (Config.Faces.enabled && - exif["mwg-rs"] && - exif["mwg-rs"].Regions) { - const faces: FaceRegion[] = []; - const regionListVal = Array.isArray(exif["mwg-rs"].Regions.RegionList) ? exif["mwg-rs"].Regions.RegionList : [exif["mwg-rs"].Regions.RegionList]; - if (regionListVal) { - for (const regionRoot of regionListVal) { - let type; - let name; - let box; - const createFaceBox = ( - w: string, - h: string, - x: string, - y: string - ) => { - if (4 < orientation) { //roation is sidewards (90 or 270 degrees) - [x, y] = [y, x]; - [w, h] = [h, w]; - } - let swapX = 0; - let swapY = 0; - switch (orientation) { - case 2: //TOP RIGHT (Mirror horizontal): - case 6: //RIGHT TOP (Rotate 90 CW) - swapX = 1; - break; - case 3: // BOTTOM RIGHT (Rotate 180) - case 7: // RIGHT BOTTOM (Mirror horizontal and rotate 90 CW) - swapX = 1; - swapY = 1; - break; - case 4: //BOTTOM_LEFT (Mirror vertical) - case 8: //LEFT_BOTTOM (Rotate 270 CW) - swapY = 1; - break; - } - // converting ratio to px - return { - width: Math.round(parseFloat(w) * metadata.size.width), - height: Math.round(parseFloat(h) * metadata.size.height), - left: Math.round(Math.abs(parseFloat(x) - swapX) * metadata.size.width), - top: Math.round(Math.abs(parseFloat(y) - swapY) * metadata.size.height), - }; - }; - /* Adobe Lightroom based face region structure */ - if ( - regionRoot && - regionRoot['rdf:Description'] && - regionRoot['rdf:Description'] && - regionRoot['rdf:Description']['mwg-rs:Area'] - ) { - const region = regionRoot['rdf:Description']; - const regionBox = region['mwg-rs:Area'].attributes; - - name = region['mwg-rs:Name']; - type = region['mwg-rs:Type']; - box = createFaceBox( - regionBox['stArea:w'], - regionBox['stArea:h'], - regionBox['stArea:x'], - regionBox['stArea:y'] - ); - /* Load exiftool edited face region structure, see github issue #191 */ - } else if ( - regionRoot && - regionRoot.Name && - regionRoot.Type && - regionRoot.Area - ) { - const regionBox = regionRoot.Area; - name = regionRoot.Name; - type = regionRoot.Type; - box = createFaceBox( - regionBox.w, - regionBox.h, - regionBox.x, - regionBox.y - ); - } - - if (type !== 'Face' || !name) { - continue; - } - - // convert center base box to corner based box - box.left = Math.round(Math.max(0, box.left - box.width / 2)); - box.top = Math.round(Math.max(0, box.top - box.height / 2)); - - - faces.push({ name, box }); - } - } - if (faces.length > 0) { - metadata.faces = faces; // save faces - if (Config.Faces.keywordsToPersons) { - // remove faces from keywords - metadata.faces.forEach((f) => { - const index = metadata.keywords.indexOf(f.name); - if (index !== -1) { - metadata.keywords.splice(index, 1); - } - }); - } - } - } + MetadataLoader.mapMetadata(metadata, exif); } catch (err) { // ignoring errors } - if (!metadata.creationDate) { - // creationDate can be negative, when it was created before epoch (1970) - metadata.creationDate = 0; - } - try { // search for sidecar and merge metadata const fullPathWithoutExt = path.join(path.parse(fullPath).dir, path.parse(fullPath).name); @@ -620,74 +279,9 @@ export class MetadataLoader { for (const sidecarPath of sidecarPaths) { if (fs.existsSync(sidecarPath)) { - const sidecarData = await exifr.sidecar(sidecarPath); - + const sidecarData: any = await exifr.sidecar(sidecarPath, exifrOptions); if (sidecarData !== undefined) { - if ((sidecarData as SideCar).dc !== undefined) { - if ((sidecarData as SideCar).dc.subject !== undefined) { - if (metadata.keywords === undefined) { - metadata.keywords = []; - } - let keywords = (sidecarData as SideCar).dc.subject || []; - if (typeof keywords === 'string') { - keywords = [keywords]; - } - for (const kw of keywords) { - if (metadata.keywords.indexOf(kw) === -1) { - metadata.keywords.push(kw); - } - } - } - } - let hasPhotoshopDate = false; - if ((sidecarData as SideCar).photoshop !== undefined) { - if ((sidecarData as SideCar).photoshop.DateCreated !== undefined) { - const date = Utils.timestampToMS((sidecarData as SideCar).photoshop.DateCreated, null); - if (date) { - metadata.creationDate = date; - hasPhotoshopDate = true; - } - } - } - if (Object.hasOwn(sidecarData, 'xap')) { - (sidecarData as any)['xmp'] = (sidecarData as any)['xap']; - delete (sidecarData as any)['xap']; - } - if ((sidecarData as SideCar).xmp !== undefined) { - if ( - (sidecarData as SideCar).xmp.Rating !== undefined && - (sidecarData as SideCar).xmp.Rating > 0 - ) { - metadata.rating = (sidecarData as SideCar).xmp.Rating; - } - if ( - !hasPhotoshopDate && ( - (sidecarData as SideCar).xmp.CreateDate !== undefined || - (sidecarData as SideCar).xmp.ModifyDate !== undefined - ) - ) { - metadata.creationDate = - Utils.timestampToMS((sidecarData as SideCar).xmp.CreateDate, null) || - Utils.timestampToMS((sidecarData as SideCar).xmp.ModifyDate, null) || - metadata.creationDate; - } - } - if ((sidecarData as SideCar).exif !== undefined) { - if ( - (sidecarData as SideCar).exif.GPSLatitude !== undefined && - (sidecarData as SideCar).exif.GPSLongitude !== undefined - ) { - metadata.positionData = metadata.positionData || {}; - metadata.positionData.GPSData = {}; - - metadata.positionData.GPSData.longitude = Utils.xmpExifGpsCoordinateToDecimalDegrees( - (sidecarData as SideCar).exif.GPSLongitude - ); - metadata.positionData.GPSData.latitude = Utils.xmpExifGpsCoordinateToDecimalDegrees( - (sidecarData as SideCar).exif.GPSLatitude - ); - } - } + MetadataLoader.mapMetadata(metadata, sidecarData); } } } @@ -695,7 +289,10 @@ export class MetadataLoader { Logger.silly(LOG_TAG, 'Error loading sidecar metadata for : ' + fullPath); Logger.silly(err); } - + if (!metadata.creationDate) { + // creationDate can be negative, when it was created before epoch (1970) + metadata.creationDate = 0; + } } catch (err) { Logger.error(LOG_TAG, 'Error during reading photo: ' + fullPath); console.error(err); @@ -708,4 +305,320 @@ export class MetadataLoader { } return metadata; } + + private static mapMetadata(metadata: PhotoMetadata, exif: any) { + //replace adobe xap-section with xmp to reuse parsing + if (Object.hasOwn(exif, 'xap')) { + exif['xmp'] = exif['xap']; + delete exif['xap']; + } + let orientation = MetadataLoader.getOrientation(exif); + MetadataLoader.mapImageDimensions(metadata, exif, orientation); + MetadataLoader.mapKeywords(metadata, exif); + MetadataLoader.mapTimestampAndOffset(metadata, exif); + MetadataLoader.mapCameraData(metadata, exif); + MetadataLoader.mapGPS(metadata, exif); + MetadataLoader.mapToponyms(metadata, exif); + MetadataLoader.mapRating(metadata, exif); + if (Config.Faces.enabled) { + MetadataLoader.mapFaces(metadata, exif, orientation); + } + + } + private static getOrientation(exif: any): number { + let orientation = 1; //Orientation 1 is normal + if (exif.ifd0?.Orientation != undefined) { + orientation = parseInt(exif.ifd0.Orientation as any, 10) as number; + } + return orientation; + } + + private static mapImageDimensions(metadata: PhotoMetadata, exif: any, orientation: number) { + if (metadata.size.width <= 0) { + metadata.size.width = exif.ifd0?.ImageWidth || exif.exif?.ExifImageWidth; + } + if (metadata.size.height <= 0) { + metadata.size.height = exif.ifd0?.ImageHeight || exif.exif?.ExifImageHeight; + } + metadata.size.height = Math.max(metadata.size.height, 1); //ensure height dimension is positive + metadata.size.width = Math.max(metadata.size.width, 1); //ensure width dimension is positive + + //we need to switch width and height for images that are rotated sideways + if (4 < orientation) { //Orientation is sideways (rotated 90% or 270%) + // noinspection JSSuspiciousNameCombination + const height = metadata.size.width; + // noinspection JSSuspiciousNameCombination + metadata.size.width = metadata.size.height; + metadata.size.height = height; + } + } + + private static mapKeywords(metadata: PhotoMetadata, exif: any) { + if (exif.dc && + exif.dc.subject && + exif.dc.subject.length > 0) { + const subj = Array.isArray(exif.dc.subject) ? exif.dc.subject : [exif.dc.subject]; + if (metadata.keywords === undefined) { + metadata.keywords = []; + } + for (const kw of subj) { + if (metadata.keywords.indexOf(kw) === -1) { + metadata.keywords.push(kw); + } + } + } + } + + private static mapTimestampAndOffset(metadata: PhotoMetadata, exif: any) { + metadata.creationDate = Utils.timestampToMS(exif?.photoshop?.DateCreated, null) || + Utils.timestampToMS(exif?.xmp?.CreateDate, null) || + Utils.timestampToMS(exif?.xmp?.ModifyDate, null) || + metadata.creationDate; + + metadata.creationDateOffset = Utils.timestampToOffsetString(exif?.photoshop?.DateCreated) || + Utils.timestampToOffsetString(exif?.xmp?.CreateDate) || + metadata.creationDateOffset; + if (exif.exif) { + let offset = undefined; + //Preceedence of dates: exif.DateTimeOriginal, exif.CreateDate, ifd0.ModifyDate, ihdr["Creation Time"], xmp.MetadataDate, file system date + //Filesystem is the absolute last resort, and it's hard to write tests for, since file system dates are changed on e.g. git clone. + if (exif.exif.DateTimeOriginal) { + //DateTimeOriginal is when the camera shutter closed + offset = exif.exif.OffsetTimeOriginal; //OffsetTimeOriginal is the corresponding offset + if (!offset) { //Find offset among other options if possible + offset = exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); + } + metadata.creationDate = Utils.timestampToMS(exif.exif.DateTimeOriginal, offset); + } else if (exif.exif.CreateDate) { //using else if here, because DateTimeOriginal has preceedence + //Create is when the camera wrote the file (typically within the same ms as shutter close) + offset = exif.exif.OffsetTimeDigitized; //OffsetTimeDigitized is the corresponding offset + if (!offset) { //Find offset among other options if possible + offset = exif.exif.OffsetTimeOriginal || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); + } + metadata.creationDate = Utils.timestampToMS(exif.exif.CreateDate, offset); + } else if (exif.ifd0?.ModifyDate) { //using else if here, because DateTimeOriginal and CreatDate have preceedence + offset = exif.exif.OffsetTime; //exif.Offsettime is the offset corresponding to ifd0.ModifyDate + if (!offset) { //Find offset among other options if possible + offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); + } + metadata.creationDate = Utils.timestampToMS(exif.ifd0.ModifyDate, offset); + } else if (exif.ihdr && exif.ihdr["Creation Time"]) {// again else if (another fallback date if the good ones aren't there) { + const any_offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); + metadata.creationDate = Utils.timestampToMS(exif.ihdr["Creation Time"], any_offset); + offset = any_offset; + } else if (exif.xmp?.MetadataDate) {// again else if (another fallback date if the good ones aren't there - metadata date is probably later than actual creation date, but much better than file time) { + const any_offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); + metadata.creationDate = Utils.timestampToMS(exif.xmp.MetadataDate, any_offset); + offset = any_offset; + } + metadata.creationDateOffset = offset || metadata.creationDateOffset; + } + } + + private static mapCameraData(metadata: PhotoMetadata, exif: any) { + metadata.cameraData = metadata.cameraData || {}; + if (exif.ifd0) { + if (exif.ifd0.Make && exif.ifd0.Make !== '') { + metadata.cameraData.make = '' + exif.ifd0.Make; + } + if (exif.ifd0.Model && exif.ifd0.Model !== '') { + metadata.cameraData.model = '' + exif.ifd0.Model; + } + } + if (exif.exif) { + if (exif.exif.LensModel && exif.exif.LensModel !== '') { + metadata.cameraData.lens = '' + exif.exif.LensModel; + } + if (Utils.isUInt32(exif.exif.ISO)) { + metadata.cameraData.ISO = parseInt('' + exif.exif.ISO, 10); + } + if (Utils.isFloat32(exif.exif.FocalLength)) { + metadata.cameraData.focalLength = parseFloat( + '' + exif.exif.FocalLength + ); + } + if (Utils.isFloat32(exif.exif.ExposureTime)) { + metadata.cameraData.exposure = parseFloat( + parseFloat('' + exif.exif.ExposureTime).toFixed(6) + ); + } + if (Utils.isFloat32(exif.exif.FNumber)) { + metadata.cameraData.fStop = parseFloat( + parseFloat('' + exif.exif.FNumber).toFixed(2) + ); + } + } + if (Object.keys(metadata.cameraData).length === 0) { + delete metadata.cameraData; + } + } + + private static mapGPS(metadata: PhotoMetadata, exif: any) { + try { + if (exif.gps || (exif.exif && exif.exif.GPSLatitude && exif.exif.GPSLongitude)) { + metadata.positionData = metadata.positionData || {}; + metadata.positionData.GPSData = metadata.positionData.GPSData || {}; + + metadata.positionData.GPSData.longitude = Utils.isFloat32(exif.gps?.longitude) ? exif.gps.longitude : Utils.xmpExifGpsCoordinateToDecimalDegrees(exif.exif.GPSLongitude); + metadata.positionData.GPSData.latitude = Utils.isFloat32(exif.gps?.latitude) ? exif.gps.latitude : Utils.xmpExifGpsCoordinateToDecimalDegrees(exif.exif.GPSLatitude); + + metadata.positionData.GPSData.longitude = parseFloat(metadata.positionData.GPSData.longitude.toFixed(6)) + metadata.positionData.GPSData.latitude = parseFloat(metadata.positionData.GPSData.latitude.toFixed(6)) + } + } catch (err) { + Logger.error(LOG_TAG, 'Error during reading of GPS data: ' + err); + } finally { + if (metadata.positionData?.GPSData && + (Object.keys(metadata.positionData.GPSData).length === 0 || + metadata.positionData.GPSData.longitude === undefined || + metadata.positionData.GPSData.latitude === undefined)) { + delete metadata.positionData.GPSData; + } + if (metadata.positionData) { + if (Object.keys(metadata.positionData).length === 0) { + delete metadata.positionData; + } + } + } + } + + private static mapToponyms(metadata: PhotoMetadata, exif: any) { + //Function to convert html code for special characters into their corresponding character (used in exif.photoshop-section) + const unescape = (tag: string) => { + return tag.replace(/&#([0-9]{1,3});/gi, function (match, numStr) { + return String.fromCharCode(parseInt(numStr, 10)); + }); + } + //photoshop section sometimes has City, Country and State + if (exif.photoshop) { + if (!metadata.positionData?.country && exif.photoshop.Country) { + metadata.positionData = metadata.positionData || {}; + metadata.positionData.country = unescape(exif.photoshop.Country); + } + if (!metadata.positionData?.state && exif.photoshop.State) { + metadata.positionData = metadata.positionData || {}; + metadata.positionData.state = unescape(exif.photoshop.State); + } + if (!metadata.positionData?.city && exif.photoshop.City) { + metadata.positionData = metadata.positionData || {}; + metadata.positionData.city = unescape(exif.photoshop.City); + } + } + } + + private static mapRating(metadata: PhotoMetadata, exif: any) { + if (exif.xmp && + exif.xmp.Rating !== undefined) { + metadata.rating = exif.xmp.Rating; + } + } + + private static mapFaces(metadata: PhotoMetadata, exif: any, orientation: number) { + //xmp."mwg-rs" section + if (exif["mwg-rs"] && + exif["mwg-rs"].Regions) { + const faces: FaceRegion[] = []; + const regionListVal = Array.isArray(exif["mwg-rs"].Regions.RegionList) ? exif["mwg-rs"].Regions.RegionList : [exif["mwg-rs"].Regions.RegionList]; + if (regionListVal) { + for (const regionRoot of regionListVal) { + let type; + let name; + let box; + const createFaceBox = ( + w: string, + h: string, + x: string, + y: string + ) => { + if (4 < orientation) { //roation is sidewards (90 or 270 degrees) + [x, y] = [y, x]; + [w, h] = [h, w]; + } + let swapX = 0; + let swapY = 0; + switch (orientation) { + case 2: //TOP RIGHT (Mirror horizontal): + case 6: //RIGHT TOP (Rotate 90 CW) + swapX = 1; + break; + case 3: // BOTTOM RIGHT (Rotate 180) + case 7: // RIGHT BOTTOM (Mirror horizontal and rotate 90 CW) + swapX = 1; + swapY = 1; + break; + case 4: //BOTTOM_LEFT (Mirror vertical) + case 8: //LEFT_BOTTOM (Rotate 270 CW) + swapY = 1; + break; + } + // converting ratio to px + return { + width: Math.round(parseFloat(w) * metadata.size.width), + height: Math.round(parseFloat(h) * metadata.size.height), + left: Math.round(Math.abs(parseFloat(x) - swapX) * metadata.size.width), + top: Math.round(Math.abs(parseFloat(y) - swapY) * metadata.size.height), + }; + }; + /* Adobe Lightroom based face region structure */ + if ( + regionRoot && + regionRoot['rdf:Description'] && + regionRoot['rdf:Description'] && + regionRoot['rdf:Description']['mwg-rs:Area'] + ) { + const region = regionRoot['rdf:Description']; + const regionBox = region['mwg-rs:Area'].attributes; + + name = region['mwg-rs:Name']; + type = region['mwg-rs:Type']; + box = createFaceBox( + regionBox['stArea:w'], + regionBox['stArea:h'], + regionBox['stArea:x'], + regionBox['stArea:y'] + ); + /* Load exiftool edited face region structure, see github issue #191 */ + } else if ( + regionRoot && + regionRoot.Name && + regionRoot.Type && + regionRoot.Area + ) { + const regionBox = regionRoot.Area; + name = regionRoot.Name; + type = regionRoot.Type; + box = createFaceBox( + regionBox.w, + regionBox.h, + regionBox.x, + regionBox.y + ); + } + + if (type !== 'Face' || !name) { + continue; + } + + // convert center base box to corner based box + box.left = Math.round(Math.max(0, box.left - box.width / 2)); + box.top = Math.round(Math.max(0, box.top - box.height / 2)); + + + faces.push({ name, box }); + } + } + if (faces.length > 0) { + metadata.faces = faces; // save faces + if (Config.Faces.keywordsToPersons) { + // remove faces from keywords + metadata.faces.forEach((f) => { + const index = metadata.keywords.indexOf(f.name); + if (index !== -1) { + metadata.keywords.splice(index, 1); + } + }); + } + } + } + } } diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 710a43f8..44604e75 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -141,6 +141,22 @@ export class Utils { return Date.parse(formattedTimestamp); } + //function to extract offset string from timestamp string, returns undefined if timestamp does not contain offset + static timestampToOffsetString(timestamp: string) { + try { + const idx = timestamp.indexOf("+"); + if (idx > 0) { + return timestamp.substring(idx, timestamp.length); + } + if (timestamp.indexOf("Z") > 0) { + return '+00:00'; + } + return undefined; + } catch (err) { + return undefined; + } + } + //function to calculate offset from exif.exif.gpsTimeStamp or exif.gps.GPSDateStamp + exif.gps.GPSTimestamp static getTimeOffsetByGPSStamp(timestamp: string, gpsTimeStamp: string, gps: any) { let UTCTimestamp = gpsTimeStamp; @@ -383,11 +399,14 @@ export class Utils { } public static xmpExifGpsCoordinateToDecimalDegrees(text: string): number { + if (!text) { + return undefined; + } const parts = text.match(/^([0-9]+),([0-9.]+)([EWNS])$/); const degrees: number = parseInt(parts[1], 10); const minutes: number = parseFloat(parts[2]); const sign = (parts[3] === "N" || parts[3] === "E") ? 1 : -1; - return sign * (degrees + (minutes / 60.0)) + return (sign * (degrees + (minutes / 60.0))) } } diff --git a/src/common/entities/MediaDTO.ts b/src/common/entities/MediaDTO.ts index 05acc4ce..92807a39 100644 --- a/src/common/entities/MediaDTO.ts +++ b/src/common/entities/MediaDTO.ts @@ -29,34 +29,6 @@ export interface MediaDimension { height: number; } -export interface SideCar { - exif?: SideCarExif; - dc?: SideCarDc; - xmp?: SideCarXmp; - photoshop?: SideCarPhotoshop; -} - -export interface SideCarExif { - GPSLatitude?: string; - GPSLongitude?: string; -} - -export interface SideCarDc { - subject?: string[]; -} - -export interface SideCarXmp { - Rating?: RatingTypes; - CreateDate?: string; - ModifyDate?: string; -} - -export interface SideCarPhotoshop { - // Corresponds to Exif.Photo.DateTimeOriginal. No corresponding key exists in - // the xmp namespace! - DateCreated?: string; -} - export const MediaDTOUtils = { hasPositionData: (media: MediaDTO): boolean => { return ( diff --git a/test/backend/assets/Chars.json b/test/backend/assets/Chars.json index 83bec784..03352e6c 100644 --- a/test/backend/assets/Chars.json +++ b/test/backend/assets/Chars.json @@ -17,7 +17,7 @@ }, "keywords": [ ], - "rating": 0, + "rating": -1, "faces": [ { "box": { diff --git a/test/backend/assets/Chars_exiftool.json b/test/backend/assets/Chars_exiftool.json index 4c09dfec..7f57c2f2 100644 --- a/test/backend/assets/Chars_exiftool.json +++ b/test/backend/assets/Chars_exiftool.json @@ -16,7 +16,7 @@ }, "keywords": [ ], - "rating": 0, + "rating": -1, "faces": [ { "box": { diff --git a/test/backend/assets/sidecar/20240107_110258.json b/test/backend/assets/sidecar/20240107_110258.json index a466e4f9..0bece513 100644 --- a/test/backend/assets/sidecar/20240107_110258.json +++ b/test/backend/assets/sidecar/20240107_110258.json @@ -20,8 +20,8 @@ ], "positionData": { "GPSData": { - "latitude": 50.08958748333333, - "longitude": 14.397409516666666 + "latitude": 50.089587, + "longitude": 14.39741 } }, "rating": 3 diff --git a/test/backend/assets/sidecar/20240121_102400.json b/test/backend/assets/sidecar/20240121_102400.json index 551b7d9a..58c49509 100644 --- a/test/backend/assets/sidecar/20240121_102400.json +++ b/test/backend/assets/sidecar/20240121_102400.json @@ -21,8 +21,8 @@ ], "positionData": { "GPSData": { - "latitude": 50.08958748333333, - "longitude": 14.397409516666666 + "latitude": 50.089587, + "longitude": 14.39741 } }, "rating": 3 diff --git a/test/backend/assets/sidecar/20240128_105420.json b/test/backend/assets/sidecar/20240128_105420.json index 40587ac4..de4bde68 100644 --- a/test/backend/assets/sidecar/20240128_105420.json +++ b/test/backend/assets/sidecar/20240128_105420.json @@ -11,5 +11,11 @@ "keywords": [ "Travel" ], - "rating": 3 -} + "rating": 3, + "positionData": { + "GPSData": { + "latitude": 50.089587, + "longitude": 14.39741 + } + } +} \ No newline at end of file diff --git a/test/backend/assets/sidecar/20240128_120909.json b/test/backend/assets/sidecar/20240128_120909.json index c8e2ddd0..22ee4209 100644 --- a/test/backend/assets/sidecar/20240128_120909.json +++ b/test/backend/assets/sidecar/20240128_120909.json @@ -11,5 +11,11 @@ "keywords": [ "Travel" ], - "rating": 3 -} + "rating": 3, + "positionData": { + "GPSData": { + "latitude": 50.089587, + "longitude": 14.39741 + } + } +} \ No newline at end of file diff --git a/test/backend/assets/sidecar/20240128_185808.json b/test/backend/assets/sidecar/20240128_185808.json index 9d53fca1..e9defe72 100644 --- a/test/backend/assets/sidecar/20240128_185808.json +++ b/test/backend/assets/sidecar/20240128_185808.json @@ -21,8 +21,8 @@ ], "positionData": { "GPSData": { - "latitude": 50.08958748333333, - "longitude": 14.397409516666666 + "latitude": 50.089587, + "longitude": 14.39741 } }, "rating": 3 diff --git a/test/backend/assets/sidecar/Chars.jpg b/test/backend/assets/sidecar/Chars.jpg new file mode 100644 index 00000000..2bf92826 Binary files /dev/null and b/test/backend/assets/sidecar/Chars.jpg differ diff --git a/test/backend/assets/sidecar/Chars.jpg.xmp b/test/backend/assets/sidecar/Chars.jpg.xmp new file mode 100644 index 00000000..c7e83b2a --- /dev/null +++ b/test/backend/assets/sidecar/Chars.jpg.xmp @@ -0,0 +1,172 @@ + + + + + + SWE + Jönköping + + + + + + æÆøØåÅéÉüÜäÄöÖïÏñÑ + abcdefghijklmnopqrstuvwxyz + abcdefghijklmnopqrstuvwxyz + + + + + + + + + + æÆøØåÅéÉüÜäÄöÖïÏñÑ + 0.294791666666667, 0.13287037037037, 0.107291666666667, 0.237962962962963 + + + abcdefghijklmnopqrstuvwxyz + 0.451041666666667, 0.13287037037037, 0.110416666666667, 0.24537037037037 + + + abcdefghijklmnopqrstuvwxyz + 0.605208333333333, 0.138425925925926, 0.110416666666667, 0.24537037037037 + + + + + + + + + + æÆøØåÅéÉüÜäÄöÖïÏñÑ + abcdefghijklmnopqrstuvwxyz + + + + + + 65535 + + + 1 + 2 + 3 + 0 + + + 2024-01-31T00:02:07+01:00 + 0232 + 0100 + 102/1 + 0 + 57,46.8417528N + 14,9.7753146E + WGS-84 + 1899-12-30T01:00:14Z + 2.2.0.0 + + + + + + 1080 + pixel + 1920 + + + + + + 0.237962962962963 + normalized + 0.107291666666667 + 0.3484375 + 0.251851851851852 + + æÆøØåÅéÉüÜäÄöÖïÏñÑ + Face + + + + 0.24537037037037 + normalized + 0.110416666666667 + 0.50625 + 0.255555555555556 + + abcdefghijklmnopqrstuvwxyz + Face + + + + 0.24537037037037 + normalized + 0.110416666666667 + 0.660416666666667 + 0.261111111111111 + + abcdefghijklmnopqrstuvwxyz + Face + + + + + + + + æÆøØåÅéÉüÜäÄöÖïÏñÑ, abcdefghijklmnopqrstuvwxyz + + + + Jönköping + Sverige + Jönköping + + + + + + 8 + + + 1080 + 1920 + 2 + 300/1 + 1 + + + 1 + 1 + + + 300/1 + + + + 2024-01-31T00:02:07+01:00 + Tag That Photo + 2024-01-30T23:02:06Z + 2024-01-31T00:02:07+01:00 + -1 + + + + \ No newline at end of file diff --git a/test/backend/assets/sidecar/Chars.json b/test/backend/assets/sidecar/Chars.json new file mode 100644 index 00000000..b30a3ded --- /dev/null +++ b/test/backend/assets/sidecar/Chars.json @@ -0,0 +1,50 @@ +{ + "size": { + "width": 1920, + "height": 1080 + }, + "creationDate": 1706655727000, + "creationDateOffset": "+01:00", + "fileSize": 101948, + "positionData": { + "GPSData": { + "longitude": 14.162922, + "latitude": 57.780696 + }, + "country": "Sverige", + "state": "Jönköping", + "city": "Jönköping" + }, + "keywords": [ + ], + "rating": -1, + "faces": [ + { + "box": { + "width": 206, + "height": 257, + "left": 566, + "top": 144 + }, + "name": "æÆøØåÅéÉüÜäÄöÖïÏñÑ" + }, + { + "name": "abcdefghijklmnopqrstuvwxyz", + "box": { + "width": 212, + "height": 265, + "left": 866, + "top": 144 + } + }, + { + "name": "abcdefghijklmnopqrstuvwxyz", + "box": { + "width": 212, + "height": 265, + "left": 1162, + "top": 150 + } + } + ] +} \ No newline at end of file diff --git a/test/backend/assets/sidecar/Chars_exiftool.jpg b/test/backend/assets/sidecar/Chars_exiftool.jpg new file mode 100644 index 00000000..2bf92826 Binary files /dev/null and b/test/backend/assets/sidecar/Chars_exiftool.jpg differ diff --git a/test/backend/assets/sidecar/Chars_exiftool.jpg.xmp b/test/backend/assets/sidecar/Chars_exiftool.jpg.xmp new file mode 100644 index 00000000..efdfb0b9 --- /dev/null +++ b/test/backend/assets/sidecar/Chars_exiftool.jpg.xmp @@ -0,0 +1,173 @@ + + + + + + SWE + Jönköping + + + + + + æÆøØåÅéÉüÜäÄöÖïÏñÑ + abcdefghijklmnopqrstuvwxyz + abcdefghijklmnopqrstuvwxyz + + + + + + + + + + æÆøØåÅéÉüÜäÄöÖïÏñÑ + 0.294791666666667, 0.13287037037037, 0.107291666666667, 0.237962962962963 + + + abcdefghijklmnopqrstuvwxyz + 0.451041666666667, 0.13287037037037, 0.110416666666667, 0.24537037037037 + + + abcdefghijklmnopqrstuvwxyz + 0.605208333333333, 0.138425925925926, 0.110416666666667, 0.24537037037037 + + + + + + + + + + æÆøØåÅéÉüÜäÄöÖïÏñÑ + abcdefghijklmnopqrstuvwxyz + + + + + + 65535 + + + 1 + 2 + 3 + 0 + + + 2024-01-30T12:00:00+01:00 + 0232 + 0100 + 102/1 + 0 + 57,46.8417528N + 14,9.7753146E + WGS-84 + 1899-12-30T01:00:14Z + 2.2.0.0 + + + + + + 1080 + pixel + 1920 + + + + + + 0.237962962962963 + normalized + 0.107291666666667 + 0.3484375 + 0.251851851851852 + + æÆøØåÅéÉüÜäÄöÖïÏñÑ + Face + + + + 0.24537037037037 + normalized + 0.110416666666667 + 0.50625 + 0.255555555555556 + + abcdefghijklmnopqrstuvwxyz + Face + + + + 0.24537037037037 + normalized + 0.110416666666667 + 0.660416666666667 + 0.261111111111111 + + abcdefghijklmnopqrstuvwxyz + Face + + + + + + + + æÆøØåÅéÉüÜäÄöÖïÏñÑ, abcdefghijklmnopqrstuvwxyz + + + + Jönköping + Sverige + Jönköping + + + + + + 8 + + + 1080 + 1920 + 1 + 2 + 300/1 + 1 + + + 1 + 1 + + + 300/1 + + + + 2024-01-30T12:00:00+01:00 + Tag That Photo + 2024-01-30T23:02:06Z + 2024-01-30T12:00:00+01:00 + -1 + + + + \ No newline at end of file diff --git a/test/backend/assets/sidecar/Chars_exiftool.json b/test/backend/assets/sidecar/Chars_exiftool.json new file mode 100644 index 00000000..febff88f --- /dev/null +++ b/test/backend/assets/sidecar/Chars_exiftool.json @@ -0,0 +1,50 @@ +{ + "size": { + "width": 1920, + "height": 1080 + }, + "creationDate": 1706612400000, + "creationDateOffset": "+01:00", + "fileSize": 101948, + "positionData": { + "GPSData": { + "longitude": 14.162922, + "latitude": 57.780696 + }, + "country": "Sverige", + "state": "Jönköping", + "city": "Jönköping" + }, + "keywords": [ + ], + "rating": -1, + "faces": [ + { + "box": { + "width": 206, + "height": 257, + "left": 566, + "top": 144 + }, + "name": "æÆøØåÅéÉüÜäÄöÖïÏñÑ" + }, + { + "name": "abcdefghijklmnopqrstuvwxyz", + "box": { + "width": 212, + "height": 265, + "left": 866, + "top": 144 + } + }, + { + "name": "abcdefghijklmnopqrstuvwxyz", + "box": { + "width": 212, + "height": 265, + "left": 1162, + "top": 150 + } + } + ] +} \ No newline at end of file diff --git a/test/backend/assets/sidecar/bunny_1sec.json b/test/backend/assets/sidecar/bunny_1sec.json index eec9a09a..d7cd2e46 100644 --- a/test/backend/assets/sidecar/bunny_1sec.json +++ b/test/backend/assets/sidecar/bunny_1sec.json @@ -6,6 +6,7 @@ "bitRate": 1794127, "duration": 290, "creationDate": 1542482851000, + "creationDateOffset": "+01:00", "fileSize": 65073, "fps": 40000, "keywords": [ diff --git a/test/backend/assets/sidecar/bunny_1sec_v2.json b/test/backend/assets/sidecar/bunny_1sec_v2.json index eec9a09a..d7cd2e46 100644 --- a/test/backend/assets/sidecar/bunny_1sec_v2.json +++ b/test/backend/assets/sidecar/bunny_1sec_v2.json @@ -6,6 +6,7 @@ "bitRate": 1794127, "duration": 290, "creationDate": 1542482851000, + "creationDateOffset": "+01:00", "fileSize": 65073, "fps": 40000, "keywords": [ diff --git a/test/backend/assets/sidecar/bunny_1sec_v3.json b/test/backend/assets/sidecar/bunny_1sec_v3.json index d3abbdba..825f71e7 100644 --- a/test/backend/assets/sidecar/bunny_1sec_v3.json +++ b/test/backend/assets/sidecar/bunny_1sec_v3.json @@ -6,6 +6,7 @@ "bitRate": 1794127, "duration": 290, "creationDate": 1542482851000, + "creationDateOffset": "+01:00", "fileSize": 65073, "fps": 40000, "keywords": [ diff --git a/test/backend/assets/sidecar/metadata.json b/test/backend/assets/sidecar/metadata.json index 3593766b..1f7eab71 100644 --- a/test/backend/assets/sidecar/metadata.json +++ b/test/backend/assets/sidecar/metadata.json @@ -3,6 +3,7 @@ "width": 10, "height": 5 }, + "rating": 0, "creationDate": 1710188754000, "fileSize": 5095, "keywords": [ diff --git a/test/backend/assets/sidecar/metadata_v2.json b/test/backend/assets/sidecar/metadata_v2.json index 36a37353..ceda26fb 100644 --- a/test/backend/assets/sidecar/metadata_v2.json +++ b/test/backend/assets/sidecar/metadata_v2.json @@ -5,6 +5,7 @@ }, "creationDate": 1710188754000, "fileSize": 5095, + "rating": 0, "keywords": [ "floor", "book", diff --git a/test/backend/assets/sidecar/no_metadata.json b/test/backend/assets/sidecar/no_metadata.json index 2ce48ad6..6574ba17 100644 --- a/test/backend/assets/sidecar/no_metadata.json +++ b/test/backend/assets/sidecar/no_metadata.json @@ -4,6 +4,7 @@ "height": 5 }, "creationDate": 1542482851000, + "creationDateOffset": "+01:00", "fileSize": 1430, "keywords": [ "first", diff --git a/test/backend/assets/sidecar/no_metadata_v2.json b/test/backend/assets/sidecar/no_metadata_v2.json index 2ce48ad6..6574ba17 100644 --- a/test/backend/assets/sidecar/no_metadata_v2.json +++ b/test/backend/assets/sidecar/no_metadata_v2.json @@ -4,6 +4,7 @@ "height": 5 }, "creationDate": 1542482851000, + "creationDateOffset": "+01:00", "fileSize": 1430, "keywords": [ "first", diff --git a/test/backend/assets/sidecar/no_metadata_v3.json b/test/backend/assets/sidecar/no_metadata_v3.json index 7f904f44..2c27c2a6 100644 --- a/test/backend/assets/sidecar/no_metadata_v3.json +++ b/test/backend/assets/sidecar/no_metadata_v3.json @@ -4,6 +4,7 @@ "height": 5 }, "creationDate": 1542482851000, + "creationDateOffset": "+01:00", "fileSize": 1430, "keywords": [ "first" diff --git a/test/tmp/sqlite.db-journal b/test/tmp/sqlite.db-journal new file mode 100644 index 00000000..41fef9ee Binary files /dev/null and b/test/tmp/sqlite.db-journal differ