mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-04 05:10:11 +00:00
Add Transmission/NZBGet and per-client paths and much more
Extend multi-download-client support to include Transmission and NZBGet and introduce per-client custom download paths. Adds protocol mapping and new client types, Transmission/NZBGet integration services, API CRUD and validation changes, UI components/modal updates and live path previews, and manager routing by protocol. Includes DB migrations (download_path on download_history, interactive_search_access on users), schema updates, and related processor/service fixes and tests to ensure backward compatibility and proper path resolution.
This commit is contained in:
@@ -17,7 +17,7 @@ export interface TorrentResult {
|
||||
infoUrl?: string; // Link to indexer's info page (for user reference)
|
||||
infoHash?: string;
|
||||
guid: string;
|
||||
format?: 'M4B' | 'M4A' | 'MP3' | 'OTHER';
|
||||
format?: 'M4B' | 'M4A' | 'MP3' | 'FLAC' | 'OTHER';
|
||||
bitrate?: string;
|
||||
hasChapters?: boolean;
|
||||
flags?: string[]; // Indexer flags like "Freeleech", "Internal", etc.
|
||||
@@ -254,6 +254,7 @@ export class RankingAlgorithm {
|
||||
* Reduced from 25 to make room for data-driven size scoring
|
||||
* M4B with chapters: 10 pts
|
||||
* M4B without chapters: 9 pts
|
||||
* FLAC: 7 pts (lossless audio, excellent quality)
|
||||
* M4A: 6 pts
|
||||
* MP3: 4 pts
|
||||
* Other: 1 pt
|
||||
@@ -264,6 +265,8 @@ export class RankingAlgorithm {
|
||||
switch (format) {
|
||||
case 'M4B':
|
||||
return torrent.hasChapters !== false ? 10 : 9;
|
||||
case 'FLAC':
|
||||
return 7;
|
||||
case 'M4A':
|
||||
return 6;
|
||||
case 'MP3':
|
||||
@@ -395,11 +398,13 @@ export class RankingAlgorithm {
|
||||
.filter(word => word.length > 0 && !stopList.includes(word));
|
||||
};
|
||||
|
||||
// Separate required words (outside parentheses/brackets) from optional words (inside)
|
||||
// This handles common patterns like "Title (Subtitle)" where subtitle may be omitted
|
||||
// Note: Run on ORIGINAL title to preserve brackets, then normalize the result
|
||||
// Separate required words (outside parentheses/brackets/colon subtitles) from optional words
|
||||
// This handles common patterns like:
|
||||
// "Title (Subtitle)" where subtitle may be omitted
|
||||
// "Title: Series Name" where Audible appends series names after a colon
|
||||
// Note: Run on ORIGINAL title to preserve brackets/colons, then normalize the result
|
||||
const separateRequiredOptional = (title: string): { required: string; optional: string } => {
|
||||
// Work with original title format for bracket detection
|
||||
// Work with original title format for bracket/colon detection
|
||||
const originalTitle = audiobook.title.toLowerCase();
|
||||
|
||||
// Extract content in parentheses/brackets as optional
|
||||
@@ -411,8 +416,20 @@ export class RankingAlgorithm {
|
||||
optionalMatches.push(match[1]);
|
||||
}
|
||||
|
||||
// Remove parenthetical/bracketed content to get required portion
|
||||
const requiredRaw = originalTitle.replace(/[(\[{][^)\]}]+[)\]}]/g, ' ').trim();
|
||||
// Remove parenthetical/bracketed content to get the non-bracketed portion
|
||||
let requiredRaw = originalTitle.replace(/[(\[{][^)\]}]+[)\]}]/g, ' ').trim();
|
||||
|
||||
// Treat content after a colon as optional (Audible commonly appends series names)
|
||||
// e.g., "The Finest Edge of Twilight: Dungeons & Dragons" → required: title, optional: series
|
||||
const colonIndex = requiredRaw.indexOf(':');
|
||||
if (colonIndex > 0 && colonIndex < requiredRaw.length - 1) {
|
||||
const afterColon = requiredRaw.substring(colonIndex + 1).trim();
|
||||
if (afterColon.length > 0) {
|
||||
optionalMatches.push(afterColon);
|
||||
}
|
||||
requiredRaw = requiredRaw.substring(0, colonIndex).trim();
|
||||
}
|
||||
|
||||
// Normalize the required portion (handles CamelCase, punctuation)
|
||||
const required = this.normalizeForMatching(requiredRaw);
|
||||
const optional = optionalMatches.join(' ');
|
||||
@@ -652,7 +669,7 @@ export class RankingAlgorithm {
|
||||
/**
|
||||
* Detect format from torrent title
|
||||
*/
|
||||
private detectFormat(torrent: TorrentResult): 'M4B' | 'M4A' | 'MP3' | 'OTHER' {
|
||||
private detectFormat(torrent: TorrentResult): 'M4B' | 'M4A' | 'MP3' | 'FLAC' | 'OTHER' {
|
||||
// Use explicit format if provided
|
||||
if (torrent.format) {
|
||||
return torrent.format;
|
||||
@@ -664,6 +681,7 @@ export class RankingAlgorithm {
|
||||
if (title.includes('M4B')) return 'M4B';
|
||||
if (title.includes('M4A')) return 'M4A';
|
||||
if (title.includes('MP3')) return 'MP3';
|
||||
if (title.includes('FLAC')) return 'FLAC';
|
||||
|
||||
// Default to OTHER if no format detected
|
||||
return 'OTHER';
|
||||
@@ -686,6 +704,8 @@ export class RankingAlgorithm {
|
||||
if (torrent.hasChapters !== false) {
|
||||
notes.push('Has chapter markers');
|
||||
}
|
||||
} else if (format === 'FLAC') {
|
||||
notes.push('Lossless format (FLAC)');
|
||||
} else if (format === 'M4A') {
|
||||
notes.push('Good format (M4A)');
|
||||
} else if (format === 'MP3') {
|
||||
|
||||
Reference in New Issue
Block a user