The manga title to search for.
Optional
token: stringOptional authentication token.
Optional search service configuration.
Optional
abortSignal: AbortSignalOptional abort signal to cancel the search.
A promise resolving to an array of MangaMatch objects.
export async function searchMangaByTitle(
title: string,
token?: string,
config: Partial<SearchServiceConfig> = {},
abortSignal?: AbortSignal,
): Promise<MangaMatch[]> {
const searchConfig = { ...DEFAULT_SEARCH_CONFIG, ...config };
// Generate cache key for this title
const cacheKey = generateCacheKey(title);
// If bypassing cache, explicitly clear any existing cache for this title
if (searchConfig.bypassCache && cacheKey) {
console.log(`🔥 Fresh search: Explicitly clearing cache for "${title}"`);
// Check if we have this title in cache first
if (mangaCache[cacheKey]) {
delete mangaCache[cacheKey];
console.log(`🧹 Removed existing cache entry for "${title}"`);
// Also save the updated cache to persist the removal
saveCache();
} else {
console.log(`🔍 No existing cache entry found for "${title}" to clear`);
}
} else if (!searchConfig.bypassCache) {
// Check cache first (existing logic - only if not bypassing)
if (isCacheValid(cacheKey)) {
console.log(`Using cache for ${title}`);
// Filter out Light Novels from cache results
const filteredManga = mangaCache[cacheKey].manga.filter(
(manga) => manga.format !== "NOVEL" && manga.format !== "LIGHT_NOVEL",
);
// Always calculate fresh confidence scores, even for cached results
console.log(
`⚖️ Calculating fresh confidence scores for ${filteredManga.length} cached matches`,
);
return filteredManga.map((manga) => {
// Calculate a fresh confidence score using the original search title
const confidence = calculateConfidence(title, manga);
console.log(
`⚖️ Cached match confidence for "${manga.title?.english || manga.title?.romaji}": ${confidence}%`,
);
return {
manga,
confidence,
};
});
}
} else {
console.log(
`🚨 FORCE SEARCH: Bypassing cache for "${title}" - will query AniList API directly`,
);
// For manual searches, ensure we're not too strict with exact matching
if (searchConfig.exactMatchingOnly) {
console.log(
`🔍 MANUAL SEARCH: Ensuring exact matching is correctly configured`,
);
searchConfig.exactMatchingOnly = true; // Keep it true, but we've enhanced the matching logic
}
}
const searchQuery = title;
// Now we need to use the API - wait for our turn in the rate limiting queue
await acquireRateLimit();
// Initialize search variables
let results: AniListManga[] = [];
let currentPage = 1;
let hasNextPage = true;
// Add debug log to show we're making a network request
console.log(
`🌐 Making network request to AniList API for "${title}" - bypassCache=${searchConfig.bypassCache}`,
);
// Search until we have enough results or there are no more pages
while (hasNextPage && results.length < searchConfig.maxSearchResults) {
try {
// Check if aborted before searching
if (abortSignal && abortSignal.aborted) {
throw new Error("Search aborted by abort signal");
}
let searchResult: SearchResult<AniListManga>;
console.log(
`🔍 Searching for "${searchQuery}" (page ${currentPage}, bypassCache=${searchConfig.bypassCache ? "true" : "false"})`,
);
if (searchConfig.useAdvancedSearch) {
searchResult = await advancedSearchWithRateLimit(
searchQuery,
{}, // No filters for initial search
currentPage,
searchConfig.searchPerPage,
token,
false, // Don't acquire rate limit again, we already did
// Pass bypassCache flag to search functions
0,
searchConfig.bypassCache,
);
} else {
searchResult = await searchWithRateLimit(
searchQuery,
currentPage,
searchConfig.searchPerPage,
token,
false, // Don't acquire rate limit again, we already did
0,
searchConfig.bypassCache,
);
}
console.log(
`🔍 Search response for "${searchQuery}" page ${currentPage}: ${searchResult?.Page?.media?.length || 0} results`,
);
// If doing a manual search, log the actual titles received for debugging
if (searchConfig.bypassCache && searchResult?.Page?.media?.length > 0) {
console.log(
`🔍 Titles received from API:`,
searchResult.Page.media.map((m) => ({
id: m.id,
romaji: m.title?.romaji,
english: m.title?.english,
native: m.title?.native,
synonyms: m.synonyms?.length,
})),
);
}
// Validate the search result structure
if (!searchResult || !searchResult.Page) {
console.error(`Invalid search result for "${title}":`, searchResult);
break; // Exit the loop but continue with whatever results we have
}
// Validate that media array exists
if (!searchResult.Page.media) {
console.error(
`Search result for "${title}" missing media array:`,
searchResult,
);
searchResult.Page.media = []; // Provide empty array to prevent errors
}
// Add results
results = [...results, ...searchResult.Page.media];
// Validate pageInfo exists
if (!searchResult.Page.pageInfo) {
console.error(
`Search result for "${title}" missing pageInfo:`,
searchResult,
);
break; // Exit the loop but continue with whatever results we have
}
// Check if there are more pages
hasNextPage =
searchResult.Page.pageInfo.hasNextPage &&
currentPage < searchResult.Page.pageInfo.lastPage &&
results.length < searchConfig.maxSearchResults;
currentPage++;
// If we need to fetch another page, wait for rate limit again
if (hasNextPage) {
await acquireRateLimit();
}
} catch (error: unknown) {
// Log the error with its details to show it's being used
if (error instanceof Error) {
console.error(
`Error searching for manga "${searchQuery}": ${error.message}`,
error,
);
} else {
console.error(`Error searching for manga "${searchQuery}"`, error);
}
break; // Break out of the loop, but continue with whatever results we have
}
}
console.log(
`🔍 Found ${results.length} raw results for "${title}" before filtering/ranking`,
);
// For manual searches, always ensure we show results
let exactMatchMode = searchConfig.exactMatchingOnly;
// If this is a manual search or we have few results, be more lenient
if ((searchConfig.bypassCache && results.length > 0) || results.length <= 3) {
console.log(
`🔍 Using enhanced title matching to ensure results are displayed`,
);
exactMatchMode = false; // Don't be too strict with manual searches
}
// Filter and rank results by match quality with modified exact matching behavior
const rankedResults = rankMangaResults(results, title, exactMatchMode);
console.log(
`🔍 Search complete for "${title}": Found ${results.length} results, ranked to ${rankedResults.length} relevant matches`,
);
// Only cache the results if we're not bypassing cache
if (!searchConfig.bypassCache) {
// Cache the results
const cacheKey = generateCacheKey(title);
mangaCache[cacheKey] = {
manga: rankedResults,
timestamp: Date.now(),
};
// Save the updated cache to localStorage
saveCache();
} else {
console.log(`🔍 MANUAL SEARCH: Skipping cache save for "${title}"`);
}
// Filter out any Light Novels before returning results
const filteredResults = rankedResults.filter(
(manga) => manga.format !== "NOVEL" && manga.format !== "LIGHT_NOVEL",
);
// If after all filtering we have no results but the API returned some,
// include at least the first API result regardless of score
let finalResults = filteredResults;
if (filteredResults.length === 0 && results.length > 0) {
console.log(
`⚠️ No matches passed filtering, but including raw API results anyway`,
);
// Include first few results from the API as low-confidence matches
finalResults = results
.slice(0, 3)
.filter(
(manga) => manga.format !== "NOVEL" && manga.format !== "LIGHT_NOVEL",
);
// Log what we're including
console.log(
`🔍 Including these API results:`,
finalResults.map((m) => ({
id: m.id,
romaji: m.title?.romaji,
english: m.title?.english,
})),
);
}
console.log(`🔍 Final result count: ${finalResults.length} manga`);
// For manual searches with no results but API had results, always include the API results
if (
searchConfig.bypassCache &&
finalResults.length === 0 &&
results.length > 0
) {
console.log(
`⚠️ MANUAL SEARCH with no ranked results - forcing inclusion of API results`,
);
finalResults = results.filter(
(manga) => manga.format !== "NOVEL" && manga.format !== "LIGHT_NOVEL",
);
}
// Always calculate fresh confidence scores, even on cached results
console.log(
`⚖️ Calculating fresh confidence scores for ${finalResults.length} matches`,
);
return finalResults.map((manga) => {
// Calculate a fresh confidence score using the original search title
const confidence = calculateConfidence(
typeof title === "string" ? title : "",
manga,
);
console.log(
`⚖️ Confidence for "${manga.title?.english || manga.title?.romaji}": ${confidence}%`,
);
return {
manga,
confidence,
};
});
}
Search for manga by title with rate limiting and caching.