From 492543dd36e943d5aaef5c8fb8f930ef311fba32 Mon Sep 17 00:00:00 2001 From: Wilb Date: Sat, 16 May 2026 16:07:03 +0100 Subject: [PATCH] feat: add date-format fallback for date-named TV releases (fixes #28) Shows like AEW Dynamite are filed on Usenet as 'AEW.Dynamite.2026.05.13' rather than SxxExx. All prior fallbacks (primary, alias, absolute-episode) returned zero results because they only tried season/episode numbering. This adds a new dateFallback step (enabled by default, opt-out via SearchConfig.dateFallback = false) that fires when: - type is 'series' and results are still zero - TVDB provides an episodeAired date (YYYY-MM-DD) - show is not anime It calls the existing searchTVShow() date-scheme path with the canonical title (plus additional titles in parallel-alt mode), building a query like 'AEW Dynamite 2026.05.13'. UTS only. Closes #28 --- src/addon/searchOrchestrator.ts | 68 +++++++++++++++++++++++++++++++++ src/types.ts | 1 + 2 files changed, 69 insertions(+) diff --git a/src/addon/searchOrchestrator.ts b/src/addon/searchOrchestrator.ts index 9df21b5..2e131f2 100644 --- a/src/addon/searchOrchestrator.ts +++ b/src/addon/searchOrchestrator.ts @@ -471,6 +471,74 @@ export async function indexManagerSearch(ctx: SearchContext): Promise { } } + // 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 => { + 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 diff --git a/src/types.ts b/src/types.ts index ad8f187..92982da 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 ' <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)