mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Implement file hash-based library matching and remove fuzzy ASIN matching
Adds file hash-based matching for Audiobookshelf library items to ensure 100% accurate ASIN assignment for RMAB-organized content. Removes fuzzy matching from library availability checks, making all matching ASIN-only to eliminate false positives and race conditions. Updates database schema, processors, and matcher utilities; adds new tests and documentation for the new matching strategy. Removes obsolete scripts, Dockerfile, and related tests; updates docker-compose for test environments.
This commit is contained in:
@@ -14,8 +14,10 @@ const logger = RMABLogger.create('API.Admin.Settings.ProwlarrIndexers');
|
||||
interface SavedIndexerConfig {
|
||||
id: number;
|
||||
name: string;
|
||||
protocol: string;
|
||||
priority: number;
|
||||
seedingTimeMinutes: number;
|
||||
seedingTimeMinutes?: number; // Torrents only
|
||||
removeAfterProcessing?: boolean; // Usenet only
|
||||
rssEnabled?: boolean;
|
||||
categories?: number[]; // Array of category IDs (default: [3030] for audiobooks)
|
||||
}
|
||||
@@ -50,8 +52,9 @@ export async function GET(request: NextRequest) {
|
||||
const indexersWithConfig = indexers.map((indexer: any) => {
|
||||
const saved = savedIndexersMap.get(indexer.id);
|
||||
const isAdded = !!saved;
|
||||
const isTorrent = indexer.protocol?.toLowerCase() === 'torrent';
|
||||
|
||||
return {
|
||||
const config: any = {
|
||||
id: indexer.id,
|
||||
name: indexer.name,
|
||||
protocol: indexer.protocol,
|
||||
@@ -59,11 +62,19 @@ export async function GET(request: NextRequest) {
|
||||
enabled: isAdded, // Enabled if in saved list
|
||||
isAdded, // Explicit flag for UI (new card-based interface)
|
||||
priority: saved?.priority || 10,
|
||||
seedingTimeMinutes: saved?.seedingTimeMinutes ?? 0,
|
||||
rssEnabled: saved?.rssEnabled ?? false,
|
||||
categories: saved?.categories || [3030], // Default to audiobooks category
|
||||
supportsRss: indexer.capabilities?.supportsRss !== false, // Default to true if not specified
|
||||
};
|
||||
|
||||
// Add protocol-specific fields
|
||||
if (isTorrent) {
|
||||
config.seedingTimeMinutes = saved?.seedingTimeMinutes ?? 0;
|
||||
} else {
|
||||
config.removeAfterProcessing = saved?.removeAfterProcessing ?? true;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -99,14 +110,26 @@ export async function PUT(request: NextRequest) {
|
||||
// Filter to only enabled indexers and convert to wizard format
|
||||
const enabledIndexers = indexers
|
||||
.filter((indexer: any) => indexer.enabled)
|
||||
.map((indexer: any) => ({
|
||||
id: indexer.id,
|
||||
name: indexer.name,
|
||||
priority: indexer.priority,
|
||||
seedingTimeMinutes: indexer.seedingTimeMinutes,
|
||||
rssEnabled: indexer.rssEnabled || false,
|
||||
categories: indexer.categories || [3030], // Default to audiobooks if not specified
|
||||
}));
|
||||
.map((indexer: any) => {
|
||||
const config: any = {
|
||||
id: indexer.id,
|
||||
name: indexer.name,
|
||||
protocol: indexer.protocol,
|
||||
priority: indexer.priority,
|
||||
rssEnabled: indexer.rssEnabled || false,
|
||||
categories: indexer.categories || [3030], // Default to audiobooks if not specified
|
||||
};
|
||||
|
||||
// Add protocol-specific fields
|
||||
const isTorrent = indexer.protocol?.toLowerCase() === 'torrent';
|
||||
if (isTorrent) {
|
||||
config.seedingTimeMinutes = indexer.seedingTimeMinutes ?? 0;
|
||||
} else {
|
||||
config.removeAfterProcessing = indexer.removeAfterProcessing ?? true;
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
// Save to configuration (matches wizard format)
|
||||
const configService = getConfigService();
|
||||
|
||||
@@ -23,6 +23,14 @@ export async function GET(request: NextRequest) {
|
||||
where: { authProvider: 'local' }
|
||||
})) > 0;
|
||||
|
||||
// Check if any local admin users exist (for validation)
|
||||
const hasLocalAdmins = (await prisma.user.count({
|
||||
where: {
|
||||
authProvider: 'local',
|
||||
role: 'admin'
|
||||
}
|
||||
})) > 0;
|
||||
|
||||
// Mask sensitive values
|
||||
const maskValue = (key: string, value: string | null | undefined) => {
|
||||
const sensitiveKeys = ['token', 'api_key', 'password', 'secret'];
|
||||
@@ -36,6 +44,7 @@ export async function GET(request: NextRequest) {
|
||||
const settings = {
|
||||
backendMode: configMap.get('system.backend_mode') || 'plex',
|
||||
hasLocalUsers,
|
||||
hasLocalAdmins,
|
||||
audibleRegion: configMap.get('audible.region') || 'us',
|
||||
plex: {
|
||||
url: configMap.get('plex_url') || '',
|
||||
|
||||
@@ -110,7 +110,7 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
results: [],
|
||||
message: 'No torrents found',
|
||||
message: 'No torrents/nzbs found',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,7 +138,12 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Rank torrents using the ranking algorithm with indexer priorities and flag configs
|
||||
// Note: rankTorrents now filters out results < 20 MB internally
|
||||
const rankedResults = rankTorrents(results, { title, author, durationMinutes }, indexerPriorities, flagConfigs);
|
||||
// requireAuthor: false - interactive search, show all results for user decision
|
||||
const rankedResults = rankTorrents(results, { title, author, durationMinutes }, {
|
||||
indexerPriorities,
|
||||
flagConfigs,
|
||||
requireAuthor: false // Interactive mode - let user decide
|
||||
});
|
||||
|
||||
// Log filter results
|
||||
const postFilterCount = rankedResults.length;
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function GET(request: NextRequest) {
|
||||
plexId: result.user.id, // Use id as plexId for consistency
|
||||
username: result.user.username,
|
||||
email: result.user.email,
|
||||
role: result.user.isAdmin ? 'admin' : 'user',
|
||||
role: result.user.role || 'user',
|
||||
avatarUrl: result.user.avatarUrl,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,8 +36,9 @@ export async function GET() {
|
||||
|
||||
const providers: string[] = [];
|
||||
if (oidcEnabled) providers.push('oidc');
|
||||
// Only add 'local' provider if not disabled and users exist
|
||||
if (hasLocalUsers && !localLoginDisabled) providers.push('local');
|
||||
// Add 'local' provider if not disabled and (users exist OR registration is enabled)
|
||||
// Registration needs local auth form to be shown even when no users exist yet
|
||||
if ((hasLocalUsers || registrationEnabled) && !localLoginDisabled) providers.push('local');
|
||||
|
||||
return NextResponse.json({
|
||||
backendMode: 'audiobookshelf',
|
||||
|
||||
@@ -39,7 +39,7 @@ async function getConfig(req: AuthenticatedRequest) {
|
||||
async function saveConfig(req: AuthenticatedRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { provider, apiKey, model, baseUrl, libraryScope, customPrompt, isEnabled } = body;
|
||||
const { provider, apiKey, model, baseUrl, isEnabled } = body;
|
||||
|
||||
// Check if config exists
|
||||
const existingConfig = await prisma.bookDateConfig.findFirst();
|
||||
@@ -143,14 +143,11 @@ async function saveConfig(req: AuthenticatedRequest) {
|
||||
});
|
||||
} else {
|
||||
// Create new global config
|
||||
// Note: libraryScope and customPrompt are now per-user settings (deprecated in global config)
|
||||
config = await prisma.bookDateConfig.create({
|
||||
data: {
|
||||
provider,
|
||||
model,
|
||||
baseUrl: provider === 'custom' ? baseUrl : null,
|
||||
libraryScope: 'full', // Default value for backwards compatibility
|
||||
customPrompt: null,
|
||||
isEnabled: isEnabled !== undefined ? isEnabled : true,
|
||||
isVerified: true,
|
||||
apiKey: encryptedApiKeyToUse,
|
||||
|
||||
@@ -123,16 +123,21 @@ export async function POST(
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
results: [],
|
||||
message: 'No torrents found',
|
||||
message: 'No torrents/nzbs found',
|
||||
});
|
||||
}
|
||||
|
||||
// Rank torrents using the ranking algorithm with indexer priorities and flag configs
|
||||
// Always use the audiobook's title/author for ranking (not custom search query)
|
||||
// requireAuthor: false - interactive mode, show all results for user decision
|
||||
const rankedResults = rankTorrents(results, {
|
||||
title: requestRecord.audiobook.title,
|
||||
author: requestRecord.audiobook.author,
|
||||
}, indexerPriorities, flagConfigs);
|
||||
}, {
|
||||
indexerPriorities,
|
||||
flagConfigs,
|
||||
requireAuthor: false // Interactive mode - let user decide
|
||||
});
|
||||
|
||||
// No threshold filtering for interactive search - show all results
|
||||
// User can see scores and make their own decision
|
||||
|
||||
@@ -468,8 +468,6 @@ export async function POST(request: NextRequest) {
|
||||
provider: bookdate.provider,
|
||||
apiKey: encryptedApiKey,
|
||||
model: bookdate.model,
|
||||
libraryScope: 'full', // Default value for backwards compatibility
|
||||
customPrompt: null,
|
||||
isVerified: true,
|
||||
isEnabled: true,
|
||||
},
|
||||
@@ -481,8 +479,6 @@ export async function POST(request: NextRequest) {
|
||||
provider: bookdate.provider,
|
||||
apiKey: encryptedApiKey,
|
||||
model: bookdate.model,
|
||||
libraryScope: 'full', // Default value for backwards compatibility
|
||||
customPrompt: null,
|
||||
isVerified: true,
|
||||
isEnabled: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user