anilist integrated to my list
This commit is contained in:
@@ -1,27 +1,24 @@
|
||||
// list.service.ts (Actualizado)
|
||||
|
||||
import {queryAll, run} from '../../shared/database';
|
||||
import {queryAll, run, queryOne} from '../../shared/database';
|
||||
import {getExtension} from '../../shared/extensions';
|
||||
import * as animeService from '../anime/anime.service';
|
||||
import * as booksService from '../books/books.service';
|
||||
import * as aniListService from '../anilist/anilist.service';
|
||||
|
||||
// Define la interfaz de entrada de lista (sin external_id)
|
||||
interface ListEntryData {
|
||||
entry_type: any;
|
||||
user_id: number;
|
||||
entry_id: number; // ID de contenido de la fuente (AniList, MAL, o local)
|
||||
source: string; // 'anilist', 'local', etc.
|
||||
status: string; // 'COMPLETED', 'WATCHING', etc.
|
||||
entry_id: number;
|
||||
|
||||
source: string;
|
||||
|
||||
status: string;
|
||||
|
||||
progress: number;
|
||||
score: number | null;
|
||||
}
|
||||
|
||||
const USER_DB = 'userdata';
|
||||
|
||||
/**
|
||||
* Inserta o actualiza una entrada de lista.
|
||||
* Utiliza ON CONFLICT(user_id, entry_id) para el upsert.
|
||||
*/
|
||||
export async function upsertListEntry(entry: any) {
|
||||
const {
|
||||
user_id,
|
||||
@@ -33,6 +30,29 @@ export async function upsertListEntry(entry: any) {
|
||||
score
|
||||
} = entry;
|
||||
|
||||
if (source === 'anilist') {
|
||||
|
||||
const token = await getActiveAccessToken(user_id);
|
||||
|
||||
if (token) {
|
||||
|
||||
try {
|
||||
const result = await aniListService.updateAniListEntry(token, {
|
||||
mediaId: entry_id,
|
||||
status,
|
||||
progress,
|
||||
score
|
||||
});
|
||||
|
||||
return { changes: 0, external: true, anilistResult: result };
|
||||
} catch (err) {
|
||||
console.error("Error actualizando AniList:", err);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const sql = `
|
||||
INSERT INTO ListEntry
|
||||
(user_id, entry_id, source, entry_type, status, progress, score, updated_at)
|
||||
@@ -59,16 +79,13 @@ export async function upsertListEntry(entry: any) {
|
||||
|
||||
try {
|
||||
const result = await run(sql, params, USER_DB);
|
||||
return { changes: result.changes, lastID: result.lastID };
|
||||
return { changes: result.changes, lastID: result.lastID, external: false };
|
||||
} catch (error) {
|
||||
console.error("Error al guardar la entrada de lista:", error);
|
||||
throw new Error("Error en la base de datos al guardar la entrada.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera la lista completa de un usuario.
|
||||
*/
|
||||
export async function getUserList(userId: number): Promise<any> {
|
||||
const sql = `
|
||||
SELECT * FROM ListEntry
|
||||
@@ -77,11 +94,25 @@ export async function getUserList(userId: number): Promise<any> {
|
||||
`;
|
||||
|
||||
try {
|
||||
// 1. Obtener la lista base de la DB
|
||||
const dbList = await queryAll(sql, [userId], USER_DB) as ListEntryData[];
|
||||
|
||||
// 2. Crear un array de promesas para obtener los detalles de cada entrada concurrentemente
|
||||
const enrichedListPromises = dbList.map(async (entry) => {
|
||||
const connected = await isConnected(userId);
|
||||
|
||||
let finalList: ListEntryData[] = [...dbList];
|
||||
|
||||
if (connected) {
|
||||
const token = await getActiveAccessToken(userId);
|
||||
|
||||
const anilistEntries = await aniListService.getUserAniList(userId);
|
||||
|
||||
const localWithoutAnilist = dbList.filter(
|
||||
entry => entry.source !== 'anilist'
|
||||
);
|
||||
|
||||
finalList = [...anilistEntries, ...localWithoutAnilist];
|
||||
}
|
||||
|
||||
const enrichedListPromises = finalList.map(async (entry) => {
|
||||
let contentDetails: any | null = null;
|
||||
const id = entry.entry_id;
|
||||
const source = entry.source;
|
||||
@@ -89,13 +120,12 @@ export async function getUserList(userId: number): Promise<any> {
|
||||
|
||||
try {
|
||||
if (type === 'ANIME') {
|
||||
// Lógica para ANIME
|
||||
let anime: any;
|
||||
|
||||
if (source === 'anilist') {
|
||||
anime = await animeService.getAnimeById(id);
|
||||
} else {
|
||||
const ext = getExtension(source);
|
||||
// Asegurar que id sea una cadena para getAnimeInfoExtension si el id es un número
|
||||
anime = await animeService.getAnimeInfoExtension(ext, id.toString());
|
||||
}
|
||||
|
||||
@@ -106,13 +136,12 @@ export async function getUserList(userId: number): Promise<any> {
|
||||
};
|
||||
|
||||
} else if (type === 'MANGA' || type === 'NOVEL') {
|
||||
// Lógica para MANGA, NOVEL y otros "books"
|
||||
let book: any;
|
||||
|
||||
if (source === 'anilist') {
|
||||
book = await booksService.getBookById(id);
|
||||
} else {
|
||||
const ext = getExtension(source);
|
||||
// Asegurar que id sea una cadena
|
||||
const result = await booksService.getBookInfoExtension(ext, id.toString());
|
||||
book = result || null;
|
||||
}
|
||||
@@ -120,42 +149,33 @@ export async function getUserList(userId: number): Promise<any> {
|
||||
contentDetails = {
|
||||
title: book?.title || 'Unknown Book Title',
|
||||
poster: book?.coverImage?.extraLarge || book?.image || '',
|
||||
// Priorizar chapters, luego volumes * 10, sino 0
|
||||
total_chapters: book?.chapters || book?.volumes * 10 || 0,
|
||||
};
|
||||
}
|
||||
} catch (contentError) {
|
||||
console.error(`Error fetching details for entry ${id} (${source}):`, contentError);
|
||||
|
||||
} catch {
|
||||
contentDetails = {
|
||||
title: 'Error Loading Details',
|
||||
poster: '/public/assets/placeholder.png',
|
||||
poster: 'https://placehold.co/400x600?text=No+Cover',
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Estandarizar y Combinar los datos.
|
||||
|
||||
let finalTitle = contentDetails?.title || 'Unknown Title';
|
||||
let finalPoster = contentDetails?.poster || '/public/assets/placeholder.png';
|
||||
let finalPoster = contentDetails?.poster || 'https://placehold.co/400x600?text=No+Cover';
|
||||
|
||||
// Aplanamiento del título (Necesario para Anilist que devuelve un objeto)
|
||||
if (typeof finalTitle === 'object' && finalTitle !== null) {
|
||||
// Priorizar userPreferred, luego english, luego romaji, sino 'Unknown Title'
|
||||
finalTitle = finalTitle.userPreferred || finalTitle.english || finalTitle.romaji || 'Unknown Title';
|
||||
}
|
||||
|
||||
|
||||
// Retornar el objeto combinado y estandarizado
|
||||
return {
|
||||
...entry,
|
||||
// Datos estandarizados para el frontend:
|
||||
title: finalTitle,
|
||||
poster: finalPoster,
|
||||
total_episodes: contentDetails?.total_episodes, // Será undefined si es Manga/Novel
|
||||
total_chapters: contentDetails?.total_chapters, // Será undefined si es Anime
|
||||
total_episodes: contentDetails?.total_episodes,
|
||||
total_chapters: contentDetails?.total_chapters,
|
||||
};
|
||||
});
|
||||
|
||||
// 4. Ejecutar todas las promesas y esperar el resultado
|
||||
return await Promise.all(enrichedListPromises);
|
||||
|
||||
} catch (error) {
|
||||
@@ -164,10 +184,39 @@ export async function getUserList(userId: number): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina una entrada de lista por user_id y entry_id.
|
||||
*/
|
||||
export async function deleteListEntry(userId: number, entryId: number) {
|
||||
export async function deleteListEntry(userId: number, entryId: string | number) {
|
||||
|
||||
const checkSql = `
|
||||
SELECT source
|
||||
FROM ListEntry
|
||||
WHERE user_id = ? AND entry_id = ?;
|
||||
`;
|
||||
|
||||
const existing = await queryOne(checkSql, [userId, entryId], USER_DB) as any;
|
||||
|
||||
if (existing?.source === 'anilist') {
|
||||
const sql = `
|
||||
SELECT access_token, anilist_user_id
|
||||
FROM UserIntegration
|
||||
WHERE user_id = ? AND platform = 'AniList';
|
||||
`;
|
||||
|
||||
const integration = await queryOne(sql, [userId], USER_DB) as any;
|
||||
|
||||
if (integration?.access_token) {
|
||||
try {
|
||||
await aniListService.deleteAniListEntry(
|
||||
integration.access_token,
|
||||
Number(entryId)
|
||||
);
|
||||
|
||||
return { success: true, external: true };
|
||||
} catch (err) {
|
||||
console.error("Error borrando en AniList:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sql = `
|
||||
DELETE FROM ListEntry
|
||||
WHERE user_id = ? AND entry_id = ?;
|
||||
@@ -175,9 +224,189 @@ export async function deleteListEntry(userId: number, entryId: number) {
|
||||
|
||||
try {
|
||||
const result = await run(sql, [userId, entryId], USER_DB);
|
||||
return { success: result.changes > 0, changes: result.changes };
|
||||
return { success: result.changes > 0, changes: result.changes, external: false };
|
||||
} catch (error) {
|
||||
console.error("Error al eliminar la entrada de lista:", error);
|
||||
throw new Error("Error en la base de datos al eliminar la entrada.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSingleListEntry(
|
||||
userId: number,
|
||||
entryId: string | number,
|
||||
source: string,
|
||||
entryType: string
|
||||
): Promise<any> {
|
||||
|
||||
const connected = await isConnected(userId);
|
||||
|
||||
if (source === 'anilist' && connected) {
|
||||
|
||||
const sql = `
|
||||
SELECT access_token, anilist_user_id
|
||||
FROM UserIntegration
|
||||
WHERE user_id = ? AND platform = 'AniList';
|
||||
`;
|
||||
|
||||
const integration = await queryOne(sql, [userId], USER_DB) as any;
|
||||
|
||||
if (!integration) return null;
|
||||
|
||||
const aniEntry = await aniListService.getSingleAniListEntry(
|
||||
integration.access_token,
|
||||
Number(entryId),
|
||||
entryType as any
|
||||
);
|
||||
|
||||
if (!aniEntry) return null;
|
||||
|
||||
let contentDetails: any = null;
|
||||
|
||||
if (entryType === 'ANIME') {
|
||||
const anime:any = await animeService.getAnimeById(entryId);
|
||||
|
||||
contentDetails = {
|
||||
title: anime?.title,
|
||||
poster: anime?.coverImage?.extraLarge || anime?.image || '',
|
||||
total_episodes: anime?.episodes || anime?.nextAiringEpisode?.episode - 1 || 0,
|
||||
};
|
||||
} else {
|
||||
const book: any = await booksService.getBookById(entryId);
|
||||
|
||||
contentDetails = {
|
||||
title: book?.title,
|
||||
poster: book?.coverImage?.extraLarge || book?.image || '',
|
||||
total_chapters: book?.chapters || book?.volumes * 10 || 0,
|
||||
};
|
||||
}
|
||||
|
||||
let finalTitle = contentDetails?.title || 'Unknown Title';
|
||||
let finalPoster = contentDetails?.poster || 'https://placehold.co/400x600?text=No+Cover';
|
||||
|
||||
if (typeof finalTitle === 'object' && finalTitle !== null) {
|
||||
finalTitle = finalTitle.userPreferred || finalTitle.english || finalTitle.romaji || 'Unknown Title';
|
||||
}
|
||||
|
||||
return {
|
||||
user_id: userId,
|
||||
entry_id: Number(entryId),
|
||||
source: 'anilist',
|
||||
entry_type: entryType,
|
||||
status: aniEntry.status,
|
||||
progress: aniEntry.progress,
|
||||
score: aniEntry.score,
|
||||
title: finalTitle,
|
||||
poster: finalPoster,
|
||||
total_episodes: contentDetails?.total_episodes,
|
||||
total_chapters: contentDetails?.total_chapters,
|
||||
};
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT * FROM ListEntry
|
||||
WHERE user_id = ? AND entry_id = ? AND source = ? AND entry_type = ?;
|
||||
`;
|
||||
|
||||
const dbEntry = await queryAll(sql, [userId, entryId, source, entryType], USER_DB) as ListEntryData[];
|
||||
|
||||
if (!dbEntry || dbEntry.length === 0) return null;
|
||||
|
||||
const entry = dbEntry[0];
|
||||
|
||||
let contentDetails: any | null = null;
|
||||
|
||||
try {
|
||||
if (entryType === 'ANIME') {
|
||||
let anime: any;
|
||||
|
||||
if (source === 'anilist') {
|
||||
anime = await animeService.getAnimeById(entryId);
|
||||
} else {
|
||||
const ext = getExtension(source);
|
||||
anime = await animeService.getAnimeInfoExtension(ext, entryId.toString());
|
||||
}
|
||||
|
||||
contentDetails = {
|
||||
title: anime?.title,
|
||||
poster: anime?.coverImage?.extraLarge || anime?.image || '',
|
||||
total_episodes: anime?.episodes || anime?.nextAiringEpisode?.episode - 1 || 0,
|
||||
};
|
||||
|
||||
} else {
|
||||
let book: any;
|
||||
|
||||
if (source === 'anilist') {
|
||||
book = await booksService.getBookById(entryId);
|
||||
} else {
|
||||
const ext = getExtension(source);
|
||||
book = await booksService.getBookInfoExtension(ext, entryId.toString());
|
||||
}
|
||||
|
||||
contentDetails = {
|
||||
title: book?.title,
|
||||
poster: book?.coverImage?.extraLarge || book?.image || '',
|
||||
total_chapters: book?.chapters || book?.volumes * 10 || 0,
|
||||
};
|
||||
}
|
||||
|
||||
} catch {
|
||||
contentDetails = {
|
||||
title: 'Unknown',
|
||||
poster: 'https://placehold.co/400x600?text=No+Cover',
|
||||
};
|
||||
}
|
||||
|
||||
let finalTitle = contentDetails?.title || 'Unknown Title';
|
||||
let finalPoster = contentDetails?.poster || 'https://placehold.co/400x600?text=No+Cover';
|
||||
|
||||
if (typeof finalTitle === 'object' && finalTitle !== null) {
|
||||
finalTitle = finalTitle.userPreferred || finalTitle.english || finalTitle.romaji || 'Unknown Title';
|
||||
}
|
||||
|
||||
return {
|
||||
...entry,
|
||||
title: finalTitle,
|
||||
poster: finalPoster,
|
||||
total_episodes: contentDetails?.total_episodes,
|
||||
total_chapters: contentDetails?.total_chapters,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getActiveAccessToken(userId: number): Promise<string | null> {
|
||||
const sql = `
|
||||
SELECT access_token, expires_at
|
||||
FROM UserIntegration
|
||||
WHERE user_id = ? AND platform = 'AniList';
|
||||
`;
|
||||
|
||||
try {
|
||||
const integration = await queryOne(sql, [userId], USER_DB) as any | null;
|
||||
|
||||
if (!integration) {
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
const expiryDate = new Date(integration.expires_at);
|
||||
const now = new Date();
|
||||
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
|
||||
if (expiryDate.getTime() < (now.getTime() + fiveMinutes)) {
|
||||
|
||||
console.log(`AniList token for user ${userId} expired or near expiry.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return integration.access_token;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error al verificar la integración de AniList:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isConnected(userId: number): Promise<boolean> {
|
||||
const token = await getActiveAccessToken(userId);
|
||||
return !!token;
|
||||
}
|
||||
Reference in New Issue
Block a user