The manga title to search for.
Optional
token: stringOptional authentication token.
Optional search service configuration.
Optional
abortSignal: AbortSignalOptional abort signal to cancel the search.
Optional
specificPage: numberA promise resolving to an array of MangaMatch objects.
export async function searchMangaByTitle(
title: string,
token?: string,
config: Partial<SearchServiceConfig> = {},
abortSignal?: AbortSignal,
specificPage?: number,
): Promise<MangaSearchResponse> {
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
let filteredManga = mangaCache[cacheKey].manga.filter(
(manga) => manga.format !== "NOVEL" && manga.format !== "LIGHT_NOVEL",
);
// For automatic matching, also filter out one-shots if the setting is enabled
const matchConfig = await getMatchConfig();
if (matchConfig.ignoreOneShots) {
const beforeFilter = filteredManga.length;
filteredManga = filteredManga.filter((manga) => !isOneShot(manga));
const afterFilter = filteredManga.length;
if (beforeFilter > afterFilter) {
console.log(
`🚫 Filtered out ${beforeFilter - afterFilter} one-shot(s) from cached results for "${title}"`,
);
}
}
// For automatic matching, also filter out adult content if the setting is enabled
if (matchConfig.ignoreAdultContent) {
const beforeFilter = filteredManga.length;
filteredManga = filteredManga.filter((manga) => !manga.isAdult);
const afterFilter = filteredManga.length;
if (beforeFilter > afterFilter) {
console.log(
`🚫 Filtered out ${beforeFilter - afterFilter} adult content manga from cached results for "${title}"`,
);
}
}
// Always calculate fresh confidence scores, even for cached results
console.log(
`⚖️ Calculating fresh confidence scores for ${filteredManga.length} cached matches`,
);
const matches = filteredManga.map((manga) => {
// Calculate a fresh confidence score using the original search title
const confidence = calculateConfidence(title, manga);
// Calculate title type priority for tie-breaking when confidence scores are equal
const titleTypePriority = calculateTitleTypePriority(manga, title);
console.log(
`⚖️ Cached match confidence for "${manga.title?.english || manga.title?.romaji}": ${confidence}% (priority: ${titleTypePriority})`,
);
return {
manga,
confidence,
titleTypePriority,
};
});
// Sort by confidence first (descending), then by title type priority (descending) for ties
matches.sort((a, b) => {
if (a.confidence !== b.confidence) {
return b.confidence - a.confidence; // Higher confidence first
}
// When confidence is equal, prioritize main titles over synonyms
return b.titleTypePriority - a.titleTypePriority; // Higher priority first
});
// Remove the titleTypePriority from the final results to maintain the original interface
const finalMatches = matches.map(({ manga, confidence }) => ({
manga,
confidence,
}));
return {
matches: finalMatches,
pageInfo: undefined, // No pagination info available for cached results
};
}
} 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 = specificPage || 1; // Use specific page if provided
let hasNextPage = true;
let lastPageInfo: PageInfo | undefined = undefined;
// If a specific page is requested, we only fetch that one page
const singlePageMode = specificPage !== undefined;
// 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 (or just one page if specific page requested)
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
}
// Store the last pageInfo for the response
lastPageInfo = searchResult.Page.pageInfo;
// If we're fetching a specific page, stop after this iteration
if (singlePageMode) {
console.log(
`🔍 Single page mode: Fetched page ${currentPage}, stopping search`,
);
break;
}
// 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,
searchConfig.bypassCache,
);
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
let filteredResults = rankedResults.filter(
(manga) => manga.format !== "NOVEL" && manga.format !== "LIGHT_NOVEL",
);
// For automatic matching (not manual searches), also filter out one-shots if the setting is enabled
if (!searchConfig.bypassCache) {
const matchConfig = await getMatchConfig();
if (matchConfig.ignoreOneShots) {
const beforeFilter = filteredResults.length;
filteredResults = filteredResults.filter((manga) => !isOneShot(manga));
const afterFilter = filteredResults.length;
if (beforeFilter > afterFilter) {
console.log(
`🚫 Filtered out ${beforeFilter - afterFilter} one-shot(s) during automatic matching for "${title}"`,
);
}
}
// For automatic matching, also filter out adult content if the setting is enabled
if (matchConfig.ignoreAdultContent) {
const beforeFilter = filteredResults.length;
filteredResults = filteredResults.filter((manga) => !manga.isAdult);
const afterFilter = filteredResults.length;
if (beforeFilter > afterFilter) {
console.log(
`🚫 Filtered out ${beforeFilter - afterFilter} adult content manga during automatic matching for "${title}"`,
);
}
}
}
// 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`,
);
const matches = finalResults.map((manga) => {
// Calculate a fresh confidence score using the original search title
const confidence = calculateConfidence(
typeof title === "string" ? title : "",
manga,
);
// Calculate title type priority for tie-breaking when confidence scores are equal
const titleTypePriority = calculateTitleTypePriority(
manga,
typeof title === "string" ? title : "",
);
console.log(
`⚖️ Confidence for "${manga.title?.english || manga.title?.romaji}": ${confidence}% (priority: ${titleTypePriority})`,
);
return {
manga,
confidence,
titleTypePriority,
};
});
// Sort by confidence first (descending), then by title type priority (descending) for ties
matches.sort((a, b) => {
if (a.confidence !== b.confidence) {
return b.confidence - a.confidence; // Higher confidence first
}
// When confidence is equal, prioritize main titles over synonyms
return b.titleTypePriority - a.titleTypePriority; // Higher priority first
});
// Remove the titleTypePriority from the final results to maintain the original interface
const finalMatches = matches.map(({ manga, confidence }) => ({
manga,
confidence,
}));
return {
matches: finalMatches,
pageInfo: lastPageInfo,
};
}
Search for manga by title with rate limiting and caching.