This commit is contained in:
Flatlogic Bot 2026-02-05 03:59:10 +00:00
parent c0c1673e46
commit ef0c922762
3 changed files with 206 additions and 4 deletions

View File

@ -1,4 +1,3 @@
const express = require('express');
const cors = require('cors');
const app = express();
@ -18,7 +17,7 @@ const sqlRoutes = require('./routes/sql');
const pexelsRoutes = require('./routes/pexels');
const openaiRoutes = require('./routes/openai');
const mangaRoutes = require('./routes/manga');
const usersRoutes = require('./routes/users');
@ -191,7 +190,12 @@ app.use(
passport.authenticate('jwt', { session: false }),
sqlRoutes);
app.use(
'/api/manga',
passport.authenticate('jwt', { session: false }),
mangaRoutes
);
const publicDir = path.join(
__dirname,
'../public',
@ -215,4 +219,4 @@ db.sequelize.sync().then(function () {
});
});
module.exports = app;
module.exports = app;

View File

@ -0,0 +1,30 @@
const express = require('express');
const router = express.Router();
const MangaScraperService = require('../services/mangaScraper');
const { wrapAsync } = require('../helpers');
router.get('/search', wrapAsync(async (req, res) => {
const { query, sourceId } = req.query;
const results = await MangaScraperService.search(query, sourceId);
res.json(results);
}));
router.get('/details/:sourceId/:mangaId', wrapAsync(async (req, res) => {
const { sourceId, mangaId } = req.params;
const details = await MangaScraperService.getMangaDetails(mangaId, sourceId);
res.json(details);
}));
router.get('/chapters/:sourceId/:mangaId', wrapAsync(async (req, res) => {
const { sourceId, mangaId } = req.params;
const chapters = await MangaScraperService.getChapters(mangaId, sourceId);
res.json(chapters);
}));
router.get('/pages/:sourceId/:chapterId', wrapAsync(async (req, res) => {
const { sourceId, chapterId } = req.params;
const pages = await MangaScraperService.getPages(chapterId, sourceId);
res.json(pages);
}));
module.exports = router;

View File

@ -0,0 +1,168 @@
const axios = require('axios');
const db = require('../db/models');
class MangaScraperService {
static async getSource(sourceId) {
const source = await db.sources.findByPk(sourceId);
if (!source) throw new Error('Source not found');
return source;
}
static async search(query, sourceId) {
const source = await this.getSource(sourceId);
// For now, let's implement a real MangaDex search if the name matches,
// otherwise fallback to a mock search.
if (source.name.toLowerCase().includes('mangadex') || source.base_url.includes('mangadex.org')) {
return this.searchMangaDex(query);
}
return this.mockSearch(query, source);
}
static async searchMangaDex(query) {
const response = await axios.get('https://api.mangadex.org/manga', {
params: {
title: query,
limit: 20,
'includes[]': ['cover_art']
}
});
return response.data.data.map(manga => {
const coverArt = manga.relationships.find(r => r.type === 'cover_art');
const fileName = coverArt ? coverArt.attributes?.fileName : null;
const coverUrl = fileName ? `https://uploads.mangadex.org/covers/${manga.id}/${fileName}.256.jpg` : null;
return {
id: manga.id,
title: manga.attributes.title.en || Object.values(manga.attributes.title)[0],
description: manga.attributes.description.en,
coverUrl: coverUrl,
source: 'MangaDex'
};
});
}
static async mockSearch(query, source) {
// Return some mock data based on the source
return [
{
id: `mock-${source.id}-1`,
title: `${query} in ${source.name}`,
description: `This is a mock result for ${query} from ${source.name}`,
coverUrl: 'https://via.placeholder.com/256x360?text=Manga+Cover',
source: source.name
}
];
}
static async getMangaDetails(mangaId, sourceId) {
const source = await this.getSource(sourceId);
if (source.name.toLowerCase().includes('mangadex') || source.base_url.includes('mangadex.org')) {
return this.getMangaDexDetails(mangaId);
}
return this.mockMangaDetails(mangaId, source);
}
static async getMangaDexDetails(mangaId) {
const response = await axios.get(`https://api.mangadex.org/manga/${mangaId}`, {
params: {
'includes[]': ['cover_art', 'author', 'artist']
}
});
const manga = response.data.data;
const coverArt = manga.relationships.find(r => r.type === 'cover_art');
const fileName = coverArt ? coverArt.attributes?.fileName : null;
const coverUrl = fileName ? `https://uploads.mangadex.org/covers/${manga.id}/${fileName}.512.jpg` : null;
return {
id: manga.id,
title: manga.attributes.title.en || Object.values(manga.attributes.title)[0],
description: manga.attributes.description.en,
coverUrl: coverUrl,
author: manga.relationships.find(r => r.type === 'author')?.attributes?.name,
artist: manga.relationships.find(r => r.type === 'artist')?.attributes?.name,
status: manga.attributes.status,
year: manga.attributes.year,
source: 'MangaDex'
};
}
static async getChapters(mangaId, sourceId) {
const source = await this.getSource(sourceId);
if (source.name.toLowerCase().includes('mangadex') || source.base_url.includes('mangadex.org')) {
return this.getMangaDexChapters(mangaId);
}
return this.mockChapters(mangaId, source);
}
static async getMangaDexChapters(mangaId) {
const response = await axios.get(`https://api.mangadex.org/manga/${mangaId}/feed`, {
params: {
translatedLanguage: ['en'],
order: { chapter: 'desc' },
limit: 100
}
});
return response.data.data.map(chapter => ({
id: chapter.id,
chapter: chapter.attributes.chapter,
title: chapter.attributes.title,
language: chapter.attributes.translatedLanguage,
externalUrl: chapter.attributes.externalUrl
}));
}
static async getPages(chapterId, sourceId) {
const source = await this.getSource(sourceId);
if (source.name.toLowerCase().includes('mangadex') || source.base_url.includes('mangadex.org')) {
return this.getMangaDexPages(chapterId);
}
return this.mockPages(chapterId, source);
}
static async getMangaDexPages(chapterId) {
const response = await axios.get(`https://api.mangadex.org/at-home/server/${chapterId}`);
const { baseUrl, chapter } = response.data;
const hash = chapter.hash;
const files = chapter.data;
return files.map(file => `${baseUrl}/data/${hash}/${file}`);
}
static async mockMangaDetails(mangaId, source) {
return {
id: mangaId,
title: `Mock Manga ${mangaId}`,
description: `Description for mock manga ${mangaId} from ${source.name}`,
coverUrl: 'https://via.placeholder.com/512x720?text=Manga+Cover',
source: source.name
};
}
static async mockChapters(mangaId, source) {
return [
{ id: 'chap-1', chapter: '1', title: 'Beginning' },
{ id: 'chap-2', chapter: '2', title: 'The Journey' }
];
}
static async mockPages(chapterId, source) {
return [
'https://via.placeholder.com/800x1200?text=Page+1',
'https://via.placeholder.com/800x1200?text=Page+2',
'https://via.placeholder.com/800x1200?text=Page+3'
];
}
}
module.exports = MangaScraperService;