Skip to content

Commit 86b7f8b

Browse files
authored
Merge pull request #94 from jamcalli/develop
Patch to fix syncing issue with multiple instances and default routing
2 parents 33d83f9 + edb35dc commit 86b7f8b

File tree

1 file changed

+206
-13
lines changed

1 file changed

+206
-13
lines changed

src/services/content-router.service.ts

+206-13
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type {
1010
} from '@root/types/router.types.js'
1111
import type { SonarrItem } from '@root/types/sonarr.types.js'
1212
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'
1315

1416
export class ContentRouterService {
1517
private plugins: RouterPlugin[] = []
@@ -206,7 +208,16 @@ export class ContentRouterService {
206208
this.log.warn(
207209
`No routing decisions returned from any plugin for "${item.title}", using default routing`,
208210
)
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)
210221
}
211222

212223
return { routedInstances }
@@ -291,26 +302,208 @@ export class ContentRouterService {
291302
return { routedInstances }
292303
}
293304

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+
*/
294321
private async routeUsingDefault(
295322
item: ContentItem,
296323
key: string,
297324
contentType: 'movie' | 'show',
298325
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,
299381
): 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`,
306391
)
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,
313417
)
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+
}
314507
}
315508
}
316509

0 commit comments

Comments
 (0)