Files
ReadMeABook/src/lib/utils/indexer-grouping.ts
T
kikootwo 5a0cce7985 Add multi-source ebook support and per-indexer categories
Introduces granular toggles for Anna's Archive and Indexer Search as ebook sources, updates settings UI to a three-section layout, and documents the new configuration. Adds per-indexer category configuration with separate tabs for audiobooks and ebooks, updates API routes and types for new settings, and ensures legacy config migration. Indexer grouping and file organization logic now support the new category structure and ebook source toggles.
2026-01-30 22:12:24 -05:00

139 lines
4.5 KiB
TypeScript

/**
* Utility: Indexer Grouping by Categories
* Documentation: documentation/phase3/prowlarr.md
*
* Groups indexers by their category configuration to minimize API calls.
* Indexers with identical categories are grouped together for a single search.
* Supports separate audiobook and ebook category configurations per indexer.
*/
export type CategoryType = 'audiobook' | 'ebook';
export interface IndexerConfig {
id: number;
name: string;
priority?: number;
audiobookCategories?: number[]; // Categories for audiobook searches
ebookCategories?: number[]; // Categories for ebook searches
categories?: number[]; // Legacy field for backwards compatibility
[key: string]: any; // Allow other properties
}
export interface IndexerGroup {
categories: number[];
indexerIds: number[];
indexers: IndexerConfig[];
}
/**
* Gets the appropriate categories from an indexer based on the category type.
*
* @param indexer - The indexer configuration
* @param type - The category type ('audiobook' or 'ebook')
* @returns Array of category IDs
*/
export function getCategoriesForType(indexer: IndexerConfig, type: CategoryType): number[] {
if (type === 'ebook') {
return indexer.ebookCategories && indexer.ebookCategories.length > 0
? indexer.ebookCategories
: [7020]; // Default ebook category
}
// Audiobook - check new field first, then legacy field
if (indexer.audiobookCategories && indexer.audiobookCategories.length > 0) {
return indexer.audiobookCategories;
}
if (indexer.categories && indexer.categories.length > 0) {
return indexer.categories; // Legacy fallback
}
return [3030]; // Default audiobook category
}
/**
* Groups indexers by their category configuration.
* Indexers with identical category arrays are grouped together.
*
* @param indexers - Array of indexer configurations
* @param type - The category type to group by ('audiobook' or 'ebook')
* @returns Array of groups, each containing indexers with matching categories
*
* @example
* const indexers = [
* { id: 1, audiobookCategories: [3030], ebookCategories: [7020] },
* { id: 2, audiobookCategories: [3030], ebookCategories: [7020] },
* { id: 3, audiobookCategories: [3030, 3010], ebookCategories: [7020] },
* ];
*
* const audiobookGroups = groupIndexersByCategories(indexers, 'audiobook');
* // Result:
* // [
* // { categories: [3030], indexerIds: [1, 2], indexers: [...] },
* // { categories: [3030, 3010], indexerIds: [3], indexers: [...] }
* // ]
*
* const ebookGroups = groupIndexersByCategories(indexers, 'ebook');
* // Result:
* // [
* // { categories: [7020], indexerIds: [1, 2, 3], indexers: [...] }
* // ]
*/
export function groupIndexersByCategories(
indexers: IndexerConfig[],
type: CategoryType = 'audiobook'
): IndexerGroup[] {
// Map to track unique category combinations
// Key: sorted category IDs as string (e.g., "3030,3010")
// Value: array of indexers with those categories
const groupMap = new Map<string, IndexerConfig[]>();
for (const indexer of indexers) {
// Get categories for the specified type
const categories = getCategoriesForType(indexer, type);
// Sort categories to ensure consistent grouping
// [3030, 3010] and [3010, 3030] should be the same group
const sortedCategories = [...categories].sort((a, b) => a - b);
const key = sortedCategories.join(',');
// Add indexer to group
if (!groupMap.has(key)) {
groupMap.set(key, []);
}
groupMap.get(key)!.push(indexer);
}
// Convert map to array of groups
const groups: IndexerGroup[] = [];
for (const [key, indexersInGroup] of groupMap.entries()) {
const categories = key.split(',').map(Number);
const indexerIds = indexersInGroup.map(idx => idx.id);
groups.push({
categories,
indexerIds,
indexers: indexersInGroup,
});
}
return groups;
}
/**
* Get a human-readable description of an indexer group.
* Useful for logging and debugging.
*
* @param group - The indexer group
* @returns Description string
*
* @example
* const description = getGroupDescription(group);
* // "3 indexers (IDs: 1, 2, 5) searching categories [3030, 3010]"
*/
export function getGroupDescription(group: IndexerGroup): string {
const indexerCount = group.indexerIds.length;
const indexerNames = group.indexers.map(idx => idx.name).join(', ');
const categoriesStr = group.categories.join(', ');
return `${indexerCount} indexer${indexerCount > 1 ? 's' : ''} (${indexerNames}) with categories [${categoriesStr}]`;
}