fix(linking collections): fixes name being propgated across all hubs when linking

adds tooltip to show which hubs will be linked/unlinked
This commit is contained in:
Tom Wheeler
2025-11-27 21:19:03 +13:00
parent de8ed928fe
commit b0320e218e
6 changed files with 388 additions and 20 deletions
@@ -18,6 +18,33 @@ export class DefaultHubConfigService {
const settings = getSettings();
const hubConfigs = settings.plex.hubConfigs || [];
// Check if any hubs are missing linkIds (legacy hubs from before automatic linking was implemented)
const hasHubsWithoutLinkIds = hubConfigs.some(
(hub) => hub.linkId === undefined && hubConfigs.length > 1
);
// If we have multiple hubs and some are missing linkIds, apply automatic linking and save
if (hasHubsWithoutLinkIds) {
logger.info(
'Detected hubs without linkIds - applying automatic linking to repair legacy hubs',
{
label: 'Default Hub Config Service',
totalHubs: hubConfigs.length,
}
);
const linkedConfigs = this.applyAutomaticLinking(hubConfigs);
settings.plex.hubConfigs = linkedConfigs;
settings.save();
logger.info('Automatic linking applied to legacy hubs', {
label: 'Default Hub Config Service',
linkedGroups: this.countLinkedGroups(linkedConfigs),
});
return linkedConfigs;
}
return hubConfigs;
}
@@ -28,6 +55,7 @@ export class DefaultHubConfigService {
const settings = getSettings();
// Preserve existing isActive status when updating hub configs
// Also repairs any broken names from the linking bug (names are refreshed from Plex discovery data)
const existingHubConfigs = settings.plex.hubConfigs || [];
const mergedHubConfigs = newConfigs.map(
(newConfig: DiscoveredHubConfig) => {
@@ -36,12 +64,30 @@ export class DefaultHubConfigService {
existing.hubIdentifier === newConfig.hubIdentifier &&
existing.libraryId === newConfig.libraryId
);
// Check if name is being corrected (for logging)
const nameChanged =
existingConfig && existingConfig.name !== newConfig.name;
if (nameChanged) {
logger.info(
`Correcting hub name from "${existingConfig.name}" to "${newConfig.name}"`,
{
label: 'Default Hub Config Service',
hubIdentifier: newConfig.hubIdentifier,
libraryId: newConfig.libraryId,
oldName: existingConfig.name,
newName: newConfig.name,
}
);
}
return {
...newConfig,
// Preserve existing ID or generate new one
id: existingConfig?.id || IdGenerator.generateId(),
// Preserve existing isActive status, or default to true for new configs
isActive: existingConfig?.isActive ?? true,
// Note: name comes from newConfig (discovery data), which fixes any broken names from the linking bug
};
}
);
@@ -144,8 +190,17 @@ export class DefaultHubConfigService {
// For linked collections, preserve library-specific fields
libraryId: configToUpdate.libraryId, // Don't change the library assignment
libraryName: configToUpdate.libraryName, // Don't change the library name
name: configToUpdate.name, // Don't change the hub display name (library-specific)
hubIdentifier: configToUpdate.hubIdentifier, // Don't change the hub identifier
mediaType: configToUpdate.mediaType, // Don't change the media type
sortOrderHome: configToUpdate.sortOrderHome, // Library-specific ordering
sortOrderLibrary: configToUpdate.sortOrderLibrary, // Library-specific ordering
isLibraryPromoted: configToUpdate.isLibraryPromoted, // Library-specific promotion status
everLibraryPromoted: configToUpdate.everLibraryPromoted, // Library-specific promotion history
isPromotedToHub: configToUpdate.isPromotedToHub, // Library-specific promotability
missing: configToUpdate.missing, // Library-specific existence status
lastSyncedAt: configToUpdate.lastSyncedAt, // Library-specific sync timestamp
needsSync: configToUpdate.needsSync, // Library-specific sync status
// Note: isLinked, linkId, isUnlinked come from settings spread above
};
@@ -177,6 +232,7 @@ export class DefaultHubConfigService {
const existingHubConfigs = settings.plex.hubConfigs || [];
// Add isActive field and default time restrictions server-side
// Also repairs any broken names from the linking bug (names are refreshed from Plex discovery data)
const hubConfigsWithActiveStatus = newConfigs.map(
(config: DiscoveredHubConfig) => {
// Try to find existing hub by natural key to preserve ID
@@ -186,6 +242,22 @@ export class DefaultHubConfigService {
existing.libraryId === config.libraryId
);
// Check if name is being corrected (for logging)
const nameChanged =
existingConfig && existingConfig.name !== config.name;
if (nameChanged) {
logger.info(
`Correcting hub name from "${existingConfig.name}" to "${config.name}"`,
{
label: 'Default Hub Config Service',
hubIdentifier: config.hubIdentifier,
libraryId: config.libraryId,
oldName: existingConfig.name,
newName: config.name,
}
);
}
return {
...config,
// Preserve existing ID or generate new one
@@ -194,6 +266,7 @@ export class DefaultHubConfigService {
timeRestriction: config.timeRestriction || {
alwaysActive: true, // Default to always active
},
// Note: name comes from config (discovery data), which fixes any broken names from the linking bug
};
}
);
@@ -264,11 +337,13 @@ export class DefaultHubConfigService {
);
// Link all hubs in this group
// BUT: respect isUnlinked flag - don't re-link deliberately unlinked hubs
hubs.forEach((hub: PlexHubConfig) => {
resultConfigs.push({
...hub,
isLinked: true,
isLinked: hub.isUnlinked ? false : true, // Don't re-link if deliberately unlinked
linkId,
// Keep isUnlinked as-is - it remains true if user deliberately unlinked
});
});
} else {
@@ -186,6 +186,8 @@ export class DiscoveryService {
await this.resetPreExistingPromotionStatus();
// STEP 3: Discover hubs and enhance pre-existing collections with hub data
// Use an object to pass repairedHubNamesCount by reference so it can be modified
const repairedHubNamesCounter = { count: 0 };
await this.discoverHubsAndEnhance(
plexClient,
libraries,
@@ -197,7 +199,9 @@ export class DiscoveryService {
existingCollectionKeys,
existingCollectionIds,
allCollections,
enhancedExistingConfigs
enhancedExistingConfigs,
existingHubConfigs,
repairedHubNamesCounter
);
// STEP 4: Promote collections that should be visible but aren't in hub management
@@ -286,21 +290,31 @@ export class DiscoveryService {
// STEP 4: Update settings with discovered configs if requested
if (updateSettings) {
// Add discovered hub configs to settings
// Add discovered hub configs to settings using DefaultHubConfigService
// This ensures automatic linking is applied properly
if (discoveredHubConfigs.length > 0) {
const existingHubConfigs = settings.plex.hubConfigs || [];
const newHubConfigs = [...existingHubConfigs];
const { defaultHubConfigService } = await import(
'@server/lib/collections/services/DefaultHubConfigService'
);
for (const discoveredHub of discoveredHubConfigs) {
// Add isActive: true to make it a complete PlexHubConfig
newHubConfigs.push({ ...discoveredHub, isActive: true });
}
// Use appendConfigs which applies automatic linking logic
// This also saves the repaired names from existing hubs
defaultHubConfigService.appendConfigs(discoveredHubConfigs);
settings.plex.hubConfigs = newHubConfigs;
logger.debug(
`Added ${discoveredHubConfigs.length} new hub configs to settings`,
logger.info(
`Added ${discoveredHubConfigs.length} new hub configs to settings with automatic linking`,
{
label: 'Discovery Service',
repairedNamesCount: repairedHubNamesCounter.count,
}
);
} else if (repairedHubNamesCounter.count > 0) {
// No new hubs, but we repaired existing hub names - save settings
settings.save();
logger.info(
`Repaired ${repairedHubNamesCounter.count} existing hub names (no new hubs discovered)`,
{
label: 'Discovery Service - Name Repair',
}
);
}
@@ -715,7 +729,9 @@ export class DiscoveryService {
existingCollectionKeys: Set<string>,
existingCollectionIds: Set<string>,
allCollections: PlexCollection[],
enhancedExistingConfigs: PreExistingCollectionConfig[]
enhancedExistingConfigs: PreExistingCollectionConfig[],
existingHubConfigs: PlexHubConfig[],
repairedHubNamesCounter: { count: number }
): Promise<void> {
// Counters for summary logging
let skippedAgregarrCollections = 0;
@@ -898,6 +914,28 @@ export class DiscoveryService {
if (!existingHubKeys.has(hubKey)) {
discoveredHubConfigs.push(hubConfig);
processedHubs++;
} else {
// Hub already exists - check if name needs repair (from linking bug)
const existingHub = existingHubConfigs.find(
(h: PlexHubConfig) =>
h.hubIdentifier === hubConfig.hubIdentifier &&
h.libraryId === hubConfig.libraryId
);
if (existingHub && existingHub.name !== hubConfig.name) {
logger.info(
`Repairing hub name from "${existingHub.name}" to "${hubConfig.name}"`,
{
label: 'Discovery Service - Name Repair',
hubIdentifier: hubConfig.hubIdentifier,
libraryId: hubConfig.libraryId,
oldName: existingHub.name,
newName: hubConfig.name,
}
);
// Update the existing hub's name directly
existingHub.name = hubConfig.name;
repairedHubNamesCounter.count++;
}
}
} else if (parsedHub.ratingKey) {
// This has a rating key - check if it's an Agregarr collection or pre-existing