mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 21:00:09 +00:00
5a0cce7985
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.
139 lines
4.5 KiB
TypeScript
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}]`;
|
|
}
|