diff --git a/src/backend/model/fileaccess/MetadataLoader.ts b/src/backend/model/fileaccess/MetadataLoader.ts index 02a0d949..d13a3796 100644 --- a/src/backend/model/fileaccess/MetadataLoader.ts +++ b/src/backend/model/fileaccess/MetadataLoader.ts @@ -127,7 +127,7 @@ export class MetadataLoader { try { // search for sidecar and merge metadata - const fullPathWithoutExt = path.parse(fullPath).name; + const fullPathWithoutExt = path.join(path.parse(fullPath).dir, path.parse(fullPath).name); const sidecarPaths = [ fullPath + '.xmp', fullPath + '.XMP', @@ -143,7 +143,11 @@ export class MetadataLoader { if (metadata.keywords === undefined) { metadata.keywords = []; } - for (const kw of (sidecarData as SideCar).dc.subject) { + 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); } @@ -194,7 +198,7 @@ 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 timestamp into milliseconds taking offset into account const timestampToMS = (timestamp: string, offset: string) => { if (!timestamp) { @@ -606,7 +610,7 @@ export class MetadataLoader { try { // search for sidecar and merge metadata - const fullPathWithoutExt = path.parse(fullPath).name; + const fullPathWithoutExt = path.join(path.parse(fullPath).dir, path.parse(fullPath).name); const sidecarPaths = [ fullPath + '.xmp', fullPath + '.XMP', @@ -623,7 +627,11 @@ export class MetadataLoader { if (metadata.keywords === undefined) { metadata.keywords = []; } - for (const kw of (sidecarData as SideCar).dc.subject) { + 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); } @@ -632,6 +640,9 @@ export class MetadataLoader { if ((sidecarData as SideCar).xmp.Rating !== undefined) { metadata.rating = (sidecarData as SideCar).xmp.Rating; } + if ((sidecarData as SideCar).xmp.CreateDate) { + metadata.creationDate = timestampToMS((sidecarData as SideCar).xmp.CreateDate, null); + } } } } diff --git a/src/common/entities/MediaDTO.ts b/src/common/entities/MediaDTO.ts index 7bcdcd99..c1d9f8e9 100644 --- a/src/common/entities/MediaDTO.ts +++ b/src/common/entities/MediaDTO.ts @@ -40,6 +40,7 @@ export interface SideCarDc { export interface SideCarXmp { Rating?: RatingTypes; + CreateDate?: string; } export const MediaDTOUtils = { diff --git a/test/backend/assets/sidecar/bunny_1sec.mp4 b/test/backend/assets/sidecar/bunny_1sec.mp4 new file mode 100644 index 00000000..18af94e6 Binary files /dev/null and b/test/backend/assets/sidecar/bunny_1sec.mp4 differ diff --git a/test/backend/assets/sidecar/bunny_1sec.mp4.json b/test/backend/assets/sidecar/bunny_1sec.mp4.json new file mode 100644 index 00000000..29d2cb74 --- /dev/null +++ b/test/backend/assets/sidecar/bunny_1sec.mp4.json @@ -0,0 +1,16 @@ +{ + "size": { + "width": 640, + "height": 360 + }, + "bitRate": 1794127, + "duration": 290, + "creationDate": 1709052692000, + "fileSize": 65073, + "fps": 40000, + "keywords": [ + "rabbit", + "test" + ], + "rating": 4 +} diff --git a/test/backend/assets/sidecar/bunny_1sec.mp4.xmp b/test/backend/assets/sidecar/bunny_1sec.mp4.xmp new file mode 100644 index 00000000..707ec9f3 --- /dev/null +++ b/test/backend/assets/sidecar/bunny_1sec.mp4.xmp @@ -0,0 +1,119 @@ + + + + + + 16 + + + + 0.0.0 + + + + H.264 + + + rabbit + test + + + + + + 480 + 852 + 72/1 + 72/1 + + + + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + 4 + + + + + 25Timecode + 00:00:00:00 + + Stereo + 48000 + 16Int + + 1/90000 + 2928000 + + 1 + 25 + + 25Timecode + 00:00:00:00 + + Progressive + 25.000000 + + 480 + pixel + 852 + + 1/1 + + + + + d47681ee-e57e-2256-f455-43c40000004b + 2a5a623f-09cc-ea32-5014-869400000078 + xmp.did:5af8e6dd-2af0-e94e-8f3c-767794b3efa1 + + b2c5763f-bfb9-7ee1-6893-05220000004b + + + + saved + / + 63c4c09c-9648-ee43-720e-644600000078 + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:27:31+01:00 + + + saved + / + 2a5a623f-09cc-ea32-5014-869400000078 + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:26:54+01:00 + + + saved + / + xmp.iid:6e43712c-17f5-cc4c-b90d-766dca2590dc + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:27:31+01:00 + + + saved + /metadata + xmp.iid:4d4376d3-c650-354b-b4eb-0fc2d21bcaa4 + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:27:31+01:00 + + + + xmp.iid:4d4376d3-c650-354b-b4eb-0fc2d21bcaa4 + xmp.did:1aa2d671-bf82-2043-9053-ec864079866c + + + + \ No newline at end of file diff --git a/test/backend/assets/sidecar/bunny_1sec_v2.mp4 b/test/backend/assets/sidecar/bunny_1sec_v2.mp4 new file mode 100644 index 00000000..18af94e6 Binary files /dev/null and b/test/backend/assets/sidecar/bunny_1sec_v2.mp4 differ diff --git a/test/backend/assets/sidecar/bunny_1sec_v2.xmp b/test/backend/assets/sidecar/bunny_1sec_v2.xmp new file mode 100644 index 00000000..707ec9f3 --- /dev/null +++ b/test/backend/assets/sidecar/bunny_1sec_v2.xmp @@ -0,0 +1,119 @@ + + + + + + 16 + + + + 0.0.0 + + + + H.264 + + + rabbit + test + + + + + + 480 + 852 + 72/1 + 72/1 + + + + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + 4 + + + + + 25Timecode + 00:00:00:00 + + Stereo + 48000 + 16Int + + 1/90000 + 2928000 + + 1 + 25 + + 25Timecode + 00:00:00:00 + + Progressive + 25.000000 + + 480 + pixel + 852 + + 1/1 + + + + + d47681ee-e57e-2256-f455-43c40000004b + 2a5a623f-09cc-ea32-5014-869400000078 + xmp.did:5af8e6dd-2af0-e94e-8f3c-767794b3efa1 + + b2c5763f-bfb9-7ee1-6893-05220000004b + + + + saved + / + 63c4c09c-9648-ee43-720e-644600000078 + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:27:31+01:00 + + + saved + / + 2a5a623f-09cc-ea32-5014-869400000078 + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:26:54+01:00 + + + saved + / + xmp.iid:6e43712c-17f5-cc4c-b90d-766dca2590dc + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:27:31+01:00 + + + saved + /metadata + xmp.iid:4d4376d3-c650-354b-b4eb-0fc2d21bcaa4 + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:27:31+01:00 + + + + xmp.iid:4d4376d3-c650-354b-b4eb-0fc2d21bcaa4 + xmp.did:1aa2d671-bf82-2043-9053-ec864079866c + + + + \ No newline at end of file diff --git a/test/backend/assets/sidecar/bunny_1sec_v3.mp4 b/test/backend/assets/sidecar/bunny_1sec_v3.mp4 new file mode 100644 index 00000000..18af94e6 Binary files /dev/null and b/test/backend/assets/sidecar/bunny_1sec_v3.mp4 differ diff --git a/test/backend/assets/sidecar/bunny_1sec_v3.mp4.json b/test/backend/assets/sidecar/bunny_1sec_v3.mp4.json new file mode 100644 index 00000000..8cd3db65 --- /dev/null +++ b/test/backend/assets/sidecar/bunny_1sec_v3.mp4.json @@ -0,0 +1,15 @@ +{ + "size": { + "width": 640, + "height": 360 + }, + "bitRate": 1794127, + "duration": 290, + "creationDate": 1709052692000, + "fileSize": 65073, + "fps": 40000, + "keywords": [ + "rabbit" + ], + "rating": 4 +} diff --git a/test/backend/assets/sidecar/bunny_1sec_v3.xmp b/test/backend/assets/sidecar/bunny_1sec_v3.xmp new file mode 100644 index 00000000..d56f5385 --- /dev/null +++ b/test/backend/assets/sidecar/bunny_1sec_v3.xmp @@ -0,0 +1,118 @@ + + + + + + 16 + + + + 0.0.0 + + + + H.264 + + + rabbit + + + + + + 480 + 852 + 72/1 + 72/1 + + + + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + 4 + + + + + 25Timecode + 00:00:00:00 + + Stereo + 48000 + 16Int + + 1/90000 + 2928000 + + 1 + 25 + + 25Timecode + 00:00:00:00 + + Progressive + 25.000000 + + 480 + pixel + 852 + + 1/1 + + + + + d47681ee-e57e-2256-f455-43c40000004b + 2a5a623f-09cc-ea32-5014-869400000078 + xmp.did:5af8e6dd-2af0-e94e-8f3c-767794b3efa1 + + b2c5763f-bfb9-7ee1-6893-05220000004b + + + + saved + / + 63c4c09c-9648-ee43-720e-644600000078 + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:27:31+01:00 + + + saved + / + 2a5a623f-09cc-ea32-5014-869400000078 + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:26:54+01:00 + + + saved + / + xmp.iid:6e43712c-17f5-cc4c-b90d-766dca2590dc + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:27:31+01:00 + + + saved + /metadata + xmp.iid:4d4376d3-c650-354b-b4eb-0fc2d21bcaa4 + Adobe Adobe Media Encoder CC 2017.1 (Windows) + 2018-11-17T20:27:31+01:00 + + + + xmp.iid:4d4376d3-c650-354b-b4eb-0fc2d21bcaa4 + xmp.did:1aa2d671-bf82-2043-9053-ec864079866c + + + + \ No newline at end of file diff --git a/test/backend/assets/sidecar/metadata.jpg b/test/backend/assets/sidecar/metadata.jpg new file mode 100644 index 00000000..6afec581 Binary files /dev/null and b/test/backend/assets/sidecar/metadata.jpg differ diff --git a/test/backend/assets/sidecar/metadata.jpg.json b/test/backend/assets/sidecar/metadata.jpg.json new file mode 100644 index 00000000..3593766b --- /dev/null +++ b/test/backend/assets/sidecar/metadata.jpg.json @@ -0,0 +1,12 @@ +{ + "size": { + "width": 10, + "height": 5 + }, + "creationDate": 1710188754000, + "fileSize": 5095, + "keywords": [ + "floor", + "book" + ] +} diff --git a/test/backend/assets/sidecar/metadata_v2.jpg b/test/backend/assets/sidecar/metadata_v2.jpg new file mode 100644 index 00000000..6afec581 Binary files /dev/null and b/test/backend/assets/sidecar/metadata_v2.jpg differ diff --git a/test/backend/assets/sidecar/metadata_v2.jpg.json b/test/backend/assets/sidecar/metadata_v2.jpg.json new file mode 100644 index 00000000..e66ce699 --- /dev/null +++ b/test/backend/assets/sidecar/metadata_v2.jpg.json @@ -0,0 +1,15 @@ +{ + "size": { + "width": 10, + "height": 5 + }, + "creationDate": 1710188754000, + "fileSize": 5095, + "keywords": [ + "floor", + "book", + "first", + "second" + ], + "rating": 0 +} diff --git a/test/backend/assets/sidecar/metadata_v2.xmp b/test/backend/assets/sidecar/metadata_v2.xmp new file mode 100644 index 00000000..532af7a5 --- /dev/null +++ b/test/backend/assets/sidecar/metadata_v2.xmp @@ -0,0 +1,18 @@ + + + + 0 + 0 + + + first + second + + + + + diff --git a/test/backend/assets/sidecar/no_metadata.jpg b/test/backend/assets/sidecar/no_metadata.jpg new file mode 100644 index 00000000..c119808d Binary files /dev/null and b/test/backend/assets/sidecar/no_metadata.jpg differ diff --git a/test/backend/assets/sidecar/no_metadata.jpg.json b/test/backend/assets/sidecar/no_metadata.jpg.json new file mode 100644 index 00000000..2ce48ad6 --- /dev/null +++ b/test/backend/assets/sidecar/no_metadata.jpg.json @@ -0,0 +1,12 @@ +{ + "size": { + "width": 10, + "height": 5 + }, + "creationDate": 1542482851000, + "fileSize": 1430, + "keywords": [ + "first", + "second" + ] +} diff --git a/test/backend/assets/sidecar/no_metadata.jpg.xmp b/test/backend/assets/sidecar/no_metadata.jpg.xmp new file mode 100644 index 00000000..68dc1368 --- /dev/null +++ b/test/backend/assets/sidecar/no_metadata.jpg.xmp @@ -0,0 +1,24 @@ + + + + + + + first + second + + + + + + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + + + + \ No newline at end of file diff --git a/test/backend/assets/sidecar/no_metadata_v2.jpg b/test/backend/assets/sidecar/no_metadata_v2.jpg new file mode 100644 index 00000000..c119808d Binary files /dev/null and b/test/backend/assets/sidecar/no_metadata_v2.jpg differ diff --git a/test/backend/assets/sidecar/no_metadata_v2.jpg.json b/test/backend/assets/sidecar/no_metadata_v2.jpg.json new file mode 100644 index 00000000..2ce48ad6 --- /dev/null +++ b/test/backend/assets/sidecar/no_metadata_v2.jpg.json @@ -0,0 +1,12 @@ +{ + "size": { + "width": 10, + "height": 5 + }, + "creationDate": 1542482851000, + "fileSize": 1430, + "keywords": [ + "first", + "second" + ] +} diff --git a/test/backend/assets/sidecar/no_metadata_v2.xmp b/test/backend/assets/sidecar/no_metadata_v2.xmp new file mode 100644 index 00000000..07376ab3 --- /dev/null +++ b/test/backend/assets/sidecar/no_metadata_v2.xmp @@ -0,0 +1,23 @@ + + + + + + + first + second + + + + + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + + + + diff --git a/test/backend/assets/sidecar/no_metadata_v3.jpg b/test/backend/assets/sidecar/no_metadata_v3.jpg new file mode 100644 index 00000000..c119808d Binary files /dev/null and b/test/backend/assets/sidecar/no_metadata_v3.jpg differ diff --git a/test/backend/assets/sidecar/no_metadata_v3.jpg.json b/test/backend/assets/sidecar/no_metadata_v3.jpg.json new file mode 100644 index 00000000..7f904f44 --- /dev/null +++ b/test/backend/assets/sidecar/no_metadata_v3.jpg.json @@ -0,0 +1,11 @@ +{ + "size": { + "width": 10, + "height": 5 + }, + "creationDate": 1542482851000, + "fileSize": 1430, + "keywords": [ + "first" + ] +} diff --git a/test/backend/assets/sidecar/no_metadata_v3.xmp b/test/backend/assets/sidecar/no_metadata_v3.xmp new file mode 100644 index 00000000..906cbf96 --- /dev/null +++ b/test/backend/assets/sidecar/no_metadata_v3.xmp @@ -0,0 +1,22 @@ + + + + + + + first + + + + + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + 2018-11-17T20:27:31+01:00 + + + + diff --git a/test/backend/unit/model/threading/MetaDataLoader.spec.ts b/test/backend/unit/model/threading/MetaDataLoader.spec.ts index 30542f2b..e0b0b8c6 100644 --- a/test/backend/unit/model/threading/MetaDataLoader.spec.ts +++ b/test/backend/unit/model/threading/MetaDataLoader.spec.ts @@ -99,6 +99,67 @@ describe('MetadataLoader', () => { const expected = require(path.join(__dirname, '/../../../assets/timestamps/big_ben_only_time.json')); expect(Utils.clone(data)).to.be.deep.equal(expected); }); + 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')); + 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 + 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')); + 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')); + 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')); + 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')); + 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')); + 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')); + 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')); + 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 + expect(Utils.clone(data)).to.be.deep.equal(expected); + }); + + describe('should load jpg with proper height and orientation', () => { it('jpg 1', async () => { const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/orientation/broken_orientation_exif.jpg'));