246 lines
7.9 KiB
TypeScript
246 lines
7.9 KiB
TypeScript
import { getCache, setCache, getCachedExtension, cacheExtension, getExtensionTitle } from '../../shared/queries';
|
|
import { queryAll, queryOne } from '../../shared/database';
|
|
import {Anime, Episode, Extension, StreamData} from '../types';
|
|
|
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
|
export async function getAnimeById(id: string | number): Promise<Anime | { error: string }> {
|
|
const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [id]);
|
|
|
|
if (!row) {
|
|
return { error: "Anime not found" };
|
|
}
|
|
|
|
return JSON.parse(row.full_data);
|
|
}
|
|
|
|
export async function getTrendingAnime(): Promise<Anime[]> {
|
|
const rows = await queryAll("SELECT full_data FROM trending ORDER BY rank ASC LIMIT 10");
|
|
return rows.map((r: { full_data: string; }) => JSON.parse(r.full_data));
|
|
}
|
|
|
|
export async function getTopAiringAnime(): Promise<Anime[]> {
|
|
const rows = await queryAll("SELECT full_data FROM top_airing ORDER BY rank ASC LIMIT 10");
|
|
return rows.map((r: { full_data: string; }) => JSON.parse(r.full_data));
|
|
}
|
|
|
|
export async function searchAnimeLocal(query: string): Promise<Anime[]> {
|
|
if (!query || query.length < 2) {
|
|
return [];
|
|
}
|
|
|
|
const sql = `SELECT full_data FROM anime WHERE full_data LIKE ? LIMIT 50`;
|
|
const rows = await queryAll(sql, [`%${query}%`]);
|
|
|
|
const results: Anime[] = rows.map((row: { full_data: string; }) => JSON.parse(row.full_data));
|
|
|
|
const cleanResults = results.filter(anime => {
|
|
const q = query.toLowerCase();
|
|
const titles = [
|
|
anime.title.english,
|
|
anime.title.romaji,
|
|
anime.title.native,
|
|
...(anime.synonyms || [])
|
|
].filter(Boolean).map(t => t!.toLowerCase());
|
|
|
|
return titles.some(t => t.includes(q));
|
|
});
|
|
|
|
return cleanResults.slice(0, 10);
|
|
}
|
|
|
|
export async function getAnimeInfoExtension(ext: Extension | null, id: string): Promise<Anime | { error: string }> {
|
|
if (!ext) return { error: "not found" };
|
|
|
|
const extName = ext.constructor.name;
|
|
|
|
const cached = await getCachedExtension(extName, id);
|
|
if (cached) {
|
|
try {
|
|
console.log(`[${extName}] Metadata cache hit for ID: ${id}`);
|
|
return JSON.parse(cached.metadata) as Anime;
|
|
} catch {
|
|
|
|
}
|
|
}
|
|
|
|
if ((ext.type === 'anime-board') && ext.getMetadata) {
|
|
try {
|
|
const match = await ext.getMetadata(id);
|
|
|
|
if (match) {
|
|
|
|
await cacheExtension(extName, id, match.title, match);
|
|
return match;
|
|
}
|
|
} catch (e) {
|
|
console.error(`Extension getMetadata failed:`, e);
|
|
}
|
|
}
|
|
|
|
return { error: "not found" };
|
|
}
|
|
|
|
export async function searchAnimeInExtension(ext: Extension | null, name: string, query: string): Promise<Anime[]> {
|
|
if (!ext) return [];
|
|
|
|
if (ext.type === 'anime-board' && ext.search) {
|
|
try {
|
|
console.log(`[${name}] Searching for anime: ${query}`);
|
|
const matches = await ext.search({
|
|
query: query,
|
|
media: {
|
|
romajiTitle: query,
|
|
englishTitle: query,
|
|
startDate: { year: 0, month: 0, day: 0 }
|
|
}
|
|
});
|
|
|
|
if (matches && matches.length > 0) {
|
|
return matches.map(m => ({
|
|
id: m.id,
|
|
extensionName: name,
|
|
title: { romaji: m.title, english: m.title, native: null },
|
|
coverImage: { large: m.image || '' },
|
|
averageScore: m.rating || m.score || null,
|
|
format: 'ANIME',
|
|
seasonYear: null,
|
|
isExtensionResult: true,
|
|
}));
|
|
}
|
|
} catch (e) {
|
|
console.error(`Extension search failed for ${name}:`, e);
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
export async function searchEpisodesInExtension(ext: Extension | null, name: string, query: string): Promise<Episode[]> {
|
|
if (!ext) return [];
|
|
|
|
const cacheKey = `anime:episodes:${name}:${query}`;
|
|
const cached = await getCache(cacheKey);
|
|
|
|
if (cached) {
|
|
const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS;
|
|
|
|
if (!isExpired) {
|
|
console.log(`[${name}] Episodes cache hit for: ${query}`);
|
|
try {
|
|
return JSON.parse(cached.result) as Episode[];
|
|
} catch (e) {
|
|
console.error(`[${name}] Error parsing cached episodes:`, e);
|
|
}
|
|
} else {
|
|
console.log(`[${name}] Episodes cache expired for: ${query}`);
|
|
}
|
|
}
|
|
|
|
if (ext.type === "anime-board" && ext.search && typeof ext.findEpisodes === "function") {
|
|
try {
|
|
const title = await getExtensionTitle(name, query);
|
|
let mediaId: string;
|
|
|
|
if (title) {
|
|
|
|
const matches = await ext.search({
|
|
query,
|
|
media: {
|
|
romajiTitle: query,
|
|
englishTitle: query,
|
|
startDate: { year: 0, month: 0, day: 0 }
|
|
}
|
|
});
|
|
|
|
if (!matches || matches.length === 0) return [];
|
|
|
|
const res = matches[0];
|
|
if (!res?.id) return [];
|
|
|
|
mediaId = res.id;
|
|
|
|
} else {
|
|
|
|
mediaId = query;
|
|
}
|
|
|
|
const chapterList = await ext.findEpisodes(mediaId);
|
|
|
|
if (!Array.isArray(chapterList)) return [];
|
|
|
|
const result: Episode[] = chapterList.map(ep => ({
|
|
id: ep.id,
|
|
number: ep.number,
|
|
url: ep.url,
|
|
title: ep.title
|
|
}));
|
|
|
|
await setCache(cacheKey, result, CACHE_TTL_MS);
|
|
|
|
return result;
|
|
|
|
} catch (e) {
|
|
console.error(`Extension search failed for ${name}:`, e);
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
export async function getStreamData(extension: Extension, animeData: Anime, episode: string, server?: string, category?: string): Promise<StreamData> {
|
|
const providerName = extension.constructor.name;
|
|
|
|
const cacheKey = `anime:stream:${providerName}:${animeData.id}:${episode}:${server || 'default'}:${category || 'sub'}`;
|
|
|
|
const cached = await getCache(cacheKey);
|
|
|
|
if (cached) {
|
|
const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS;
|
|
|
|
if (!isExpired) {
|
|
console.log(`[${providerName}] Stream data cache hit for episode ${episode}`);
|
|
try {
|
|
return JSON.parse(cached.result) as StreamData;
|
|
} catch (e) {
|
|
console.error(`[${providerName}] Error parsing cached stream data:`, e);
|
|
}
|
|
} else {
|
|
console.log(`[${providerName}] Stream data cache expired for episode ${episode}`);
|
|
}
|
|
}
|
|
|
|
const searchOptions = {
|
|
query: animeData.title.english || animeData.title.romaji,
|
|
dub: category === 'dub',
|
|
media: {
|
|
romajiTitle: animeData.title.romaji,
|
|
englishTitle: animeData.title.english || "",
|
|
startDate: animeData.startDate || { year: 0, month: 0, day: 0 }
|
|
}
|
|
};
|
|
|
|
if (!extension.search || !extension.findEpisodes || !extension.findEpisodeServer) {
|
|
throw new Error("Extension doesn't support required methods");
|
|
}
|
|
|
|
const searchResults = await extension.search(searchOptions);
|
|
|
|
if (!searchResults || searchResults.length === 0) {
|
|
throw new Error("Anime not found on provider");
|
|
}
|
|
|
|
const bestMatch = searchResults[0];
|
|
const episodes = await extension.findEpisodes(bestMatch.id);
|
|
const targetEp = episodes.find(e => e.number === parseInt(episode));
|
|
|
|
if (!targetEp) {
|
|
throw new Error("Episode not found");
|
|
}
|
|
|
|
const serverName = server || "default";
|
|
const streamData = await extension.findEpisodeServer(targetEp, serverName);
|
|
|
|
await setCache(cacheKey, streamData, CACHE_TTL_MS);
|
|
return streamData;
|
|
} |