1
1
import { Stats } from 'node:fs' ;
2
+ import { SystemConfig } from 'src/config' ;
3
+ import { AssetEntity } from 'src/entities/asset.entity' ;
2
4
import { ExifEntity } from 'src/entities/exif.entity' ;
3
5
import {
4
6
AssetFileType ,
7
+ AssetPathType ,
5
8
AssetType ,
6
9
AudioCodec ,
7
10
Colorspace ,
@@ -12,9 +15,10 @@ import {
12
15
VideoCodec ,
13
16
} from 'src/enum' ;
14
17
import { IAssetRepository , WithoutProperty } from 'src/interfaces/asset.interface' ;
15
- import { IJobRepository , JobName , JobStatus } from 'src/interfaces/job.interface' ;
18
+ import { IJobRepository , JobCounts , JobName , JobStatus } from 'src/interfaces/job.interface' ;
16
19
import { ILoggerRepository } from 'src/interfaces/logger.interface' ;
17
20
import { IMediaRepository , RawImageInfo } from 'src/interfaces/media.interface' ;
21
+ import { IMoveRepository } from 'src/interfaces/move.interface' ;
18
22
import { IPersonRepository } from 'src/interfaces/person.interface' ;
19
23
import { IStorageRepository } from 'src/interfaces/storage.interface' ;
20
24
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface' ;
@@ -33,12 +37,13 @@ describe(MediaService.name, () => {
33
37
let jobMock : Mocked < IJobRepository > ;
34
38
let loggerMock : Mocked < ILoggerRepository > ;
35
39
let mediaMock : Mocked < IMediaRepository > ;
40
+ let moveMock : Mocked < IMoveRepository > ;
36
41
let personMock : Mocked < IPersonRepository > ;
37
42
let storageMock : Mocked < IStorageRepository > ;
38
43
let systemMock : Mocked < ISystemMetadataRepository > ;
39
44
40
45
beforeEach ( ( ) => {
41
- ( { sut, assetMock, jobMock, loggerMock, mediaMock, personMock, storageMock, systemMock } =
46
+ ( { sut, assetMock, jobMock, loggerMock, mediaMock, moveMock , personMock, storageMock, systemMock } =
42
47
newTestService ( MediaService ) ) ;
43
48
} ) ;
44
49
@@ -134,10 +139,10 @@ describe(MediaService.name, () => {
134
139
hasNextPage : false ,
135
140
} ) ;
136
141
personMock . getAll . mockResolvedValue ( {
137
- items : [ personStub . noThumbnail ] ,
142
+ items : [ personStub . noThumbnail , personStub . noThumbnail ] ,
138
143
hasNextPage : false ,
139
144
} ) ;
140
- personMock . getRandomFace . mockResolvedValue ( faceStub . face1 ) ;
145
+ personMock . getRandomFace . mockResolvedValueOnce ( faceStub . face1 ) ;
141
146
142
147
await sut . handleQueueGenerateThumbnails ( { force : false } ) ;
143
148
@@ -146,6 +151,7 @@ describe(MediaService.name, () => {
146
151
147
152
expect ( personMock . getAll ) . toHaveBeenCalledWith ( { skip : 0 , take : 1000 } , { where : { thumbnailPath : '' } } ) ;
148
153
expect ( personMock . getRandomFace ) . toHaveBeenCalled ( ) ;
154
+ expect ( personMock . update ) . toHaveBeenCalledTimes ( 1 ) ;
149
155
expect ( jobMock . queueAll ) . toHaveBeenCalledWith ( [
150
156
{
151
157
name : JobName . GENERATE_PERSON_THUMBNAIL ,
@@ -229,6 +235,46 @@ describe(MediaService.name, () => {
229
235
} ) ;
230
236
} ) ;
231
237
238
+ describe ( 'handleQueueMigration' , ( ) => {
239
+ it ( 'should remove empty directories and queue jobs' , async ( ) => {
240
+ assetMock . getAll . mockResolvedValue ( { hasNextPage : false , items : [ assetStub . image ] } ) ;
241
+ jobMock . getJobCounts . mockResolvedValue ( { active : 1 , waiting : 0 } as JobCounts ) ;
242
+ personMock . getAll . mockResolvedValue ( { hasNextPage : false , items : [ personStub . withName ] } ) ;
243
+
244
+ await expect ( sut . handleQueueMigration ( ) ) . resolves . toBe ( JobStatus . SUCCESS ) ;
245
+
246
+ expect ( storageMock . removeEmptyDirs ) . toHaveBeenCalledTimes ( 2 ) ;
247
+ expect ( jobMock . queueAll ) . toHaveBeenCalledWith ( [
248
+ { name : JobName . MIGRATE_ASSET , data : { id : assetStub . image . id } } ,
249
+ ] ) ;
250
+ expect ( jobMock . queueAll ) . toHaveBeenCalledWith ( [
251
+ { name : JobName . MIGRATE_PERSON , data : { id : personStub . withName . id } } ,
252
+ ] ) ;
253
+ } ) ;
254
+ } ) ;
255
+
256
+ describe ( 'handleAssetMigration' , ( ) => {
257
+ it ( 'should fail if asset does not exist' , async ( ) => {
258
+ await expect ( sut . handleAssetMigration ( { id : assetStub . image . id } ) ) . resolves . toBe ( JobStatus . FAILED ) ;
259
+
260
+ expect ( moveMock . getByEntity ) . not . toHaveBeenCalled ( ) ;
261
+ } ) ;
262
+
263
+ it ( 'should move asset files' , async ( ) => {
264
+ assetMock . getByIds . mockResolvedValue ( [ assetStub . image ] ) ;
265
+ moveMock . create . mockResolvedValue ( {
266
+ entityId : assetStub . image . id ,
267
+ id : 'move-id' ,
268
+ newPath : '/new/path' ,
269
+ oldPath : '/old/path' ,
270
+ pathType : AssetPathType . ORIGINAL ,
271
+ } ) ;
272
+
273
+ await expect ( sut . handleAssetMigration ( { id : assetStub . image . id } ) ) . resolves . toBe ( JobStatus . SUCCESS ) ;
274
+ expect ( moveMock . create ) . toHaveBeenCalledTimes ( 2 ) ;
275
+ } ) ;
276
+ } ) ;
277
+
232
278
describe ( 'handleGenerateThumbnails' , ( ) => {
233
279
let rawBuffer : Buffer ;
234
280
let rawInfo : RawImageInfo ;
@@ -246,10 +292,19 @@ describe(MediaService.name, () => {
246
292
expect ( assetMock . update ) . not . toHaveBeenCalledWith ( ) ;
247
293
} ) ;
248
294
295
+ it ( 'should skip thumbnail generation if asset type is unknown' , async ( ) => {
296
+ assetMock . getById . mockResolvedValue ( { ...assetStub . image , type : 'foo' } as never as AssetEntity ) ;
297
+
298
+ await expect ( sut . handleGenerateThumbnails ( { id : assetStub . image . id } ) ) . resolves . toBe ( JobStatus . SKIPPED ) ;
299
+ expect ( mediaMock . probe ) . not . toHaveBeenCalled ( ) ;
300
+ expect ( mediaMock . generateThumbnail ) . not . toHaveBeenCalled ( ) ;
301
+ expect ( assetMock . update ) . not . toHaveBeenCalledWith ( ) ;
302
+ } ) ;
303
+
249
304
it ( 'should skip video thumbnail generation if no video stream' , async ( ) => {
250
305
mediaMock . probe . mockResolvedValue ( probeStub . noVideoStreams ) ;
251
- assetMock . getByIds . mockResolvedValue ( [ assetStub . video ] ) ;
252
- await sut . handleGenerateThumbnails ( { id : assetStub . image . id } ) ;
306
+ assetMock . getById . mockResolvedValue ( assetStub . video ) ;
307
+ await expect ( sut . handleGenerateThumbnails ( { id : assetStub . video . id } ) ) . rejects . toBeInstanceOf ( Error ) ;
253
308
expect ( mediaMock . generateThumbnail ) . not . toHaveBeenCalled ( ) ;
254
309
expect ( assetMock . update ) . not . toHaveBeenCalledWith ( ) ;
255
310
} ) ;
@@ -751,6 +806,27 @@ describe(MediaService.name, () => {
751
806
expect ( mediaMock . transcode ) . not . toHaveBeenCalled ( ) ;
752
807
} ) ;
753
808
809
+ it ( 'should throw an error if an unknown transcode policy is configured' , async ( ) => {
810
+ mediaMock . probe . mockResolvedValue ( probeStub . noAudioStreams ) ;
811
+ systemMock . get . mockResolvedValue ( { ffmpeg : { transcode : 'foo' } } as never as SystemConfig ) ;
812
+ assetMock . getByIds . mockResolvedValue ( [ assetStub . video ] ) ;
813
+
814
+ await expect ( sut . handleVideoConversion ( { id : assetStub . video . id } ) ) . rejects . toBeDefined ( ) ;
815
+ expect ( mediaMock . transcode ) . not . toHaveBeenCalled ( ) ;
816
+ } ) ;
817
+
818
+ it ( 'should throw an error if transcoding fails and hw acceleration is disabled' , async ( ) => {
819
+ mediaMock . probe . mockResolvedValue ( probeStub . multipleVideoStreams ) ;
820
+ systemMock . get . mockResolvedValue ( {
821
+ ffmpeg : { transcode : TranscodePolicy . ALL , accel : TranscodeHWAccel . DISABLED } ,
822
+ } ) ;
823
+ assetMock . getByIds . mockResolvedValue ( [ assetStub . video ] ) ;
824
+ mediaMock . transcode . mockRejectedValue ( new Error ( 'Error transcoding video' ) ) ;
825
+
826
+ await expect ( sut . handleVideoConversion ( { id : assetStub . video . id } ) ) . resolves . toBe ( JobStatus . FAILED ) ;
827
+ expect ( mediaMock . transcode ) . toHaveBeenCalledTimes ( 1 ) ;
828
+ } ) ;
829
+
754
830
it ( 'should transcode when set to all' , async ( ) => {
755
831
mediaMock . probe . mockResolvedValue ( probeStub . multipleVideoStreams ) ;
756
832
systemMock . get . mockResolvedValue ( { ffmpeg : { transcode : TranscodePolicy . ALL } } ) ;
@@ -782,7 +858,7 @@ describe(MediaService.name, () => {
782
858
) ;
783
859
} ) ;
784
860
785
- it ( 'should transcode when policy Bitrate and bitrate higher than max bitrate' , async ( ) => {
861
+ it ( 'should transcode when policy bitrate and bitrate higher than max bitrate' , async ( ) => {
786
862
mediaMock . probe . mockResolvedValue ( probeStub . videoStream40Mbps ) ;
787
863
systemMock . get . mockResolvedValue ( { ffmpeg : { transcode : TranscodePolicy . BITRATE , maxBitrate : '30M' } } ) ;
788
864
await sut . handleVideoConversion ( { id : assetStub . video . id } ) ;
@@ -797,6 +873,21 @@ describe(MediaService.name, () => {
797
873
) ;
798
874
} ) ;
799
875
876
+ it ( 'should transcode when max bitrate is not a number' , async ( ) => {
877
+ mediaMock . probe . mockResolvedValue ( probeStub . videoStream40Mbps ) ;
878
+ systemMock . get . mockResolvedValue ( { ffmpeg : { transcode : TranscodePolicy . BITRATE , maxBitrate : 'foo' } } ) ;
879
+ await sut . handleVideoConversion ( { id : assetStub . video . id } ) ;
880
+ expect ( mediaMock . transcode ) . toHaveBeenCalledWith (
881
+ '/original/path.ext' ,
882
+ 'upload/encoded-video/user-id/as/se/asset-id.mp4' ,
883
+ expect . objectContaining ( {
884
+ inputOptions : expect . any ( Array ) ,
885
+ outputOptions : expect . any ( Array ) ,
886
+ twoPass : false ,
887
+ } ) ,
888
+ ) ;
889
+ } ) ;
890
+
800
891
it ( 'should not scale resolution if no target resolution' , async ( ) => {
801
892
mediaMock . probe . mockResolvedValue ( probeStub . videoStream2160p ) ;
802
893
systemMock . get . mockResolvedValue ( { ffmpeg : { transcode : TranscodePolicy . ALL , targetResolution : 'original' } } ) ;
@@ -1600,12 +1691,13 @@ describe(MediaService.name, () => {
1600
1691
} ) ;
1601
1692
1602
1693
it ( 'should fail for qsv if no hw devices' , async ( ) => {
1603
- storageMock . readdir . mockResolvedValue ( [ ] ) ;
1694
+ storageMock . readdir . mockRejectedValue ( new Error ( 'Could not read directory' ) ) ;
1604
1695
mediaMock . probe . mockResolvedValue ( probeStub . matroskaContainer ) ;
1605
1696
systemMock . get . mockResolvedValue ( { ffmpeg : { accel : TranscodeHWAccel . QSV } } ) ;
1606
1697
assetMock . getByIds . mockResolvedValue ( [ assetStub . video ] ) ;
1607
1698
await expect ( sut . handleVideoConversion ( { id : assetStub . video . id } ) ) . resolves . toBe ( JobStatus . FAILED ) ;
1608
1699
expect ( mediaMock . transcode ) . not . toHaveBeenCalled ( ) ;
1700
+ expect ( loggerMock . debug ) . toHaveBeenCalledWith ( 'No devices found in /dev/dri.' ) ;
1609
1701
} ) ;
1610
1702
1611
1703
it ( 'should use hardware decoding for qsv if enabled' , async ( ) => {
0 commit comments