mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
Add e-book sidecar integration and improve request handling
Introduces optional e-book sidecar downloads from Anna's Archive, including admin UI, settings API, FlareSolverr integration, and documentation. Enhances request creation logic to prevent duplicate downloads by checking for 'downloaded' and 'available' statuses, updates UI to reflect processing state, and adds SABnzbd support to download and cleanup flows. Also updates ranking algorithm documentation and improves cache invalidation for recent requests.
This commit is contained in:
@@ -22,6 +22,7 @@ export interface AudiobookMatchInput {
|
||||
|
||||
export interface AudiobookMatchResult {
|
||||
plexGuid: string;
|
||||
plexRatingKey: string | null;
|
||||
title: string;
|
||||
author: string;
|
||||
}
|
||||
@@ -82,6 +83,7 @@ export async function findPlexMatch(
|
||||
},
|
||||
select: {
|
||||
plexGuid: true,
|
||||
plexRatingKey: true,
|
||||
title: true,
|
||||
author: true,
|
||||
asin: true, // Include ASIN field for direct matching
|
||||
@@ -297,6 +299,9 @@ export async function enrichAudiobooksWithMatches(
|
||||
id: true,
|
||||
audibleAsin: true,
|
||||
requests: {
|
||||
where: {
|
||||
deletedAt: null, // Only include active (non-deleted) requests
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
|
||||
@@ -9,6 +9,7 @@ import axios from 'axios';
|
||||
import { createJobLogger, JobLogger } from './job-logger';
|
||||
import { tagMultipleFiles, checkFfmpegAvailable } from './metadata-tagger';
|
||||
import { prisma } from '../db';
|
||||
import { downloadEbook } from '../services/ebook-scraper';
|
||||
|
||||
export interface AudiobookMetadata {
|
||||
title: string;
|
||||
@@ -261,6 +262,56 @@ export class FileOrganizer {
|
||||
}
|
||||
}
|
||||
|
||||
// E-book sidecar: Download accompanying e-book if enabled
|
||||
try {
|
||||
const ebookConfig = await prisma.configuration.findUnique({
|
||||
where: { key: 'ebook_sidecar_enabled' },
|
||||
});
|
||||
|
||||
const ebookEnabled = ebookConfig?.value === 'true';
|
||||
|
||||
if (ebookEnabled) {
|
||||
await logger?.info(`E-book sidecar enabled, searching for e-book...`);
|
||||
|
||||
// Get configuration
|
||||
const [formatConfig, baseUrlConfig, flaresolverrConfig] = await Promise.all([
|
||||
prisma.configuration.findUnique({ where: { key: 'ebook_sidecar_preferred_format' } }),
|
||||
prisma.configuration.findUnique({ where: { key: 'ebook_sidecar_base_url' } }),
|
||||
prisma.configuration.findUnique({ where: { key: 'ebook_sidecar_flaresolverr_url' } }),
|
||||
]);
|
||||
|
||||
const preferredFormat = formatConfig?.value || 'epub';
|
||||
const baseUrl = baseUrlConfig?.value || 'https://annas-archive.li';
|
||||
const flaresolverrUrl = flaresolverrConfig?.value || undefined;
|
||||
|
||||
// Download e-book (will try ASIN first, then fall back to title+author)
|
||||
const ebookResult = await downloadEbook(
|
||||
audiobook.asin || '', // ASIN (optional - will fallback to title+author if empty)
|
||||
audiobook.title,
|
||||
audiobook.author,
|
||||
targetPath, // Same directory as audiobook
|
||||
preferredFormat,
|
||||
baseUrl,
|
||||
logger ?? undefined,
|
||||
flaresolverrUrl
|
||||
);
|
||||
|
||||
if (ebookResult.success && ebookResult.filePath) {
|
||||
await logger?.info(`E-book downloaded: ${path.basename(ebookResult.filePath)}`);
|
||||
result.filesMovedCount++;
|
||||
} else {
|
||||
await logger?.warn(`E-book download failed: ${ebookResult.error}`);
|
||||
result.errors.push(`E-book sidecar: ${ebookResult.error}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await logger?.warn(
|
||||
`E-book sidecar error: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
result.errors.push('E-book sidecar failed');
|
||||
// Don't throw - audiobook organization continues
|
||||
}
|
||||
|
||||
result.targetPath = targetPath;
|
||||
result.success = true;
|
||||
|
||||
|
||||
@@ -350,14 +350,37 @@ export class RankingAlgorithm {
|
||||
const beforeWords = extractWords(beforeTitle, stopWords);
|
||||
|
||||
// Title is complete if:
|
||||
// 1. No significant words before it (not "This Inevitable Ruin" + "Dungeon Crawler Carl")
|
||||
// 1. Acceptable prefix (no words, OR structured metadata like "Author - Series - ")
|
||||
// 2. Followed by clear metadata markers (not "'s Secret" or " Is Watching")
|
||||
const metadataMarkers = [' by ', ' - ', ' [', ' (', ' {', ' :', ','];
|
||||
const hasNoWordsPrefix = beforeWords.length === 0;
|
||||
const hasMetadataSuffix = afterTitle === '' ||
|
||||
metadataMarkers.some(marker => afterTitle.startsWith(marker));
|
||||
|
||||
const isCompleteTitle = hasNoWordsPrefix && hasMetadataSuffix;
|
||||
// Check prefix validity:
|
||||
// - No words before = clean match
|
||||
// - Title preceded by separator (` - `, `: `) = structured metadata (Author - Series - Title)
|
||||
// - Author name in prefix = author attribution before title
|
||||
const hasNoWordsPrefix = beforeWords.length === 0;
|
||||
|
||||
// Check if title is immediately preceded by a metadata separator
|
||||
// This handles "Author - Series - 01 - Title" patterns
|
||||
const precedingText = beforeTitle.trimEnd();
|
||||
const titlePrecededBySeparator =
|
||||
precedingText.endsWith('-') ||
|
||||
precedingText.endsWith(':') ||
|
||||
precedingText.endsWith('—');
|
||||
|
||||
// Check if author name appears in the prefix
|
||||
// This handles "Author Name - Title" patterns
|
||||
const authorInPrefix = requestAuthor.length > 2 &&
|
||||
beforeTitle.includes(requestAuthor);
|
||||
|
||||
const hasAcceptablePrefix =
|
||||
hasNoWordsPrefix ||
|
||||
titlePrecededBySeparator ||
|
||||
authorInPrefix;
|
||||
|
||||
const isCompleteTitle = hasAcceptablePrefix && hasMetadataSuffix;
|
||||
|
||||
if (isCompleteTitle) {
|
||||
// Complete title match → full points
|
||||
|
||||
Reference in New Issue
Block a user