Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/addon/searchOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,74 @@ export async function indexManagerSearch(ctx: SearchContext): Promise<any[]> {
}
}

// Date-format fallback: when all prior searches (SxxExx primary, alias,
// etc.) return zero results and TVDB provides an air date for the episode,
// retry using a date-formatted query — e.g. "AEW Dynamite 2026.05.13".
// This catches shows whose Usenet releases are date-named rather than
// SxxExx-numbered (wrestling, sports, daily programmes). The alias
// fallback above already applies date-scheme when aliases exist; this
// block covers the canonical title and any additional titles for shows
// that have no useful TVDB aliases. UTS-only.
if (
results.length === 0
&& type === 'series'
&& season !== undefined
&& episode !== undefined
&& typeof episodeAired === 'string'
&& /^\d{4}-\d{2}-\d{2}/.test(episodeAired)
&& !isAnime
&& config.searchConfig?.dateFallback !== false
) {
const retryIndexers = effectiveIndexers.filter(i => !timedOutIndexers.has(i.name) && isTextCapable(i));
if (retryIndexers.length === 0 && enabledIndexers.length > 0) {
slog(`⚠️ No text-method indexers, skipping date-format fallback`);
}
if (retryIndexers.length > 0) {
const dateDotted = episodeAired.slice(0, 10).replace(/-/g, '.');
// Fan over primary title and alts when parallel mode is on (alts already
// fired upfront in that mode, so they still need a date-scheme probe here).
const titlesToRetry = (parallelAltEnabled && additionalTitles?.length)
? [title, ...additionalTitles]
: [title];
slog(`📅 Date-format fallback: no SxxExx/alias results found, trying date query (${dateDotted})`);
const datePromises = retryIndexers.map(async (indexer) => {
const { result, lines } = await withBuffer(async (): Promise<any[]> => {
const subResults = await Promise.all(titlesToRetry.map(t =>
withSubBuffer(`Date fallback: "${t} ${dateDotted}"`, async () => {
const startTime = Date.now();
const searcher = new UsenetSearcher(applySearchTimeoutOverride(indexer));
try {
// Pass additionalTitles only for the primary title so the
// filter stays tight on alt-title iterations.
const altsForFilter = t === title ? additionalTitles : undefined;
const fbResults = await searcher.searchTVShow(
imdbId, t, season, episode, episodesInSeason, year, country,
undefined, 'text', altsForFilter, titleYear,
{ numberingScheme: 'date', airedDate: episodeAired },
);
if (searcher.timedOut) timedOutIndexers.add(indexer.name);
const responseTime = Date.now() - startTime;
trackQuery(indexer.name, true, responseTime, fbResults.length);
return fbResults.map(r => ({ ...r, indexerName: indexer.name }));
} catch (error) {
if (searcher.timedOut) timedOutIndexers.add(indexer.name);
const responseTime = Date.now() - startTime;
trackQuery(indexer.name, false, responseTime, 0, error instanceof Error ? error.message : 'Unknown error');
slog(`❌ Error in date-format fallback for ${indexer.name} ("${t}"): ${error instanceof Error ? error.message : 'Unknown error'}`);
return [];
}
})
));
return subResults.flat();
});
return { result, lines, indexerName: indexer.name };
});
const dateResults = await Promise.all(datePromises);
for (const r of dateResults) accumulate(r.indexerName, r.lines);
results = dateResults.flatMap(r => r.result);
}
}

// Absolute-episode fallback: covers indexers that file releases under
// continuous absolute numbering (Title E31) rather than Title S03E07.
// When combined results are zero after the main pass + standard
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export interface SearchConfig {
parallelAlternateTitleSearch?: boolean; // Run primary + alt-title searches in parallel from the start instead of using alts only as a zero-result fallback. UTS only. (default false)
tvdbPreferEnglishTitle?: boolean; // When TVDB's canonical title is non-English, substitute the English translation for indexer text search (default true)
aliasTitleFallback?: boolean; // When a UTS search returns zero results, retry once per English alias from TVDB whose normalized form is a strict substring of the canonical title and substantially shorter. UTS only. (default true)
dateFallback?: boolean; // Series text-search: when all other searches return zero results and TVDB has an air date for the episode, retry using a date-formatted query (e.g. "AEW Dynamite 2026.05.13"). Catches shows whose releases use date-based naming rather than SxxExx (e.g. wrestling, sports, daily programmes). UTS only. (default true)
includeMultiSeasonPacks?: boolean; // Series Packs master toggle. When true (default for fresh installs, false for users upgrading from before this field existed), gates the multi-season fanout query and keyword queries.
seriesPackKeywords?: string[]; // Each enabled keyword fires a '<Title> <Keyword>' indexer query to catch keyword-only releases without Sxx tokens. Empty array means no keyword queries fire. Allowed values come from SERIES_PACK_KEYWORDS.
seriesPackPagination?: boolean; // Enable pagination for series-pack queries (multi-season fanout + keyword queries). Independent of seasonPackPagination. (default true)
Expand Down