• Search for manga by title with rate limiting and caching.

    Parameters

    • title: string

      The manga title to search for.

    • Optionaltoken: string

      Optional authentication token.

    • config: Partial<SearchServiceConfig> = {}

      Optional search service configuration.

    • OptionalabortSignal: AbortSignal

      Optional abort signal to cancel the search.

    Returns Promise<MangaMatch[]>

    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,
    };
    });
    }