diff --git a/src/backend/model/fileaccess/MetadataLoader.ts b/src/backend/model/fileaccess/MetadataLoader.ts index d13a3796..fd7364de 100644 --- a/src/backend/model/fileaccess/MetadataLoader.ts +++ b/src/backend/model/fileaccess/MetadataLoader.ts @@ -82,9 +82,14 @@ export class MetadataLoader { if (Utils.isInt32(parseInt(stream.avg_frame_rate, 10))) { metadata.fps = parseInt(stream.avg_frame_rate, 10) || null; } - metadata.creationDate = - Date.parse(stream.tags.creation_time) || - metadata.creationDate; + if ( + stream.tags !== undefined && + typeof stream.tags.creation_time === 'string' + ) { + metadata.creationDate = + Date.parse(stream.tags.creation_time) || + metadata.creationDate; + } break; } } @@ -139,22 +144,51 @@ export class MetadataLoader { if (fs.existsSync(sidecarPath)) { const sidecarData = await exifr.sidecar(sidecarPath); if (sidecarData !== 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); + 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); + } } } } - if ((sidecarData as SideCar).xmp.Rating !== undefined) { - metadata.rating = (sidecarData as SideCar).xmp.Rating; + 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) { + 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; + } } } } @@ -199,43 +233,6 @@ export class MetadataLoader { mergeOutput: false //don't merge output, because things like Microsoft Rating (percent) and xmp.rating will be merged }; - //function to convert timestamp into milliseconds taking offset into account - const timestampToMS = (timestamp: string, offset: string) => { - if (!timestamp) { - return undefined; - } - //replace : with - in the yyyy-mm-dd part of the timestamp. - let formattedTimestamp = timestamp.substring(0,9).replaceAll(':', '-') + timestamp.substring(9,timestamp.length); - if (formattedTimestamp.indexOf("Z") > 0) { //replace Z (and what comes after the Z) with offset - formattedTimestamp.substring(0, formattedTimestamp.indexOf("Z")) + (offset ? offset : '+00:00'); - } else if (formattedTimestamp.indexOf("+") > 0) { //don't do anything - } else { //add offset - formattedTimestamp = formattedTimestamp + (offset ? offset : '+00:00'); - } - //parse into MS and return - return Date.parse(formattedTimestamp); - } - - //function to calculate offset from exif.exif.gpsTimeStamp or exif.gps.GPSDateStamp + exif.gps.GPSTimestamp - const getTimeOffsetByGPSStamp = (timestamp: string, gpsTimeStamp: string, gps: any) => { - let UTCTimestamp = gpsTimeStamp; - if (!UTCTimestamp && - gps && - gps.GPSDateStamp && - gps.GPSTimeStamp) { //else use exif.gps.GPS*Stamp if available - //GPS timestamp is always UTC (+00:00) - UTCTimestamp = gps.GPSDateStamp.replaceAll(':', '-') + gps.GPSTimeStamp.join(':'); - } - if (UTCTimestamp && timestamp) { - //offset in minutes is the difference between gps timestamp and given timestamp - //to calculate this correctly, we have to work with the same offset - const offsetMinutes = (timestampToMS(timestamp, '+00:00')- timestampToMS(UTCTimestamp, '+00:00')) / 1000 / 60; - return Utils.getOffsetString(offsetMinutes); - } else { - return undefined; - } - } - //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) { @@ -368,32 +365,32 @@ export class MetadataLoader { //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 || getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); + offset = exif.exif.OffsetTimeDigitized || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); } - metadata.creationDate = timestampToMS(exif.exif.DateTimeOriginal, offset); + 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 || getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); + offset = exif.exif.OffsetTimeOriginal || exif.exif.OffsetTime || Utils.getTimeOffsetByGPSStamp(exif.exif.DateTimeOriginal, exif.exif.GPSTimeStamp, exif.gps); } - metadata.creationDate = timestampToMS(exif.exif.CreateDate, offset); + 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 || getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); + offset = exif.exif.DateTimeOriginal || exif.exif.OffsetTimeDigitized || Utils.getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); } - metadata.creationDate = timestampToMS(exif.ifd0.ModifyDate, offset); + 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 || getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); - metadata.creationDate = timestampToMS(exif.ihdr["Creation Time"], any_offset); + 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 || getTimeOffsetByGPSStamp(exif.ifd0.ModifyDate, exif.exif.GPSTimeStamp, exif.gps); - metadata.creationDate = timestampToMS(exif.xmp.MetadataDate, any_offset); + 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 !== '') { @@ -623,25 +620,51 @@ export class MetadataLoader { const sidecarData = await exifr.sidecar(sidecarPath); if (sidecarData !== 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); + 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); + } } } } - if ((sidecarData as SideCar).xmp.Rating !== undefined) { - metadata.rating = (sidecarData as SideCar).xmp.Rating; + 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 ((sidecarData as SideCar).xmp.CreateDate) { - metadata.creationDate = timestampToMS((sidecarData as SideCar).xmp.CreateDate, null); + 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) { + 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; + } } } } diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 63e514cf..f707c883 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -124,6 +124,43 @@ export class Utils { return new Date(new Date(d).toISOString().substring(0,19) + (offset ? offset : '')).getFullYear(); } + //function to convert timestamp into milliseconds taking offset into account + static timestampToMS(timestamp: string, offset: string) { + if (!timestamp) { + return undefined; + } + //replace : with - in the yyyy-mm-dd part of the timestamp. + let formattedTimestamp = timestamp.substring(0,9).replaceAll(':', '-') + timestamp.substring(9,timestamp.length); + if (formattedTimestamp.indexOf("Z") > 0) { //replace Z (and what comes after the Z) with offset + formattedTimestamp.substring(0, formattedTimestamp.indexOf("Z")) + (offset ? offset : '+00:00'); + } else if (formattedTimestamp.indexOf("+") > 0) { //don't do anything + } else { //add offset + formattedTimestamp = formattedTimestamp + (offset ? offset : '+00:00'); + } + //parse into MS and return + return Date.parse(formattedTimestamp); + } + + //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; + if (!UTCTimestamp && + gps && + gps.GPSDateStamp && + gps.GPSTimeStamp) { //else use exif.gps.GPS*Stamp if available + //GPS timestamp is always UTC (+00:00) + UTCTimestamp = gps.GPSDateStamp.replaceAll(':', '-') + gps.GPSTimeStamp.join(':'); + } + if (UTCTimestamp && timestamp) { + //offset in minutes is the difference between gps timestamp and given timestamp + //to calculate this correctly, we have to work with the same offset + const offsetMinutes = (Utils.timestampToMS(timestamp, '+00:00')- Utils.timestampToMS(UTCTimestamp, '+00:00')) / 1000 / 60; + return Utils.getOffsetString(offsetMinutes); + } else { + return undefined; + } + } + static getOffsetString(offsetMinutes: number) { if (-720 <= offsetMinutes && offsetMinutes <= 840) { //valid offset is within -12 and +14 hrs (https://en.wikipedia.org/wiki/List_of_UTC_offsets) diff --git a/src/common/entities/MediaDTO.ts b/src/common/entities/MediaDTO.ts index c1d9f8e9..84db8157 100644 --- a/src/common/entities/MediaDTO.ts +++ b/src/common/entities/MediaDTO.ts @@ -32,6 +32,7 @@ export interface MediaDimension { export interface SideCar { dc?: SideCarDc; xmp?: SideCarXmp; + photoshop?: SideCarPhotoshop; } export interface SideCarDc { @@ -41,6 +42,13 @@ export interface SideCarDc { 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 = { diff --git a/test/backend/assets/sidecar/20240107_110258.jpg b/test/backend/assets/sidecar/20240107_110258.jpg new file mode 100644 index 00000000..924a4d7f Binary files /dev/null and b/test/backend/assets/sidecar/20240107_110258.jpg differ diff --git a/test/backend/assets/sidecar/20240107_110258.jpg.xmp b/test/backend/assets/sidecar/20240107_110258.jpg.xmp new file mode 100644 index 00000000..42665535 --- /dev/null +++ b/test/backend/assets/sidecar/20240107_110258.jpg.xmp @@ -0,0 +1,73 @@ + + + + + + + + 40 + + + + + Travel + + + + + Travel + + + + + + diff --git a/test/backend/assets/sidecar/20240107_110258.json b/test/backend/assets/sidecar/20240107_110258.json new file mode 100644 index 00000000..6052ac14 --- /dev/null +++ b/test/backend/assets/sidecar/20240107_110258.json @@ -0,0 +1,22 @@ +{ + "cameraData": { + "ISO": 40, + "exposure": 0.009524, + "fStop": 2.2, + "focalLength": 3.75, + "make": "samsung", + "model": "SM-A715F" + }, + "creationDate": 1704625379177, + "creationDateOffset": "+01:00", + "fileSize": 15126, + "size": { + "height": 13, + "width": 10 + }, + "keywords": [ + "Výlet", + "Travel" + ], + "rating": 3 +} diff --git a/test/backend/assets/sidecar/20240121_102400.JPG b/test/backend/assets/sidecar/20240121_102400.JPG new file mode 100755 index 00000000..121ba391 Binary files /dev/null and b/test/backend/assets/sidecar/20240121_102400.JPG differ diff --git a/test/backend/assets/sidecar/20240121_102400.JPG.xmp b/test/backend/assets/sidecar/20240121_102400.JPG.xmp new file mode 100644 index 00000000..67fe232d --- /dev/null +++ b/test/backend/assets/sidecar/20240121_102400.JPG.xmp @@ -0,0 +1,91 @@ + + + + + + + + 1 + 2 + 3 + 0 + + + + + 100 + + + + + Travel + + + + + + + + Travel + + + + + + diff --git a/test/backend/assets/sidecar/20240121_102400.json b/test/backend/assets/sidecar/20240121_102400.json new file mode 100644 index 00000000..b3579d2b --- /dev/null +++ b/test/backend/assets/sidecar/20240121_102400.json @@ -0,0 +1,23 @@ +{ + "cameraData": { + "ISO": 100, + "exposure": 0.003125, + "fStop": 6.3, + "focalLength": 16, + "lens": "NIKKOR Z DX 16-50mm f/3.5-6.3 VR", + "make": "NIKON CORPORATION", + "model": "NIKON Z 30" + }, + "creationDate": 1705832640930, + "creationDateOffset": "+01:00", + "fileSize": 25556, + "size": { + "height": 10, + "width": 15 + }, + "keywords": [ + "Výlet", + "Travel" + ], + "rating": 3 +} diff --git a/test/backend/assets/sidecar/20240128_105420.json b/test/backend/assets/sidecar/20240128_105420.json new file mode 100644 index 00000000..40587ac4 --- /dev/null +++ b/test/backend/assets/sidecar/20240128_105420.json @@ -0,0 +1,15 @@ +{ + "bitRate": 184871, + "creationDate": 1706435660000, + "duration": 1000, + "fileSize": 23132, + "size": { + "height": 46, + "width": 80 + }, + "fps": 60000, + "keywords": [ + "Travel" + ], + "rating": 3 +} diff --git a/test/backend/assets/sidecar/20240128_105420.mp4 b/test/backend/assets/sidecar/20240128_105420.mp4 new file mode 100644 index 00000000..1de9f155 Binary files /dev/null and b/test/backend/assets/sidecar/20240128_105420.mp4 differ diff --git a/test/backend/assets/sidecar/20240128_105420.mp4.xmp b/test/backend/assets/sidecar/20240128_105420.mp4.xmp new file mode 100644 index 00000000..47155971 --- /dev/null +++ b/test/backend/assets/sidecar/20240128_105420.mp4.xmp @@ -0,0 +1,115 @@ + + + + + + + 1 + 2 + 3 + 0 + + + + + Travel + + + + + Travel + + + + + + diff --git a/test/backend/assets/sidecar/20240128_120909.json b/test/backend/assets/sidecar/20240128_120909.json new file mode 100644 index 00000000..c8e2ddd0 --- /dev/null +++ b/test/backend/assets/sidecar/20240128_120909.json @@ -0,0 +1,15 @@ +{ + "bitRate": 183168, + "creationDate": 1706440145000, + "duration": 1000, + "fileSize": 22896, + "size": { + "height": 80, + "width": 46 + }, + "fps": 30, + "keywords": [ + "Travel" + ], + "rating": 3 +} diff --git a/test/backend/assets/sidecar/20240128_120909.mp4 b/test/backend/assets/sidecar/20240128_120909.mp4 new file mode 100644 index 00000000..ffa60ea3 Binary files /dev/null and b/test/backend/assets/sidecar/20240128_120909.mp4 differ diff --git a/test/backend/assets/sidecar/20240128_120909.mp4.xmp b/test/backend/assets/sidecar/20240128_120909.mp4.xmp new file mode 100644 index 00000000..662428f7 --- /dev/null +++ b/test/backend/assets/sidecar/20240128_120909.mp4.xmp @@ -0,0 +1,117 @@ + + + + + + + 1 + 2 + 3 + 0 + + + + + Travel + + + + + Travel + + + + + + diff --git a/test/backend/assets/sidecar/20240128_185808.JPG b/test/backend/assets/sidecar/20240128_185808.JPG new file mode 100644 index 00000000..f9e7ebf8 Binary files /dev/null and b/test/backend/assets/sidecar/20240128_185808.JPG differ diff --git a/test/backend/assets/sidecar/20240128_185808.JPG.xmp b/test/backend/assets/sidecar/20240128_185808.JPG.xmp new file mode 100644 index 00000000..8644809b --- /dev/null +++ b/test/backend/assets/sidecar/20240128_185808.JPG.xmp @@ -0,0 +1,114 @@ + + + + + + + + 1 + 2 + 3 + 0 + + + + + 25600 + + + + + Travel + + + + + Travel + + + + + Travel + + + + + Travel + + + + + + + + Travel + + + + + + diff --git a/test/backend/assets/sidecar/20240128_185808.json b/test/backend/assets/sidecar/20240128_185808.json new file mode 100644 index 00000000..7afeb9d9 --- /dev/null +++ b/test/backend/assets/sidecar/20240128_185808.json @@ -0,0 +1,23 @@ +{ + "cameraData": { + "ISO": 25600, + "exposure": 0.04, + "fStop": 4.2, + "focalLength": 25, + "lens": "NIKKOR Z DX 16-50mm f/3.5-6.3 VR", + "make": "NIKON CORPORATION", + "model": "NIKON Z 30" + }, + "creationDate": 1706468288660, + "creationDateOffset": "+01:00", + "fileSize": 47059, + "size": { + "height": 10, + "width": 15 + }, + "keywords": [ + "Výlet", + "Travel" + ], + "rating": 3 +} diff --git a/test/backend/assets/sidecar/bunny_1sec.mp4.json b/test/backend/assets/sidecar/bunny_1sec.json similarity index 85% rename from test/backend/assets/sidecar/bunny_1sec.mp4.json rename to test/backend/assets/sidecar/bunny_1sec.json index 29d2cb74..eec9a09a 100644 --- a/test/backend/assets/sidecar/bunny_1sec.mp4.json +++ b/test/backend/assets/sidecar/bunny_1sec.json @@ -5,7 +5,7 @@ }, "bitRate": 1794127, "duration": 290, - "creationDate": 1709052692000, + "creationDate": 1542482851000, "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 new file mode 100644 index 00000000..eec9a09a --- /dev/null +++ b/test/backend/assets/sidecar/bunny_1sec_v2.json @@ -0,0 +1,16 @@ +{ + "size": { + "width": 640, + "height": 360 + }, + "bitRate": 1794127, + "duration": 290, + "creationDate": 1542482851000, + "fileSize": 65073, + "fps": 40000, + "keywords": [ + "rabbit", + "test" + ], + "rating": 4 +} diff --git a/test/backend/assets/sidecar/bunny_1sec_v3.mp4.json b/test/backend/assets/sidecar/bunny_1sec_v3.json similarity index 84% rename from test/backend/assets/sidecar/bunny_1sec_v3.mp4.json rename to test/backend/assets/sidecar/bunny_1sec_v3.json index 8cd3db65..d3abbdba 100644 --- a/test/backend/assets/sidecar/bunny_1sec_v3.mp4.json +++ b/test/backend/assets/sidecar/bunny_1sec_v3.json @@ -5,7 +5,7 @@ }, "bitRate": 1794127, "duration": 290, - "creationDate": 1709052692000, + "creationDate": 1542482851000, "fileSize": 65073, "fps": 40000, "keywords": [ diff --git a/test/backend/assets/sidecar/metadata.jpg.json b/test/backend/assets/sidecar/metadata.json similarity index 100% rename from test/backend/assets/sidecar/metadata.jpg.json rename to test/backend/assets/sidecar/metadata.json diff --git a/test/backend/assets/sidecar/metadata_v2.jpg.json b/test/backend/assets/sidecar/metadata_v2.json similarity index 100% rename from test/backend/assets/sidecar/metadata_v2.jpg.json rename to test/backend/assets/sidecar/metadata_v2.json diff --git a/test/backend/assets/sidecar/no_metadata.jpg.json b/test/backend/assets/sidecar/no_metadata.json similarity index 100% rename from test/backend/assets/sidecar/no_metadata.jpg.json rename to test/backend/assets/sidecar/no_metadata.json diff --git a/test/backend/assets/sidecar/no_metadata_v2.jpg.json b/test/backend/assets/sidecar/no_metadata_v2.json similarity index 100% rename from test/backend/assets/sidecar/no_metadata_v2.jpg.json rename to test/backend/assets/sidecar/no_metadata_v2.json diff --git a/test/backend/assets/sidecar/no_metadata_v3.jpg.json b/test/backend/assets/sidecar/no_metadata_v3.json similarity index 100% rename from test/backend/assets/sidecar/no_metadata_v3.jpg.json rename to test/backend/assets/sidecar/no_metadata_v3.json diff --git a/test/backend/unit/model/threading/MetaDataLoader.spec.ts b/test/backend/unit/model/threading/MetaDataLoader.spec.ts index e0b0b8c6..9b62eb58 100644 --- a/test/backend/unit/model/threading/MetaDataLoader.spec.ts +++ b/test/backend/unit/model/threading/MetaDataLoader.spec.ts @@ -5,6 +5,7 @@ import {Utils} from '../../../../../src/common/Utils'; import * as path from 'path'; import * as fs from 'fs'; import {PhotoProcessing} from '../../../../../src/backend/model/fileaccess/fileprocessing/PhotoProcessing'; +import {VideoProcessing} from '../../../../../src/backend/model/fileaccess/fileprocessing/VideoProcessing'; import {Config} from '../../../../../src/common/config/private/Config'; import {DatabaseType} from '../../../../../src/common/config/private/PrivateConfig'; @@ -101,61 +102,61 @@ describe('MetadataLoader', () => { }); it('should load sidecar file with file extension for video', async () => { const data = await MetadataLoader.loadVideoMetadata(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec.mp4')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec.mp4.json')); + const expected = require(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); it('should load sidecar file without file extension for video', async () => { const data = await MetadataLoader.loadVideoMetadata(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec_v2.mp4')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec.mp4.json'));//sidecar "bunny_1sec_v2.xmp" is identical to "bunny_1sec.mp4.xmp" so we expect the same result + const expected = require(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec.json'));//sidecar "bunny_1sec_v2.xmp" is identical to "bunny_1sec.mp4.xmp" so we expect the same result expect(Utils.clone(data)).to.be.deep.equal(expected); }); it('should retrieve both keywords from sidecar file for video', async () => { const data = await MetadataLoader.loadVideoMetadata(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec.mp4')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec.mp4.json')); + const expected = require(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); it('should retrieve one keyword from sidecar file for video', async () => { const data = await MetadataLoader.loadVideoMetadata(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec_v3.mp4')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec_v3.mp4.json')); + const expected = require(path.join(__dirname, '/../../../assets/sidecar/bunny_1sec_v3.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); it('should load sidecar file with file extension for photo', async () => { const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/sidecar/no_metadata.jpg')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/no_metadata.jpg.json')); + const expected = require(path.join(__dirname, '/../../../assets/sidecar/no_metadata.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); it('should load sidecar file without file extension for photo', async () => { const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/sidecar/no_metadata_v2.jpg')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/no_metadata_v2.jpg.json')); + const expected = require(path.join(__dirname, '/../../../assets/sidecar/no_metadata_v2.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); it('should retrieve both keywords from sidecar file for photo', async () => { const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/sidecar/no_metadata.jpg')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/no_metadata.jpg.json')); + const expected = require(path.join(__dirname, '/../../../assets/sidecar/no_metadata.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); it('should retrieve one keyword from sidecar file for photo', async () => { const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/sidecar/no_metadata_v3.jpg')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/no_metadata_v3.jpg.json')); + const expected = require(path.join(__dirname, '/../../../assets/sidecar/no_metadata_v3.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); it('should read keywords from photo without sidecar file', async () => { const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/sidecar/metadata.jpg')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/metadata.jpg.json')); + const expected = require(path.join(__dirname, '/../../../assets/sidecar/metadata.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); it('should merge keywords from photo with keywords from sidecar', async () => { const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/sidecar/metadata_v2.jpg')); - const expected = require(path.join(__dirname, '/../../../assets/sidecar/metadata_v2.jpg.json')); //"metadata_v2.jpg" is identical to "metadata.jpg" and "metadata_v2.xmp" contains 2 different keywords + const expected = require(path.join(__dirname, '/../../../assets/sidecar/metadata_v2.json')); //"metadata_v2.jpg" is identical to "metadata.jpg" and "metadata_v2.xmp" contains 2 different keywords expect(Utils.clone(data)).to.be.deep.equal(expected); }); @@ -257,4 +258,24 @@ describe('MetadataLoader', () => { expect(Utils.clone(data)).to.be.deep.equal(expected); }); + describe('should load metadata from sidecar files', () => { + const root = path.join(__dirname, '/../../../assets/sidecar'); + const files = fs.readdirSync(root); + for (const item of files) { + const fullFilePath = path.join(root, item); + if (PhotoProcessing.isPhoto(fullFilePath)) { + it(item, async () => { + const data = await MetadataLoader.loadPhotoMetadata(fullFilePath); + const expected = require(fullFilePath.split('.').slice(0, -1).join('.') + '.json'); + expect(Utils.clone(data)).to.be.deep.equal(expected); + }); + } else if (VideoProcessing.isVideo(fullFilePath)) { + it(item, async () => { + const data = await MetadataLoader.loadVideoMetadata(fullFilePath); + const expected = require(fullFilePath.split('.').slice(0, -1).join('.') + '.json'); + expect(Utils.clone(data)).to.be.deep.equal(expected); + }); + } + } + }); });