Add interactive ebook search & selection

Introduce interactive ebook support: adds two API endpoints to search (interactive-search-ebook) and create/select ebook requests (select-ebook), plus server-side handlers to route Anna's Archive (direct) and indexer (torrent/NZB) downloads. Frontend: extend RequestActionsDropdown and InteractiveTorrentSearchModal to support an "ebook" search mode and selection flow, and add hooks (useInteractiveSearchEbook / useSelectEbook). Settings: add ebook_auto_grab_enabled with UI toggle and enforce disabling when no ebook sources are enabled; settings GET/PUT updated to persist the flag (default = true to preserve behavior). Documentation updated (scheduler, ebook-sidecar, settings pages) and ranking algorithm docs/tests extended to cover ebook-related normalization and matching cases. Includes logging and ranking integration for indexer results and normalization for Anna's Archive handling.
This commit is contained in:
kikootwo
2026-02-02 19:59:58 -05:00
parent c913be5ca2
commit 1afab5d47f
19 changed files with 1339 additions and 115 deletions
+153
View File
@@ -1034,6 +1034,159 @@ describe('ranking-algorithm', () => {
});
});
describe('Initial Variations (J.N. vs J N)', () => {
const algorithm = new RankingAlgorithm();
it('matches "J.N. Chaney" to torrent with "J N Chaney" in automatic mode', () => {
const torrent = {
...baseTorrent,
title: 'Infinite Crown by Terry Maggert, J N Chaney [ENG / M4B]',
};
const breakdown = algorithm.getScoreBreakdown(torrent, {
title: 'Infinite Crown',
author: 'J.N. Chaney',
}, true); // requireAuthor: true (automatic mode)
// "J.N. Chaney" should normalize to "j n chaney"
// Torrent title should normalize to include "j n chaney"
// Author check should PASS
expect(breakdown.matchScore).toBeGreaterThan(0);
expect(breakdown.totalScore).toBeGreaterThanOrEqual(50);
});
it('matches author with periods to space-separated initials', () => {
const torrent = {
...baseTorrent,
title: 'Book Title by J K Rowling [M4B]',
};
const breakdown = algorithm.getScoreBreakdown(torrent, {
title: 'Book Title',
author: 'J.K. Rowling',
}, true);
expect(breakdown.matchScore).toBeGreaterThan(0);
});
});
describe('CamelCase and Punctuation Separator Handling', () => {
const algorithm = new RankingAlgorithm();
it('matches CamelCase torrent title "VirginaEvans TheCorrespondent" to "The Correspondent" by "Virginia Evans"', () => {
const torrent = {
...baseTorrent,
title: 'VirginaEvans TheCorrespondent',
};
const breakdown = algorithm.getScoreBreakdown(torrent, {
title: 'The Correspondent',
author: 'Virginia Evans',
}, false); // requireAuthor: false - source has typo "Virgina" vs "Virginia"
// Should match after CamelCase normalization
// "VirginaEvans TheCorrespondent" → "virgina evans the correspondent"
// "The Correspondent" → "the correspondent" → required words: ["correspondent"]
// Coverage: "correspondent" found → passes
// Note: Author has typo in source data ("Virgina" vs "Virginia"), so fuzzy matching gives partial credit
expect(breakdown.matchScore).toBeGreaterThan(35);
});
it('matches period-separated title "Twelve.Months-Jim.Butcher" to "Twelve Months" by "Jim Butcher"', () => {
const torrent = {
...baseTorrent,
title: 'Twelve.Months-Jim.Butcher',
};
const breakdown = algorithm.getScoreBreakdown(torrent, {
title: 'Twelve Months',
author: 'Jim Butcher',
});
// Should match after punctuation normalization
// "Twelve.Months-Jim.Butcher" → "twelve months jim butcher"
// Full title match + author match
expect(breakdown.matchScore).toBeGreaterThan(55);
});
it('matches mixed CamelCase and punctuation "AuthorName-BookTitle.2024"', () => {
const torrent = {
...baseTorrent,
title: 'JohnSmith-GreatBook.2024',
};
const breakdown = algorithm.getScoreBreakdown(torrent, {
title: 'Great Book',
author: 'John Smith',
});
// "JohnSmith-GreatBook.2024" → "john smith great book 2024"
// Gets good fuzzy match score (title words present, author present)
expect(breakdown.matchScore).toBeGreaterThan(35);
});
it('matches CamelCase author with no separator "AuthorNameBookTitle"', () => {
const torrent = {
...baseTorrent,
title: 'BrandonSandersonMistborn',
};
const breakdown = algorithm.getScoreBreakdown(torrent, {
title: 'Mistborn',
author: 'Brandon Sanderson',
});
// "BrandonSandersonMistborn" → "brandon sanderson mistborn"
expect(breakdown.matchScore).toBeGreaterThan(50);
});
it('handles underscore separators "Author_Name_Book_Title"', () => {
const torrent = {
...baseTorrent,
title: 'Jane_Doe_Amazing_Story',
};
const breakdown = algorithm.getScoreBreakdown(torrent, {
title: 'Amazing Story',
author: 'Jane Doe',
});
// "Jane_Doe_Amazing_Story" → "jane doe amazing story"
expect(breakdown.matchScore).toBeGreaterThan(50);
});
it('preserves apostrophes in names like "O\'Brien"', () => {
const torrent = {
...baseTorrent,
title: "Tim O'Brien - The Things They Carried",
};
const breakdown = algorithm.getScoreBreakdown(torrent, {
title: 'The Things They Carried',
author: "Tim O'Brien",
});
// Apostrophe should be preserved
expect(breakdown.matchScore).toBeGreaterThan(50);
});
it('handles real-world NZB title format with periods', () => {
const torrent = {
...baseTorrent,
title: 'William.L.Shirer-Berlin.Diary-AUDIOBOOK-96kbs',
};
const breakdown = algorithm.getScoreBreakdown(torrent, {
title: 'Berlin Diary',
author: 'William L. Shirer',
});
// "William.L.Shirer-Berlin.Diary-AUDIOBOOK-96kbs" → "william l shirer berlin diary audiobook 96kbs"
// Gets partial score from fuzzy matching (title words + author words present)
expect(breakdown.matchScore).toBeGreaterThan(30);
});
});
describe('Legacy API Compatibility', () => {
it('supports legacy rankTorrents signature with separate parameters', () => {
const torrent = {