feat(seadex): add release group fallback matching (#521)

Co-authored-by: Viren070 <viren070@protonmail.com>
This commit is contained in:
cats
2025-12-01 18:14:39 +03:00
committed by GitHub
parent f7c1124ebc
commit 7ed51bc6f0
6 changed files with 83 additions and 26 deletions
+1 -3
View File
@@ -82,9 +82,7 @@ const SizeFilter = z.object({
// max: z.number().min(1).optional(), // max: z.number().min(1).optional(),
// }) // })
.optional(), .optional(),
anime: z anime: z.tuple([z.number().min(0), z.number().min(0)]).optional(),
.tuple([z.number().min(0), z.number().min(0)])
.optional(),
}); });
const SizeFilterOptions = z.object({ const SizeFilterOptions = z.object({
+1 -1
View File
@@ -326,7 +326,7 @@ export abstract class BaseFormatter {
network: stream.parsedFile?.network || null, network: stream.parsedFile?.network || null,
container: stream.parsedFile?.container || null, container: stream.parsedFile?.container || null,
extension: stream.parsedFile?.extension || null, extension: stream.parsedFile?.extension || null,
seadex: (stream.seadex?.isSeadex && !stream.seadex?.isBest) ?? false, seadex: stream.seadex?.isSeadex ?? false,
seadexBest: stream.seadex?.isBest ?? false, seadexBest: stream.seadex?.isBest ?? false,
}, },
addon: { addon: {
+1 -1
View File
@@ -507,7 +507,7 @@ export abstract class StreamExpressionEngine {
return streams.filter((stream) => stream.seadex?.isBest === true); 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); return streams.filter((stream) => stream.seadex?.isSeadex === true);
}; };
+40 -16
View File
@@ -40,7 +40,8 @@ class StreamPrecomputer {
/** /**
* Precompute SeaDex status for anime streams * 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( private async precomputeSeaDex(
streams: ParsedStream[], streams: ParsedStream[],
@@ -80,39 +81,62 @@ class StreamPrecomputer {
if ( if (
seadexResult.bestHashes.size === 0 && 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}`); logger.debug(`No SeaDex releases found for AniList ID ${anilistId}`);
return; return;
} }
let seadexBestCount = 0; let seadexBestCount = 0;
let seadexCount = 0; let seadexCount = 0;
let seadexGroupFallbackCount = 0;
for (const stream of streams) { for (const stream of streams) {
const infoHash = stream.torrent?.infoHash?.toLowerCase(); 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); // Fallback to release group matching
const isSeadex = seadexResult.allHashes.has(infoHash); const releaseGroup = stream.parsedFile?.releaseGroup?.toLowerCase();
if (releaseGroup) {
const isBestGroup = seadexResult.bestGroups.has(releaseGroup);
const isSeadexGroup = seadexResult.allGroups.has(releaseGroup);
if (isSeadex) { if (isBestGroup || isSeadexGroup) {
stream.seadex = { stream.seadex = {
isBest, isBest: isBestGroup,
isSeadex: true, isSeadex: true,
}; };
if (isBestGroup) {
if (isBest) { seadexBestCount++;
seadexBestCount++; }
seadexCount++;
seadexGroupFallbackCount++;
} }
seadexCount++;
} }
} }
if (seadexCount > 0) { if (seadexCount > 0) {
logger.info( 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}`
); );
} }
} }
+2 -2
View File
@@ -1082,9 +1082,9 @@ export const SORT_CRITERIA_DETAILS: Record<
description: description:
'Whether the stream is a SeaDex release (curated best anime releases from releases.moe)', 'Whether the stream is a SeaDex release (curated best anime releases from releases.moe)',
ascendingDescription: ascendingDescription:
'Non-SeaDex releases are preferred over SeaDex releases', 'Streams that are not listed on SeaDex are preferred',
descendingDescription: 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; } as const;
+38 -3
View File
@@ -19,11 +19,19 @@ const seadexCache = Cache.getInstance<string, SeaDexResult>(
export interface SeaDexResult { export interface SeaDexResult {
bestHashes: Set<string>; bestHashes: Set<string>;
allHashes: Set<string>; allHashes: Set<string>;
bestGroups: Set<string>;
allGroups: Set<string>;
}
export interface SeaDexTagResult {
isBest: boolean;
isSeadex: boolean;
} }
interface SeaDexTorrent { interface SeaDexTorrent {
infoHash: string; infoHash: string;
isBest: boolean; isBest: boolean;
releaseGroup?: string;
} }
interface SeaDexEntry { interface SeaDexEntry {
@@ -77,7 +85,12 @@ export class SeaDexApi {
logger.warn( logger.warn(
`SeaDex API returned ${response.status} for AniList ID ${anilistId}` `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; const data = (await response.json()) as SeaDexResponse;
@@ -88,6 +101,8 @@ export class SeaDexApi {
const emptyResult: SeaDexResult = { const emptyResult: SeaDexResult = {
bestHashes: new Set(), bestHashes: new Set(),
allHashes: new Set(), allHashes: new Set(),
bestGroups: new Set(),
allGroups: new Set(),
}; };
await seadexCache.set(cacheKey, emptyResult, SEADEX_CACHE_TTL); await seadexCache.set(cacheKey, emptyResult, SEADEX_CACHE_TTL);
return emptyResult; return emptyResult;
@@ -95,6 +110,8 @@ export class SeaDexApi {
const bestHashes = new Set<string>(); const bestHashes = new Set<string>();
const allHashes = new Set<string>(); const allHashes = new Set<string>();
const bestGroups = new Set<string>();
const allGroups = new Set<string>();
for (const item of items) { for (const item of items) {
const trsArray = item.expand?.trs; const trsArray = item.expand?.trs;
@@ -114,13 +131,29 @@ export class SeaDexApi {
bestHashes.add(infoHash); 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( 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); await seadexCache.set(cacheKey, result, SEADEX_CACHE_TTL);
return result; return result;
} catch (error) { } catch (error) {
@@ -131,6 +164,8 @@ export class SeaDexApi {
return { return {
bestHashes: new Set(), bestHashes: new Set(),
allHashes: new Set(), allHashes: new Set(),
bestGroups: new Set(),
allGroups: new Set(),
}; };
} }
} }