Get the source identifier for this client.
The source enum value (e.g., MangaSource.Comick).
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,
};
}
Search for manga on Comick API via IPC (to avoid CORS issues). 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 Comick manga entries.
public async searchManga(
query: string,
limit: number = 10,
): Promise<ComickManga[]> {
return withGroupAsync(`[Comick] Search: "${query}"`, async () => {
// Check cache first
const cacheKey = `search:${query.toLowerCase()}:${limit}`;
const cached = this.getCachedData<ComickManga[]>(cacheKey);
if (cached) {
console.debug(`[Comick] 📦 Using cached results for "${query}"`);
return cached;
}
console.info(
`[Comick] 🔍 Searching Comick for: "${query}" (limit: ${limit})`,
);
try {
// Use generic manga source API to call main process via IPC instead of direct fetch (CORS)
const data = (await globalThis.electronAPI.mangaSource.search(
MangaSource.Comick,
query,
limit,
)) as ComickManga[];
const results = this.parseSearchResponse(data);
this.setCachedData(cacheKey, results);
console.info(
`[Comick] ✅ Comick search found ${results?.length || 0} results for "${query}"`,
);
return results;
} catch (error) {
console.error(
`[Comick] ❌ Comick search failed for "${query}":`,
error,
);
return [];
}
});
} /**
Get 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.
public async getMangaDetail(slug: string): Promise<ComickMangaDetail | null> {
return withGroupAsync(`[Comick] Detail: "${slug}"`, async () => {
console.debug(`[Comick] 📖 Getting Comick manga details for: ${slug}`);
try {
// Use generic manga source API to call main process via IPC instead of direct fetch (CORS)
const rawData = await globalThis.electronAPI.mangaSource.getMangaDetail(
MangaSource.Comick,
slug,
);
const detail = this.parseDetailResponse(rawData);
if (detail) {
console.info(`[Comick] ✅ Retrieved details for manga ${slug}`);
}
return detail;
} catch (error) {
console.error(
`[Comick] ❌ Failed to get Comick manga details for ${slug}:`,
error,
);
return null;
}
});
}
Comick-specific manga source client. Extends the base client with Comick-specific API parsing and IPC communication. Uses IPC to avoid CORS issues when calling Comick API from renderer process.
Source