@@ -10,6 +10,8 @@ import type {
10
10
} from '@root/types/router.types.js'
11
11
import type { SonarrItem } from '@root/types/sonarr.types.js'
12
12
import type { Item as RadarrItem } from '@root/types/radarr.types.js'
13
+ import type { RadarrInstance } from '@root/types/radarr.types.js'
14
+ import type { SonarrInstance } from '@root/types/sonarr.types.js'
13
15
14
16
export class ContentRouterService {
15
17
private plugins : RouterPlugin [ ] = [ ]
@@ -206,7 +208,16 @@ export class ContentRouterService {
206
208
this . log . warn (
207
209
`No routing decisions returned from any plugin for "${ item . title } ", using default routing` ,
208
210
)
209
- await this . routeUsingDefault ( item , key , contentType , options . syncing )
211
+ const defaultRoutedInstances = await this . routeUsingDefault (
212
+ item ,
213
+ key ,
214
+ contentType ,
215
+ options . syncing ,
216
+ )
217
+ this . log . debug (
218
+ `Default routing returned ${ defaultRoutedInstances . length } instances: [${ defaultRoutedInstances . join ( ', ' ) } ]` ,
219
+ )
220
+ routedInstances . push ( ...defaultRoutedInstances )
210
221
}
211
222
212
223
return { routedInstances }
@@ -291,26 +302,208 @@ export class ContentRouterService {
291
302
return { routedInstances }
292
303
}
293
304
305
+ /**
306
+ * Routes an item using the default instance and handles syncing to other instances
307
+ *
308
+ * This method is called when no specific routing rules matched the item.
309
+ * It routes the item to the default instance, and if the default instance
310
+ * is configured to sync with other instances, it routes the item to those instances as well.
311
+ *
312
+ * @param item - Content item to route
313
+ * @param key - Unique key of the watchlist item
314
+ * @param contentType - Type of content ('movie' or 'show')
315
+ * @param syncing - Whether this routing is part of a sync operation
316
+ * @returns Promise resolving to an array of instance IDs the item was routed to.
317
+ * The array will contain the default instance ID first (if successful),
318
+ * followed by any synced instance IDs that were successfully routed to.
319
+ * Returns an empty array if routing fails.
320
+ */
294
321
private async routeUsingDefault (
295
322
item : ContentItem ,
296
323
key : string ,
297
324
contentType : 'movie' | 'show' ,
298
325
syncing ?: boolean ,
326
+ ) : Promise < number [ ] > {
327
+ try {
328
+ const routedInstances : number [ ] = [ ]
329
+
330
+ if ( contentType === 'movie' ) {
331
+ // Handle routing for Radarr
332
+ await this . routeUsingDefaultHelper < RadarrInstance , RadarrItem > (
333
+ item as RadarrItem ,
334
+ key ,
335
+ 'radarr' ,
336
+ routedInstances ,
337
+ syncing ,
338
+ )
339
+ } else {
340
+ // Handle routing for Sonarr
341
+ await this . routeUsingDefaultHelper < SonarrInstance , SonarrItem > (
342
+ item as SonarrItem ,
343
+ key ,
344
+ 'sonarr' ,
345
+ routedInstances ,
346
+ syncing ,
347
+ )
348
+ }
349
+
350
+ return routedInstances
351
+ } catch ( error ) {
352
+ this . log . error ( `Error in routeUsingDefault for ${ item . title } : ${ error } ` , {
353
+ item_title : item . title ,
354
+ content_type : contentType ,
355
+ error_message : error instanceof Error ? error . message : String ( error ) ,
356
+ error_stack : error instanceof Error ? error . stack : undefined ,
357
+ } )
358
+ // Return empty array to avoid cascading failures
359
+ return [ ]
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Helper method that implements the common logic for routing to default and synced instances
365
+ *
366
+ * @param item - The content item to route (either RadarrItem or SonarrItem)
367
+ * @param key - Unique key of the watchlist item
368
+ * @param instanceType - Type of instance ('radarr' or 'sonarr')
369
+ * @param routedInstances - Array to collect successfully routed instance IDs
370
+ * @param syncing - Whether this routing is part of a sync operation
371
+ */
372
+ private async routeUsingDefaultHelper <
373
+ T extends RadarrInstance | SonarrInstance ,
374
+ I extends RadarrItem | SonarrItem ,
375
+ > (
376
+ item : I ,
377
+ key : string ,
378
+ instanceType : 'radarr' | 'sonarr' ,
379
+ routedInstances : number [ ] ,
380
+ syncing ?: boolean ,
299
381
) : Promise < void > {
300
- if ( contentType === 'movie' ) {
301
- await this . fastify . radarrManager . routeItemToRadarr (
302
- item as RadarrItem ,
303
- key ,
304
- undefined ,
305
- syncing ,
382
+ // Get default instance based on type
383
+ const defaultInstance =
384
+ instanceType === 'radarr'
385
+ ? await this . fastify . db . getDefaultRadarrInstance ( )
386
+ : await this . fastify . db . getDefaultSonarrInstance ( )
387
+
388
+ if ( ! defaultInstance ) {
389
+ this . log . warn (
390
+ `No default ${ instanceType . charAt ( 0 ) . toUpperCase ( ) + instanceType . slice ( 1 ) } instance found for routing` ,
306
391
)
307
- } else {
308
- await this . fastify . sonarrManager . routeItemToSonarr (
309
- item as SonarrItem ,
310
- key ,
311
- undefined ,
312
- syncing ,
392
+ return
393
+ }
394
+
395
+ // Route to default instance
396
+ try {
397
+ if ( instanceType === 'radarr' ) {
398
+ await this . fastify . radarrManager . routeItemToRadarr (
399
+ item as RadarrItem ,
400
+ key ,
401
+ defaultInstance . id ,
402
+ syncing ,
403
+ )
404
+ } else {
405
+ await this . fastify . sonarrManager . routeItemToSonarr (
406
+ item as SonarrItem ,
407
+ key ,
408
+ defaultInstance . id ,
409
+ syncing ,
410
+ )
411
+ }
412
+ routedInstances . push ( defaultInstance . id )
413
+ } catch ( error ) {
414
+ this . log . error (
415
+ `Error routing "${ item . title } " to default ${ instanceType } instance ${ defaultInstance . id } :` ,
416
+ error ,
313
417
)
418
+ // Continue processing synced instances even if default instance fails
419
+ }
420
+
421
+ // Check for synced instances
422
+ const syncedInstanceIds = Array . isArray ( defaultInstance . syncedInstances )
423
+ ? defaultInstance . syncedInstances
424
+ : typeof defaultInstance . syncedInstances === 'string'
425
+ ? JSON . parse ( defaultInstance . syncedInstances || '[]' )
426
+ : [ ]
427
+
428
+ if ( syncedInstanceIds . length === 0 ) {
429
+ return
430
+ }
431
+
432
+ const capitalizedType =
433
+ instanceType . charAt ( 0 ) . toUpperCase ( ) + instanceType . slice ( 1 )
434
+ this . log . info (
435
+ `Default ${ capitalizedType } instance ${ defaultInstance . id } is configured to sync with ${ syncedInstanceIds . length } other instance(s), adding item to synced instances` ,
436
+ )
437
+
438
+ // Get all instances based on type
439
+ const allInstances =
440
+ instanceType === 'radarr'
441
+ ? await this . fastify . db . getAllRadarrInstances ( )
442
+ : await this . fastify . db . getAllSonarrInstances ( )
443
+
444
+ // Map instance IDs to instances for easy lookup
445
+ const instanceMap = new Map (
446
+ allInstances . map ( ( instance ) => [ instance . id , instance ] ) ,
447
+ )
448
+
449
+ // Process each synced instance
450
+ for ( const syncedId of syncedInstanceIds ) {
451
+ if ( routedInstances . includes ( syncedId ) ) {
452
+ this . log . debug (
453
+ `Skipping already routed ${ capitalizedType } instance ${ syncedId } ` ,
454
+ )
455
+ continue
456
+ }
457
+
458
+ const syncedInstance = instanceMap . get ( syncedId ) as T | undefined
459
+ if ( ! syncedInstance ) {
460
+ this . log . warn (
461
+ `Synced ${ capitalizedType } instance ${ syncedId } not found` ,
462
+ )
463
+ continue
464
+ }
465
+
466
+ try {
467
+ // Convert rootFolder from string|null|undefined to string|undefined
468
+ const rootFolder =
469
+ syncedInstance . rootFolder === null
470
+ ? undefined
471
+ : syncedInstance . rootFolder
472
+
473
+ // Use the quality profile from the synced instance
474
+ const qualityProfile = syncedInstance . qualityProfile
475
+
476
+ this . log . info (
477
+ `Routing item "${ item . title } " to synced ${ capitalizedType } instance ${ syncedId } ` ,
478
+ )
479
+
480
+ if ( instanceType === 'radarr' ) {
481
+ await this . fastify . radarrManager . routeItemToRadarr (
482
+ item as RadarrItem ,
483
+ key ,
484
+ syncedId ,
485
+ syncing ,
486
+ rootFolder ,
487
+ qualityProfile ,
488
+ )
489
+ } else {
490
+ await this . fastify . sonarrManager . routeItemToSonarr (
491
+ item as SonarrItem ,
492
+ key ,
493
+ syncedId ,
494
+ syncing ,
495
+ rootFolder ,
496
+ qualityProfile ,
497
+ )
498
+ }
499
+ routedInstances . push ( syncedId )
500
+ } catch ( syncError ) {
501
+ this . log . error (
502
+ `Error routing to synced ${ capitalizedType } instance ${ syncedId } :` ,
503
+ syncError ,
504
+ )
505
+ // Continue processing other synced instances even if one fails
506
+ }
314
507
}
315
508
}
316
509
0 commit comments