Skip to content

Commit eee5070

Browse files
author
strxno
committed
Enhance search handling for Newznab and NZBHydra by incorporating ASCII title support and refining query parameter logic. This update ensures non-ASCII characters are managed effectively, improving search accuracy and compatibility.
1 parent 6746354 commit eee5070

3 files changed

Lines changed: 150 additions & 47 deletions

File tree

server.js

Lines changed: 88 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,7 +1542,7 @@ async function streamHandler(req, res) {
15421542
if (!usingCachedSearchResults) {
15431543
const searchPlans = [];
15441544
const seenPlans = new Set();
1545-
const addPlan = (planType, { tokens = [], rawQuery = null } = {}) => {
1545+
const addPlan = (planType, { tokens = [], rawQuery = null, asciiTitle = null } = {}) => {
15461546
const tokenList = [...tokens];
15471547
if (planType === 'tvsearch') {
15481548
if (seasonToken) tokenList.push(seasonToken);
@@ -1559,6 +1559,10 @@ async function streamHandler(req, res) {
15591559
}
15601560
seenPlans.add(planKey);
15611561
const planRecord = { type: planType, query, rawQuery: rawQuery ? rawQuery : null, tokens: normalizedTokens };
1562+
// Store asciiTitle for Newznab searches to avoid non-ASCII character issues
1563+
if (asciiTitle) {
1564+
planRecord.asciiTitle = asciiTitle;
1565+
}
15621566
if (strictTextMode && planType === 'search' && rawQuery) {
15631567
const strictPhrase = sanitizeStrictSearchPhrase(rawQuery);
15641568
if (strictPhrase) {
@@ -1570,8 +1574,8 @@ async function streamHandler(req, res) {
15701574
return true;
15711575
};
15721576

1573-
// Add ID-based searches immediately (before waiting for TMDb/Cinemeta)
1574-
// Note: movieTitle might not be available yet, will be added later if needed
1577+
// Add ID-based searches (will start after title is resolved)
1578+
// Note: We'll add the title to these plans before executing them
15751579
if (type === 'series' && metaIds.tvdb) {
15761580
addPlan('tvsearch', { tokens: [`{TvdbId:${metaIds.tvdb}}`] });
15771581
}
@@ -1584,21 +1588,6 @@ async function streamHandler(req, res) {
15841588
addPlan(searchType, { tokens: [`{ImdbId:${metaIds.imdb}}`] });
15851589
}
15861590

1587-
// Start ID-based searches immediately in background
1588-
const idSearchPromises = [];
1589-
const idSearchStartTs = Date.now();
1590-
if (searchPlans.length > 0) {
1591-
console.log(`${INDEXER_LOG_PREFIX} Starting ${searchPlans.length} ID-based search(es) immediately`);
1592-
idSearchPromises.push(...searchPlans.map((plan) => {
1593-
console.log(`${INDEXER_LOG_PREFIX} Dispatching early ID plan`, plan);
1594-
const planStartTs = Date.now();
1595-
return Promise.allSettled([
1596-
executeManagerPlanWithBackoff(plan),
1597-
executeNewznabPlan(plan),
1598-
]).then((settled) => ({ plan, settled, startTs: planStartTs, endTs: Date.now() }));
1599-
}));
1600-
}
1601-
16021591
// Now wait for TMDb to get localized titles (if applicable)
16031592
const tmdbWaitStartTs = Date.now();
16041593
if (tmdbMetadataPromise) {
@@ -1677,16 +1666,57 @@ async function streamHandler(req, res) {
16771666

16781667
// Update ID-based plans with title if it's now available
16791668
// This ensures NZBHydra searches include the title even when tokens are present
1669+
// For Newznab, prefer ASCII version if title contains non-ASCII characters
16801670
if (movieTitle && movieTitle.trim()) {
1671+
const hasNonAscii = /[^\x00-\x7F]/.test(movieTitle);
1672+
let titleToUse = movieTitle.trim();
1673+
let asciiTitleToUse = null;
1674+
1675+
// Check if we have ASCII version from TMDB
1676+
const tmdbTitles = metaSources.find(s => s?._tmdbTitles)?._tmdbTitles;
1677+
if (hasNonAscii && tmdbTitles) {
1678+
// Find English title or ASCII version
1679+
const englishTitle = tmdbTitles.find(t => t.language?.startsWith('en-'));
1680+
if (englishTitle) {
1681+
titleToUse = englishTitle.title;
1682+
asciiTitleToUse = englishTitle.asciiTitle || null;
1683+
} else {
1684+
// Try to find any ASCII title
1685+
const asciiTitleObj = tmdbTitles.find(t => t.asciiTitle && !/[^\x00-\x7F]/.test(t.asciiTitle));
1686+
if (asciiTitleObj) {
1687+
titleToUse = asciiTitleObj.asciiTitle;
1688+
}
1689+
}
1690+
}
1691+
16811692
searchPlans.forEach((plan) => {
16821693
if (plan.tokens && plan.tokens.length > 0 && !plan.rawQuery) {
16831694
// Add title to rawQuery so it's included in the search
1684-
plan.rawQuery = movieTitle.trim();
1685-
console.log(`${INDEXER_LOG_PREFIX} Updated ID plan with title: "${movieTitle}"`, { type: plan.type, tokens: plan.tokens });
1695+
plan.rawQuery = titleToUse;
1696+
if (asciiTitleToUse) {
1697+
plan.asciiTitle = asciiTitleToUse;
1698+
}
1699+
console.log(`${INDEXER_LOG_PREFIX} Updated ID plan with title: "${titleToUse}"`, { type: plan.type, tokens: plan.tokens, hasNonAscii });
16861700
}
16871701
});
16881702
}
16891703

1704+
// Start ID-based searches now that we have the title
1705+
const idSearchPromises = [];
1706+
const idSearchStartTs = Date.now();
1707+
const idPlansToExecute = searchPlans.filter(p => p.tokens && p.tokens.length > 0);
1708+
if (idPlansToExecute.length > 0) {
1709+
console.log(`${INDEXER_LOG_PREFIX} Starting ${idPlansToExecute.length} ID-based search(es) with title`);
1710+
idSearchPromises.push(...idPlansToExecute.map((plan) => {
1711+
console.log(`${INDEXER_LOG_PREFIX} Dispatching ID plan`, plan);
1712+
const planStartTs = Date.now();
1713+
return Promise.allSettled([
1714+
executeManagerPlanWithBackoff(plan),
1715+
executeNewznabPlan(plan),
1716+
]).then((settled) => ({ plan, settled, startTs: planStartTs, endTs: Date.now() }));
1717+
}));
1718+
}
1719+
16901720
// Continue with text-based searches using TMDb titles
16911721
const textQueryParts = [];
16921722
let tmdbLocalizedQuery = null;
@@ -1718,9 +1748,21 @@ async function streamHandler(req, res) {
17181748
const fallbackIdentifier = hasTmdbTitles ? null : (incomingImdbId || baseIdentifier);
17191749
textQueryFallbackValue = (textQueryCandidate || fallbackIdentifier || '').trim();
17201750
if (textQueryFallbackValue) {
1721-
const addedTextPlan = addPlan('search', { rawQuery: textQueryFallbackValue });
1751+
// Check if we need ASCII version for Newznab
1752+
const hasNonAscii = /[^\x00-\x7F]/.test(textQueryFallbackValue);
1753+
let asciiFallback = null;
1754+
if (hasNonAscii) {
1755+
const tmdbTitles = metaSources.find(s => s?._tmdbTitles)?._tmdbTitles;
1756+
if (tmdbTitles) {
1757+
const englishTitle = tmdbTitles.find(t => t.language?.startsWith('en-'));
1758+
if (englishTitle) {
1759+
asciiFallback = `${englishTitle.asciiTitle || englishTitle.title}${textQueryFallbackValue.replace(/^[^\x00-\x7F]+/, '').replace(/[^\x00-\x7F]+$/, '')}`.trim();
1760+
}
1761+
}
1762+
}
1763+
const addedTextPlan = addPlan('search', { rawQuery: textQueryFallbackValue, asciiTitle: asciiFallback });
17221764
if (addedTextPlan) {
1723-
console.log(`${INDEXER_LOG_PREFIX} Added text search plan`, { query: textQueryFallbackValue });
1765+
console.log(`${INDEXER_LOG_PREFIX} Added text search plan`, { query: textQueryFallbackValue, hasNonAscii, asciiFallback: asciiFallback || 'none' });
17241766
} else {
17251767
console.log(`${INDEXER_LOG_PREFIX} Text search plan already present`, { query: textQueryFallbackValue });
17261768
}
@@ -1734,28 +1776,36 @@ async function streamHandler(req, res) {
17341776
if (tmdbTitles && tmdbTitles.length > 0 && !isSpecialRequest) {
17351777
console.log(`[TMDB] Adding ${tmdbTitles.length} language-specific search plans`);
17361778
tmdbTitles.forEach((titleObj) => {
1737-
let localizedQuery = titleObj.title;
1779+
// For titles with non-ASCII characters, prefer ASCII version for Newznab
1780+
// Store both in the plan so Newznab can choose ASCII automatically
1781+
const hasNonAscii = /[^\x00-\x7F]/.test(titleObj.title);
1782+
const queryTitle = hasNonAscii && titleObj.asciiTitle ? titleObj.asciiTitle : titleObj.title;
1783+
1784+
let localizedQuery = queryTitle;
17381785
if (type === 'movie' && Number.isFinite(releaseYear)) {
17391786
localizedQuery = `${localizedQuery} ${releaseYear}`;
17401787
} else if (type === 'series' && Number.isFinite(seasonNum) && Number.isFinite(episodeNum)) {
17411788
localizedQuery = `${localizedQuery} S${String(seasonNum).padStart(2, '0')}E${String(episodeNum).padStart(2, '0')}`;
17421789
}
1743-
const added = addPlan('search', { rawQuery: localizedQuery });
1744-
if (added) {
1745-
console.log(`${INDEXER_LOG_PREFIX} Added TMDb ${titleObj.language} search plan`, { query: localizedQuery });
1746-
}
1747-
1748-
// Add ASCII fallback if different
1749-
if (titleObj.asciiTitle && titleObj.asciiTitle !== titleObj.title) {
1750-
let asciiQuery = titleObj.asciiTitle;
1790+
1791+
// Include asciiTitle in plan for Newznab to use if needed
1792+
const asciiQueryTitle = titleObj.asciiTitle && titleObj.asciiTitle !== titleObj.title ? titleObj.asciiTitle : null;
1793+
let asciiQuery = null;
1794+
if (asciiQueryTitle) {
1795+
asciiQuery = asciiQueryTitle;
17511796
if (type === 'movie' && Number.isFinite(releaseYear)) {
17521797
asciiQuery = `${asciiQuery} ${releaseYear}`;
17531798
} else if (type === 'series' && Number.isFinite(seasonNum) && Number.isFinite(episodeNum)) {
17541799
asciiQuery = `${asciiQuery} S${String(seasonNum).padStart(2, '0')}E${String(episodeNum).padStart(2, '0')}`;
17551800
}
1756-
const addedAscii = addPlan('search', { rawQuery: asciiQuery });
1757-
if (addedAscii) {
1758-
console.log(`${INDEXER_LOG_PREFIX} Added TMDb ${titleObj.language} ASCII search plan`, { query: asciiQuery });
1801+
}
1802+
1803+
const added = addPlan('search', { rawQuery: localizedQuery, asciiTitle: asciiQuery });
1804+
if (added) {
1805+
if (hasNonAscii && asciiQuery) {
1806+
console.log(`${INDEXER_LOG_PREFIX} Added TMDb ${titleObj.language} search plan (using ASCII for Newznab)`, { query: localizedQuery, asciiQuery });
1807+
} else {
1808+
console.log(`${INDEXER_LOG_PREFIX} Added TMDb ${titleObj.language} search plan`, { query: localizedQuery });
17591809
}
17601810
}
17611811

@@ -1981,7 +2031,10 @@ async function streamHandler(req, res) {
19812031
}
19822032

19832033
// Now execute remaining text-based search plans (exclude already-processed ID plans)
1984-
const remainingPlans = searchPlans.filter(p => !processedIdPlans.has(`${p.type}|${p.query}`));
2034+
const remainingPlans = searchPlans.filter(p => {
2035+
const planKey = `${p.type}|${p.query}`;
2036+
return !processedIdPlans.has(planKey) && (!p.tokens || p.tokens.length === 0);
2037+
});
19852038
console.log(`${INDEXER_LOG_PREFIX} Executing ${remainingPlans.length} text-based search plan(s)`);
19862039
const textSearchStartTs = Date.now();
19872040
const planExecutions = remainingPlans.map((plan) => {

src/services/indexer.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -161,24 +161,30 @@ function buildHydraSearchParams(plan) {
161161
plan.tokens.forEach((token) => applyTokenToHydraParams(token, params));
162162
}
163163

164-
// Include title in query parameter even when tokens are present
165-
// NZBHydra can use both structured params (season/ep) and text query together
164+
// Always include title in query parameter when available
165+
// NZBHydra can use both structured params (season/ep/imdbid) and text query together
166+
// This helps find results even when structured params don't match exactly
166167
if (plan.rawQuery) {
167-
params.q = plan.rawQuery;
168+
// rawQuery takes priority - it's the explicit title/text query
169+
params.q = plan.rawQuery.trim();
168170
} else if (plan.query) {
169-
// Extract title from query if it contains tokens
170-
// If query is just tokens (like "{ImdbId:tt123} {Season:1} {Episode:1}"),
171-
// we need to get the title from elsewhere or use the query as-is
172-
// For now, use the query which may contain the title if it was included
173171
const queryText = plan.query.trim();
174-
// Only set q if it's not just tokens (tokens are already in structured params)
175-
// Check if query looks like it contains actual text, not just token patterns
172+
// Check if query is just tokens (like "{ImdbId:tt123} {Season:1} {Episode:1}")
176173
const isJustTokens = /^\{[^}]+\}(\s+\{[^}]+\})*$/.test(queryText);
177-
if (!isJustTokens || !plan.tokens || plan.tokens.length === 0) {
174+
// If it's just tokens, don't set q (we don't want to search for the literal token string)
175+
// If it contains actual text, use it
176+
if (!isJustTokens) {
178177
params.q = queryText;
179178
}
179+
// If it's just tokens and we have structured params, we intentionally leave q empty
180+
// This means the search will rely only on structured params (imdbid, season, ep)
181+
// which might be why we're getting undefined results
180182
}
181183

184+
// If we have tokens but no query text, and this is a tvsearch, we should still try to include a title
185+
// But we can't do that here since we don't have access to the title
186+
// The title should be added to rawQuery before this function is called
187+
182188
return params;
183189
}
184190

src/services/newznab.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,18 +355,62 @@ function applyTokenToParams(token, params) {
355355
}
356356
}
357357

358+
/**
359+
* Check if string contains non-ASCII characters
360+
*/
361+
function containsNonAscii(text) {
362+
if (!text || typeof text !== 'string') return false;
363+
return /[^\x00-\x7F]/.test(text);
364+
}
365+
366+
/**
367+
* Normalize text to ASCII (strip diacritics and non-ASCII chars)
368+
* Similar to TMDB's normalizeToAscii but simpler for Newznab
369+
*/
370+
function normalizeToAscii(text) {
371+
if (!text || typeof text !== 'string') return '';
372+
return text
373+
.normalize('NFD')
374+
.replace(/[\u0300-\u036f]/g, '')
375+
.replace(/[^\x00-\x7F]/g, '')
376+
.trim();
377+
}
378+
358379
function buildSearchParams(plan) {
359380
const params = {
360381
t: mapPlanType(plan?.type),
361382
};
362383
if (Array.isArray(plan?.tokens)) {
363384
plan.tokens.forEach((token) => applyTokenToParams(token, params));
364385
}
386+
387+
// For Newznab indexers, prefer ASCII-only queries to avoid false results
388+
// Many indexers (like DrunkenSlug) don't handle non-ASCII characters well
389+
let queryText = null;
365390
if (plan?.rawQuery) {
366-
params.q = plan.rawQuery;
391+
queryText = plan.rawQuery;
367392
} else if ((!plan?.tokens || plan.tokens.length === 0) && plan?.query) {
368-
params.q = plan.query;
393+
queryText = plan.query;
394+
}
395+
396+
if (queryText) {
397+
// If query contains non-ASCII characters, try to use ASCII version
398+
if (containsNonAscii(queryText)) {
399+
// Check if plan has asciiTitle from TMDB
400+
const asciiVersion = plan?.asciiTitle || normalizeToAscii(queryText);
401+
if (asciiVersion && asciiVersion.trim() && asciiVersion !== queryText) {
402+
console.log(`[NEWZNAB] Query contains non-ASCII characters, using ASCII version: "${asciiVersion}" (was: "${queryText}")`);
403+
params.q = asciiVersion;
404+
} else {
405+
// If we can't get ASCII version, skip the query to avoid false results
406+
console.warn(`[NEWZNAB] Query contains non-ASCII characters but no ASCII version available, skipping query: "${queryText}"`);
407+
// Don't set params.q - rely on structured params (imdbid, season, ep) only
408+
}
409+
} else {
410+
params.q = queryText;
411+
}
369412
}
413+
370414
return params;
371415
}
372416

0 commit comments

Comments
 (0)