diff --git a/packages/core/src/db/schemas.ts b/packages/core/src/db/schemas.ts index 6fb09c8f..c7b44939 100644 --- a/packages/core/src/db/schemas.ts +++ b/packages/core/src/db/schemas.ts @@ -82,9 +82,7 @@ const SizeFilter = z.object({ // max: z.number().min(1).optional(), // }) .optional(), - anime: z - .tuple([z.number().min(0), z.number().min(0)]) - .optional(), + anime: z.tuple([z.number().min(0), z.number().min(0)]).optional(), }); const SizeFilterOptions = z.object({ diff --git a/packages/core/src/formatters/base.ts b/packages/core/src/formatters/base.ts index 0d8f1c43..adb84d76 100644 --- a/packages/core/src/formatters/base.ts +++ b/packages/core/src/formatters/base.ts @@ -326,7 +326,7 @@ export abstract class BaseFormatter { network: stream.parsedFile?.network || null, container: stream.parsedFile?.container || null, extension: stream.parsedFile?.extension || null, - seadex: (stream.seadex?.isSeadex && !stream.seadex?.isBest) ?? false, + seadex: stream.seadex?.isSeadex ?? false, seadexBest: stream.seadex?.isBest ?? false, }, addon: { diff --git a/packages/core/src/parser/streamExpression.ts b/packages/core/src/parser/streamExpression.ts index 403b015f..d986cf95 100644 --- a/packages/core/src/parser/streamExpression.ts +++ b/packages/core/src/parser/streamExpression.ts @@ -507,7 +507,7 @@ export abstract class StreamExpressionEngine { return streams.filter((stream) => stream.seadex?.isBest === true); } - // Return all SeaDex releases (best or regular) + // Return all SeaDex releases (includes group fallback matches) return streams.filter((stream) => stream.seadex?.isSeadex === true); }; diff --git a/packages/core/src/streams/precomputer.ts b/packages/core/src/streams/precomputer.ts index 7d77d700..f80ec7ce 100644 --- a/packages/core/src/streams/precomputer.ts +++ b/packages/core/src/streams/precomputer.ts @@ -40,7 +40,8 @@ class StreamPrecomputer { /** * Precompute SeaDex status for anime streams - * Tags streams with seadex.isBest and seadex.isSeadex based on infoHash matching + * Tags streams with seadex.isBest and seadex.isSeadex + * First tries to match by infoHash, then falls back to release group matching */ private async precomputeSeaDex( streams: ParsedStream[], @@ -80,39 +81,62 @@ class StreamPrecomputer { if ( seadexResult.bestHashes.size === 0 && - seadexResult.allHashes.size === 0 + seadexResult.allHashes.size === 0 && + seadexResult.bestGroups.size === 0 && + seadexResult.allGroups.size === 0 ) { logger.debug(`No SeaDex releases found for AniList ID ${anilistId}`); return; } let seadexBestCount = 0; let seadexCount = 0; + let seadexGroupFallbackCount = 0; for (const stream of streams) { const infoHash = stream.torrent?.infoHash?.toLowerCase(); - if (!infoHash) { - continue; + + // First try hash matching + if (infoHash) { + const isBest = seadexResult.bestHashes.has(infoHash); + const isSeadex = seadexResult.allHashes.has(infoHash); + + if (isSeadex) { + stream.seadex = { + isBest, + isSeadex: true, + }; + + if (isBest) { + seadexBestCount++; + } + seadexCount++; + continue; + } } - const isBest = seadexResult.bestHashes.has(infoHash); - const isSeadex = seadexResult.allHashes.has(infoHash); + // Fallback to release group matching + const releaseGroup = stream.parsedFile?.releaseGroup?.toLowerCase(); + if (releaseGroup) { + const isBestGroup = seadexResult.bestGroups.has(releaseGroup); + const isSeadexGroup = seadexResult.allGroups.has(releaseGroup); - if (isSeadex) { - stream.seadex = { - isBest, - isSeadex: true, - }; - - if (isBest) { - seadexBestCount++; + if (isBestGroup || isSeadexGroup) { + stream.seadex = { + isBest: isBestGroup, + isSeadex: true, + }; + if (isBestGroup) { + seadexBestCount++; + } + seadexCount++; + seadexGroupFallbackCount++; } - seadexCount++; } } if (seadexCount > 0) { logger.info( - `Tagged ${seadexCount} streams as SeaDex releases (${seadexBestCount} best) for AniList ID ${anilistId}` + `Tagged ${seadexCount} streams as SeaDex releases (${seadexBestCount} best, ${seadexGroupFallbackCount} via group fallback) for AniList ID ${anilistId}` ); } } diff --git a/packages/core/src/utils/constants.ts b/packages/core/src/utils/constants.ts index ae98fb06..eb30819a 100644 --- a/packages/core/src/utils/constants.ts +++ b/packages/core/src/utils/constants.ts @@ -1082,9 +1082,9 @@ export const SORT_CRITERIA_DETAILS: Record< description: 'Whether the stream is a SeaDex release (curated best anime releases from releases.moe)', ascendingDescription: - 'Non-SeaDex releases are preferred over SeaDex releases', + 'Streams that are not listed on SeaDex are preferred', descendingDescription: - 'SeaDex Best releases are preferred, then other SeaDex releases, then non-SeaDex releases', + 'Streams that are marked as the Best release on SeaDex are preferred, followed by the Alternative release', }, } as const; diff --git a/packages/core/src/utils/seadex.ts b/packages/core/src/utils/seadex.ts index 4b1b7fc3..6ae03121 100644 --- a/packages/core/src/utils/seadex.ts +++ b/packages/core/src/utils/seadex.ts @@ -19,11 +19,19 @@ const seadexCache = Cache.getInstance( export interface SeaDexResult { bestHashes: Set; allHashes: Set; + bestGroups: Set; + allGroups: Set; +} + +export interface SeaDexTagResult { + isBest: boolean; + isSeadex: boolean; } interface SeaDexTorrent { infoHash: string; isBest: boolean; + releaseGroup?: string; } interface SeaDexEntry { @@ -77,7 +85,12 @@ export class SeaDexApi { logger.warn( `SeaDex API returned ${response.status} for AniList ID ${anilistId}` ); - return { bestHashes: new Set(), allHashes: new Set() }; + return { + bestHashes: new Set(), + allHashes: new Set(), + bestGroups: new Set(), + allGroups: new Set(), + }; } const data = (await response.json()) as SeaDexResponse; @@ -88,6 +101,8 @@ export class SeaDexApi { const emptyResult: SeaDexResult = { bestHashes: new Set(), allHashes: new Set(), + bestGroups: new Set(), + allGroups: new Set(), }; await seadexCache.set(cacheKey, emptyResult, SEADEX_CACHE_TTL); return emptyResult; @@ -95,6 +110,8 @@ export class SeaDexApi { const bestHashes = new Set(); const allHashes = new Set(); + const bestGroups = new Set(); + const allGroups = new Set(); for (const item of items) { const trsArray = item.expand?.trs; @@ -114,13 +131,29 @@ export class SeaDexApi { bestHashes.add(infoHash); } } + + // Collect release groups + for (const torrent of trsArray) { + const releaseGroup = torrent.releaseGroup?.toLowerCase(); + if (!releaseGroup) continue; + + allGroups.add(releaseGroup); + if (torrent.isBest) { + bestGroups.add(releaseGroup); + } + } } logger.info( - `Found ${bestHashes.size} best and ${allHashes.size} total SeaDex hashes for AniList ID ${anilistId}` + `Found ${bestHashes.size} best hashes, ${allHashes.size} total hashes, ${bestGroups.size} best groups, ${allGroups.size} total groups for AniList ID ${anilistId}` ); - const result: SeaDexResult = { bestHashes, allHashes }; + const result: SeaDexResult = { + bestHashes, + allHashes, + bestGroups, + allGroups, + }; await seadexCache.set(cacheKey, result, SEADEX_CACHE_TTL); return result; } catch (error) { @@ -131,6 +164,8 @@ export class SeaDexApi { return { bestHashes: new Set(), allHashes: new Set(), + bestGroups: new Set(), + allGroups: new Set(), }; } }