Get the source identifier for this client.
The source enum value (e.g., MangaSource.Comick).
Search for manga on this source and enrich results with AniList data. Fetches source results, extracts AniList IDs, and merges AniList data.
The search query string.
AniList OAuth access token for fetching manga details.
Maximum number of results to return (default: 1).
Promise resolving to enhanced AniList manga entries with source info.
public async searchAndGetAniListManga(
query: string,
accessToken: string,
limit: number = 1,
): Promise<EnhancedAniListManga[]> {
try {
console.info(
`[MangaSourceBase] 🔍 Starting ${this.config.name} search for "${query}" with limit ${limit}`,
);
// Search on this source
const sourceResults = await this.searchManga(query, limit);
if (!sourceResults?.length) {
console.debug(
`[MangaSourceBase] 📦 No ${this.config.name} results found for "${query}"`,
);
return [];
}
console.debug(
`[MangaSourceBase] 📦 Found ${sourceResults.length} ${this.config.name} results, extracting AniList IDs...`,
);
const anilistIds: number[] = [];
const sourceMap = new Map<number, TMangaEntry>();
for (const sourceManga of sourceResults) {
const anilistId = await this.extractAniListId(sourceManga);
if (!anilistId) continue;
anilistIds.push(anilistId);
sourceMap.set(anilistId, sourceManga);
}
if (!anilistIds.length) {
console.debug(
`[MangaSourceBase] 🔗 No AniList links found in ${this.config.name} results for "${query}"`,
);
return [];
}
console.info(
`[MangaSourceBase] 🎯 Found ${anilistIds.length} AniList IDs from ${this.config.name}: [${anilistIds.join(", ")}]`,
);
const anilistManga = await getMangaByIds(anilistIds, accessToken);
if (!anilistManga?.length) {
console.warn(
`[MangaSourceBase] ❌ Failed to fetch AniList manga for IDs: [${anilistIds.join(", ")}]`,
);
return [];
}
// Enhance AniList manga with source info
const enhancedManga: EnhancedAniListManga[] = anilistManga.map(
(manga) => {
const sourceInfo = sourceMap.get(manga.id);
return {
...manga,
sourceInfo: sourceInfo
? {
title: sourceInfo.title,
slug: sourceInfo.slug,
sourceId: sourceInfo.id,
source: this.config.source,
isFoundViaAlternativeSearch: true,
}
: undefined,
};
},
);
console.info(
`[MangaSourceBase] ✅ Successfully enhanced ${enhancedManga.length} AniList manga with ${this.config.name} source info`,
);
return enhancedManga;
} catch (error) {
console.error(
`[MangaSourceBase] ❌ ${this.config.name} search and AniList fetch failed for "${query}":`,
error,
);
return [];
}
}
Clear cache entries for specific search queries. Removes all cache entries matching the query pattern across different limits.
Array of search queries to clear from cache.
Number of cache entries cleared.
public clearCache(queries: string[]): number {
let clearedCount = 0;
for (const query of queries) {
// Clear cache entries matching query (different limits may exist)
const keysToDelete = Object.keys(this.cache).filter((key) =>
key.startsWith(`search:${query.toLowerCase()}:`),
);
for (const key of keysToDelete) {
delete this.cache[key];
clearedCount++;
}
}
console.info(
`[MangaSourceBase] 🧹 Cleared ${clearedCount} ${this.config.name} cache entries`,
);
return clearedCount;
}
Get cache status and statistics for debugging.
Object with total entries, active entries, and expired entries counts.
public getCacheStatus() {
const totalEntries = Object.keys(this.cache).length;
const expiredEntries = Object.keys(this.cache).filter(
(key) => Date.now() - this.cache[key].timestamp > this.cacheExpiryMs,
).length;
return {
source: this.config.name,
totalEntries,
activeEntries: totalEntries - expiredEntries,
expiredEntries,
};
}
Search for manga on MangaDex API. Results are cached for 30 minutes by default.
The search query string.
Maximum number of results to return (default: 10).
Promise resolving to array of MangaDex manga entries.
public async searchManga(
query: string,
limit: number = 10,
): Promise<MangaDexManga[]> {
return withGroupAsync(`[MangaDex] Search: "${query}"`, async () => {
// Check cache first
const cacheKey = `search:${query.toLowerCase()}:${limit}`;
const cached = this.getCachedData<MangaDexManga[]>(cacheKey);
if (cached) {
console.debug(`[MangaDex] 📦 Using cached results for "${query}"`);
return cached;
}
console.info(
`[MangaDex] 🔍 Searching MangaDex for: "${query}" (limit: ${limit})`,
);
try {
// Make direct HTTP request using the base client's functionality
const url = this.buildSearchUrl(query, limit);
const data = await this.makeRequest(url);
const results = this.parseSearchResponse(data);
this.setCachedData(cacheKey, results);
console.info(
`[MangaDex] ✅ MangaDex search found ${results?.length || 0} results for "${query}"`,
);
return results;
} catch (error) {
console.error(
`[MangaDex] ❌ MangaDex search failed for "${query}":`,
error,
);
return [];
}
});
}
Get detailed information about a specific MangaDex manga.
The MangaDex manga ID.
Promise resolving to manga detail or null if not found.
public async getMangaDetail(
slug: string,
): Promise<MangaDexMangaDetail | null> {
return withGroupAsync(`[MangaDex] Detail: "${slug}"`, async () => {
try {
console.debug(
`[MangaDex] 📖 Getting MangaDex manga details for: ${slug}`,
);
// Make direct HTTP request using the base client's functionality
const url = this.buildDetailUrl(slug);
const rawData = await this.makeRequest(url);
const detail = this.parseDetailResponse(rawData);
if (detail) {
console.info(`[MangaDex] ✅ Retrieved details for manga ${slug}`);
}
return detail;
} catch (error) {
console.error(
`[MangaDex] ❌ Failed to get MangaDex manga details for ${slug}:`,
error,
);
return null;
}
});
}
Extract AniList ID from a MangaDex manga entry.
Prefer using the existing links on the lightweight MangaDexManga object
and fall back to fetching detail data if required.
The MangaDex manga entry.
AniList ID number or null.
MangaDex-specific manga source client. Extends the base client with MangaDex-specific API parsing and URL building. Makes direct HTTP requests (CORS-enabled by MangaDex API).
Source