diff --git a/backend/src/index.js b/backend/src/index.js index 1fa05a3..c02f824 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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; \ No newline at end of file diff --git a/backend/src/routes/manga.js b/backend/src/routes/manga.js new file mode 100644 index 0000000..aeff7f2 --- /dev/null +++ b/backend/src/routes/manga.js @@ -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; diff --git a/backend/src/services/mangaScraper.js b/backend/src/services/mangaScraper.js new file mode 100644 index 0000000..f883211 --- /dev/null +++ b/backend/src/services/mangaScraper.js @@ -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;