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);
+ });
+ }
+ }
+ });
});