AbstractThe manga entry type for this source.
The manga detail type for this source.
Get the source identifier for this client.
The source enum value (e.g., MangaSource.Comick).
AbstractsearchSearch for manga using this source's API. Must be implemented by each subclass with source-specific logic.
The search query string.
Optionallimit: numberMaximum number of results to return.
Promise resolving to an array of manga entries.
AbstractgetGet detailed information about a specific manga. Must be implemented by each subclass with source-specific logic.
The manga identifier or slug.
Promise resolving to manga detail or null if not found.
Extract AniList ID from a manga entry by fetching its full details.
The manga entry to extract AniList ID from.
Promise resolving to AniList ID or null if not found.
public async extractAniListId(manga: TMangaEntry): Promise<number | null> {
try {
console.debug(
`[MangaSourceBase] 🔗 ${this.config.name}: Extracting AniList ID for "${manga.title}"`,
);
const detail = await this.getMangaDetail(manga.slug);
if (!detail) {
console.debug(
`[MangaSourceBase] 🔗 No detail data found for ${this.config.name} manga: ${manga.title}`,
);
return null;
}
const anilistId = this.extractAniListIdFromDetail(detail);
if (anilistId) {
console.debug(
`[MangaSourceBase] 🎯 Found AniList ID ${anilistId} for ${this.config.name} manga: ${manga.title}`,
);
return anilistId;
}
console.debug(
`[MangaSourceBase] 🔗 No AniList ID found for ${this.config.name} manga: ${manga.title}`,
);
return null;
} catch (error) {
console.error(
`[MangaSourceBase] ❌ Failed to extract AniList ID for ${this.config.name} manga ${manga.title}:`,
error,
);
return null;
}
}
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,
};
}
Abstract base class for manga source API clients with common functionality. Provides caching, URL building, HTTP requests, and AniList integration for all manga sources.
Source