Add remote path mapping for qBittorrent integration

Implements remote-to-local path mapping for qBittorrent downloads, allowing the app to handle differing filesystem paths between qBittorrent and the local environment (e.g., remote seedboxes, Docker). Adds UI controls in admin settings and setup wizard, validates mapping configuration, and applies path transformation in download and import processors. Updates documentation, API routes, and data models to support the new feature. Also improves library scan logic to remove stale records and reset orphaned audiobooks and requests. Increases minimum torrent score threshold from 30 to 50 in search and ranking logic, and exposes torrent source URLs in the admin UI.
This commit is contained in:
kikootwo
2026-01-04 06:28:17 -05:00
parent d617e26c92
commit ca7cac0c88
26 changed files with 1108 additions and 75 deletions
+32 -2
View File
@@ -198,7 +198,37 @@ export class RankingAlgorithm {
const requestTitle = audiobook.title.toLowerCase();
const requestAuthor = audiobook.author.toLowerCase();
// Title matching (0-35 points)
// ========== STAGE 1: WORD COVERAGE FILTER (MANDATORY) ==========
// Extract significant words (filter out common stop words)
const stopWords = ['the', 'a', 'an', 'of', 'on', 'in', 'at', 'by', 'for'];
const extractWords = (text: string, stopList: string[]): string[] => {
return text
.toLowerCase()
.replace(/[^\w\s]/g, ' ') // Remove punctuation
.split(/\s+/)
.filter(word => word.length > 0 && !stopList.includes(word));
};
const requestWords = extractWords(requestTitle, stopWords);
const torrentWords = extractWords(torrentTitle, stopWords);
// Calculate word coverage: how many REQUEST words appear in TORRENT
if (requestWords.length === 0) {
// Edge case: title is only stop words, skip filter
// Fall through to normal scoring
} else {
const matchedWords = requestWords.filter(word => torrentWords.includes(word));
const coverage = matchedWords.length / requestWords.length;
// HARD REQUIREMENT: Must have 80%+ word coverage
if (coverage < 0.80) {
// Automatic rejection - doesn't contain enough of the requested words
return 0;
}
}
// ========== STAGE 2: TITLE MATCHING (0-35 points) ==========
let titleScore = 0;
if (torrentTitle.includes(requestTitle)) {
// Found the title, but is it the complete title or part of a longer one?
@@ -224,7 +254,7 @@ export class RankingAlgorithm {
titleScore = compareTwoStrings(requestTitle, torrentTitle) * 35;
}
// Author matching (0-15 points)
// ========== STAGE 3: AUTHOR MATCHING (0-15 points) ==========
// Parse requested authors (split on separators, filter out roles)
const requestAuthors = requestAuthor
.split(/,|&| and | - /)