added my lists page

This commit is contained in:
2025-12-06 14:28:40 +01:00
parent 9832384627
commit e5ec8aa7e5
21 changed files with 2400 additions and 44 deletions

View File

@@ -0,0 +1,183 @@
// list.service.ts (Actualizado)
import {queryAll, run} from '../../shared/database';
import {getExtension} from '../../shared/extensions';
import * as animeService from '../anime/anime.service';
import * as booksService from '../books/books.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.
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,
entry_id,
source,
entry_type,
status,
progress,
score
} = entry;
const sql = `
INSERT INTO ListEntry
(user_id, entry_id, source, entry_type, status, progress, score, updated_at)
VALUES
(?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(user_id, entry_id) DO UPDATE SET
source = EXCLUDED.source,
entry_type = EXCLUDED.entry_type,
status = EXCLUDED.status,
progress = EXCLUDED.progress,
score = EXCLUDED.score,
updated_at = CURRENT_TIMESTAMP;
`;
const params = [
user_id,
entry_id,
source,
entry_type,
status,
progress,
score || null
];
try {
const result = await run(sql, params, USER_DB);
return { changes: result.changes, lastID: result.lastID };
} 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
WHERE user_id = ?
ORDER BY updated_at DESC;
`;
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) => {
let contentDetails: any | null = null;
const id = entry.entry_id;
const source = entry.source;
const type = entry.entry_type;
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());
}
contentDetails = {
title: anime?.title || 'Unknown Anime Title',
poster: anime?.coverImage?.extraLarge || anime?.image || '',
total_episodes: anime?.episodes || anime?.nextAiringEpisode?.episode - 1 || 0,
};
} 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;
}
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);
contentDetails = {
title: 'Error Loading Details',
poster: '/public/assets/placeholder.png',
};
}
// 3. Estandarizar y Combinar los datos.
let finalTitle = contentDetails?.title || 'Unknown Title';
let finalPoster = contentDetails?.poster || '/public/assets/placeholder.png';
// 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
};
});
// 4. Ejecutar todas las promesas y esperar el resultado
return await Promise.all(enrichedListPromises);
} catch (error) {
console.error("Error al obtener la lista del usuario:", error);
throw new Error("Error en la base de datos al obtener la lista.");
}
}
/**
* Elimina una entrada de lista por user_id y entry_id.
*/
export async function deleteListEntry(userId: number, entryId: number) {
const sql = `
DELETE FROM ListEntry
WHERE user_id = ? AND entry_id = ?;
`;
try {
const result = await run(sql, [userId, entryId], USER_DB);
return { success: result.changes > 0, changes: result.changes };
} 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.");
}
}