ani-auto 1.4.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/anime/LICENSE +21 -0
- package/anime/api/index.js +110 -0
- package/anime/app.js +79 -0
- package/anime/controllers/animeInfoController.js +48 -0
- package/anime/controllers/animeListController.js +21 -0
- package/anime/controllers/homeController.js +42 -0
- package/anime/controllers/playController.js +140 -0
- package/anime/controllers/queueController.js +20 -0
- package/anime/controllers/testController.js +667 -0
- package/anime/index.js +139 -0
- package/anime/middleware/cache.js +64 -0
- package/anime/middleware/errorHandler.js +33 -0
- package/anime/middleware/rateLimiter.js +73 -0
- package/anime/models/animeInfoModel.js +193 -0
- package/anime/models/animeListModel.js +69 -0
- package/anime/models/homeModel.js +33 -0
- package/anime/models/playModel.js +528 -0
- package/anime/models/queueModel.js +22 -0
- package/anime/package.json +27 -0
- package/anime/pnpm-lock.yaml +1774 -0
- package/anime/readme.md +236 -0
- package/anime/routes/animeInfoRoutes.js +9 -0
- package/anime/routes/animeListRoutes.js +9 -0
- package/anime/routes/homeRoutes.js +10 -0
- package/anime/routes/playRoutes.js +11 -0
- package/anime/routes/queueRoutes.js +8 -0
- package/anime/routes/testRoutes.js +325 -0
- package/anime/routes/webhookRoutes.js +23 -0
- package/anime/scrapers/animepahe.js +1053 -0
- package/anime/test-server.js +10 -0
- package/anime/test.sh +275 -0
- package/anime/utils/browser.js +172 -0
- package/anime/utils/config.js +209 -0
- package/anime/utils/dataProcessor.js +172 -0
- package/anime/utils/diskCache.js +228 -0
- package/anime/utils/flaresolverr.js +361 -0
- package/anime/utils/jsParser.js +19 -0
- package/anime/utils/redis.js +64 -0
- package/anime/utils/requestManager.js +706 -0
- package/anime/utils/urlConverter.js +88 -0
- package/anime/utils/webhookNotifier.js +190 -0
- package/cookiereader.py +291 -0
- package/package.json +14 -6
- package/src/anilist.js +19 -2
- package/src/api/anilist.js +32 -0
- package/src/api/anime.js +181 -0
- package/src/api/cf-bypass.js +131 -0
- package/src/api/config.js +134 -0
- package/src/api/daemon.js +62 -0
- package/src/api/download.js +98 -0
- package/src/api/match.js +58 -0
- package/src/api/runs.js +15 -0
- package/src/api/update.js +96 -0
- package/src/cli.js +18 -2
- package/src/config.js +14 -2
- package/src/daemon.js +1 -1
- package/src/db.js +38 -11
- package/src/engine.js +66 -13
- package/src/notify.js +82 -7
- package/src/scraper.js +2 -2
- package/src/server.js +108 -0
- package/utils.js +21 -21
- package/web/index.html +13 -0
- package/web/package-lock.json +1420 -0
- package/web/package.json +24 -0
- package/web/src/App.vue +200 -0
- package/web/src/api/client.js +75 -0
- package/web/src/assets/styles/base.css +961 -0
- package/web/src/components/UpdateModal.vue +157 -0
- package/web/src/components/sidebar/SidebarResizable.vue +170 -0
- package/web/src/components/sidebar/sidebarConfig.js +24 -0
- package/web/src/main.js +104 -0
- package/web/src/router/index.js +44 -0
- package/web/src/stores/config.js +27 -0
- package/web/src/stores/download.js +107 -0
- package/web/src/stores/update.js +73 -0
- package/web/src/views/About.vue +42 -0
- package/web/src/views/AniListSync.vue +122 -0
- package/web/src/views/AnimeDetail.vue +202 -0
- package/web/src/views/AnimeList.vue +110 -0
- package/web/src/views/CFResult.vue +312 -0
- package/web/src/views/DaemonControl.vue +83 -0
- package/web/src/views/Dashboard.vue +187 -0
- package/web/src/views/DownloadCenter.vue +153 -0
- package/web/src/views/MatchFinder.vue +131 -0
- package/web/src/views/OAuthCallback.vue +44 -0
- package/web/src/views/Onboarding.vue +93 -0
- package/web/src/views/RunHistory.vue +122 -0
- package/web/src/views/Settings.vue +410 -0
- package/web/vite.config.js +23 -0
package/anime/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnimePahe API - Node.js Library
|
|
3
|
+
* ================================
|
|
4
|
+
* This file serves as the entry point for using animepahe-api as a library.
|
|
5
|
+
*
|
|
6
|
+
* Install: npm install github:ElijahCodes12345/animepahe-api
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const animepahe = require('animepahe-api');
|
|
10
|
+
* const results = await animepahe.search('Naruto');
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Core Scraper (Singleton)
|
|
14
|
+
const Animepahe = require('./scrapers/animepahe');
|
|
15
|
+
|
|
16
|
+
// Models (Business Logic)
|
|
17
|
+
const HomeModel = require('./models/homeModel');
|
|
18
|
+
const AnimeInfoModel = require('./models/animeInfoModel');
|
|
19
|
+
const AnimeListModel = require('./models/animeListModel');
|
|
20
|
+
const PlayModel = require('./models/playModel');
|
|
21
|
+
const QueueModel = require('./models/queueModel');
|
|
22
|
+
|
|
23
|
+
// Config Utility (for setting cookies, base URL, etc.)
|
|
24
|
+
const Config = require('./utils/config');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the library.
|
|
28
|
+
* Call this once before using any scraping functions if you need to refresh cookies.
|
|
29
|
+
* @returns {Promise<boolean>}
|
|
30
|
+
*/
|
|
31
|
+
async function initialize() {
|
|
32
|
+
return Animepahe.initialize();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Search for anime by title.
|
|
37
|
+
* @param {string} query - The search term
|
|
38
|
+
* @param {number} [page=1] - Page number for pagination
|
|
39
|
+
* @returns {Promise<object>} - Search results
|
|
40
|
+
*/
|
|
41
|
+
async function search(query, page = 1) {
|
|
42
|
+
return HomeModel.searchAnime(query, page);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get currently airing anime.
|
|
47
|
+
* @param {number} [page=1] - Page number for pagination
|
|
48
|
+
* @returns {Promise<object>} - Airing anime list
|
|
49
|
+
*/
|
|
50
|
+
async function getAiring(page = 1) {
|
|
51
|
+
return HomeModel.getAiringAnime(page);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get detailed information about an anime.
|
|
56
|
+
* @param {string} animeId - The AnimePahe session ID (e.g., "boruto-naruto-next-generations")
|
|
57
|
+
* @returns {Promise<object>} - Anime details
|
|
58
|
+
*/
|
|
59
|
+
async function getInfo(animeId) {
|
|
60
|
+
return AnimeInfoModel.getAnimeInfo(animeId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get episode releases for an anime.
|
|
65
|
+
* @param {string} animeId - The AnimePahe session ID
|
|
66
|
+
* @param {string} [sort='episode_desc'] - Sort order
|
|
67
|
+
* @param {number} [page=1] - Page number
|
|
68
|
+
* @returns {Promise<object>} - Episode list
|
|
69
|
+
*/
|
|
70
|
+
async function getReleases(animeId, sort = 'episode_desc', page = 1) {
|
|
71
|
+
return AnimeInfoModel.getAnimeReleases(animeId, sort, page);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get streaming links for a specific episode.
|
|
76
|
+
* This is the core function for video extraction.
|
|
77
|
+
* @param {string} animeId - The AnimePahe session ID
|
|
78
|
+
* @param {string} episodeId - The episode session ID
|
|
79
|
+
* @param {boolean} [includeDownloads=true] - Whether to include download links
|
|
80
|
+
* @returns {Promise<object>} - Streaming sources with m3u8 URLs
|
|
81
|
+
*/
|
|
82
|
+
async function getStreamingLinks(animeId, episodeId, includeDownloads = true) {
|
|
83
|
+
return PlayModel.getStreamingLinks(animeId, episodeId, includeDownloads);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get direct download link from a pahewin URL.
|
|
88
|
+
* @param {string} url - The pahewin download page URL
|
|
89
|
+
* @returns {Promise<object>} - Direct download URL
|
|
90
|
+
*/
|
|
91
|
+
async function getDownloadLink(url) {
|
|
92
|
+
return PlayModel.getDownloadLinks(url);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get the anime list (A-Z index).
|
|
97
|
+
* @param {string} [tab] - Filter by first letter (e.g., 'A', 'B', '#')
|
|
98
|
+
* @param {string} [tag1] - Optional genre/tag filter
|
|
99
|
+
* @param {string} [tag2] - Optional second tag filter
|
|
100
|
+
* @returns {Promise<object>} - Anime list
|
|
101
|
+
*/
|
|
102
|
+
async function getAnimeList(tab, tag1, tag2) {
|
|
103
|
+
return AnimeListModel.getAnimeList(tab, tag1, tag2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the queue (upcoming releases).
|
|
108
|
+
* @returns {Promise<object>} - Queue data
|
|
109
|
+
*/
|
|
110
|
+
async function getQueue() {
|
|
111
|
+
return QueueModel.getQueue();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Export everything
|
|
115
|
+
module.exports = {
|
|
116
|
+
// High-level API (Recommended)
|
|
117
|
+
initialize,
|
|
118
|
+
search,
|
|
119
|
+
getAiring,
|
|
120
|
+
getInfo,
|
|
121
|
+
getReleases,
|
|
122
|
+
getStreamingLinks,
|
|
123
|
+
getDownloadLink,
|
|
124
|
+
getAnimeList,
|
|
125
|
+
getQueue,
|
|
126
|
+
|
|
127
|
+
// Low-level access (Advanced)
|
|
128
|
+
Animepahe, // Raw scraper singleton
|
|
129
|
+
Config, // Configuration utility
|
|
130
|
+
|
|
131
|
+
// Models (for direct access if needed)
|
|
132
|
+
models: {
|
|
133
|
+
HomeModel,
|
|
134
|
+
AnimeInfoModel,
|
|
135
|
+
AnimeListModel,
|
|
136
|
+
PlayModel,
|
|
137
|
+
QueueModel
|
|
138
|
+
}
|
|
139
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const redis = require('../utils/redis');
|
|
2
|
+
const diskCache = require('../utils/diskCache');
|
|
3
|
+
|
|
4
|
+
const cache = (duration) => async (req, res, next) => {
|
|
5
|
+
try {
|
|
6
|
+
// Build a normalized cache key: path + sorted query params
|
|
7
|
+
let key = req.path;
|
|
8
|
+
const queryKeys = Object.keys(req.query);
|
|
9
|
+
if (queryKeys.length > 0) {
|
|
10
|
+
const sortedQuery = queryKeys
|
|
11
|
+
.sort()
|
|
12
|
+
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(req.query[k])}`)
|
|
13
|
+
.join('&');
|
|
14
|
+
key += `?${sortedQuery}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Try Redis first (if enabled)
|
|
18
|
+
if (redis.enabled) {
|
|
19
|
+
const cachedResponse = await redis.get(key);
|
|
20
|
+
if (cachedResponse) {
|
|
21
|
+
console.log(`[Cache] ✅ Redis HIT: ${key.substring(0, 60)}...`);
|
|
22
|
+
return res.json(JSON.parse(cachedResponse));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fallback to disk cache when Redis is disabled or miss
|
|
27
|
+
if (!redis.enabled) {
|
|
28
|
+
const diskCached = await diskCache.get(key);
|
|
29
|
+
if (diskCached !== null) {
|
|
30
|
+
console.log(`[Cache] ✅ Disk HIT: ${key.substring(0, 60)}...`);
|
|
31
|
+
return res.json(diskCached);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// No cache hit — proceed and cache the result
|
|
36
|
+
const originalJson = res.json;
|
|
37
|
+
|
|
38
|
+
res.json = async function(data) {
|
|
39
|
+
// Never cache non-success responses.
|
|
40
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
41
|
+
return originalJson.call(this, data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Cache in Redis if enabled
|
|
45
|
+
if (redis.enabled) {
|
|
46
|
+
await redis.setEx(key, duration, JSON.stringify(data));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Always cache to disk as fallback (even if Redis is enabled — double layer)
|
|
50
|
+
if (!redis.enabled) {
|
|
51
|
+
await diskCache.set(key, data, duration);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return originalJson.call(this, data);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
next();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Cache error:', error.message);
|
|
60
|
+
next();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = cache;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class CustomError extends Error {
|
|
2
|
+
constructor(message, statusCode = 500) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "CustomError";
|
|
5
|
+
this.statusCode = statusCode;
|
|
6
|
+
Error.captureStackTrace(this, this.constructor);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const errorHandler = (err, req, res, next) => {
|
|
11
|
+
// Check err.statusCode first (CustomError sets this correctly)
|
|
12
|
+
// before falling back to err.response?.status for HTTP errors
|
|
13
|
+
const statusCode = err.statusCode || err.response?.status || 500;
|
|
14
|
+
const message = err.message || "Something went wrong";
|
|
15
|
+
|
|
16
|
+
console.error(`Error: ${message} (Status Code: ${statusCode})`);
|
|
17
|
+
|
|
18
|
+
const response = {
|
|
19
|
+
status: statusCode,
|
|
20
|
+
message: message,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (process.env.NODE_ENV === "development") {
|
|
24
|
+
response.stack = err.stack;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
res.status(statusCode).json(response);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
CustomError,
|
|
32
|
+
errorHandler,
|
|
33
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const { redisClient, get, setEx, enabled } = require('../utils/redis');
|
|
2
|
+
|
|
3
|
+
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX || '100'); // requests per window
|
|
4
|
+
const RATE_LIMIT_WINDOW = parseInt(process.env.RATE_LIMIT_WINDOW || '900'); // seconds (15 minutes)
|
|
5
|
+
const RATE_LIMIT_SECRET = process.env.RATE_LIMIT_SECRET; // Only enable if this secret is set
|
|
6
|
+
const IS_SERVERLESS = !!(process.env.VERCEL || process.env.NETLIFY || process.env.AWS_LAMBDA_FUNCTION_NAME);
|
|
7
|
+
|
|
8
|
+
const rateLimiter = async (req, res, next) => {
|
|
9
|
+
//rate limiting if both the secret is set AND Redis is enabled
|
|
10
|
+
if (!RATE_LIMIT_SECRET || !enabled) {
|
|
11
|
+
// Skip rate limiting if no secret is set or Redis is not enabled
|
|
12
|
+
if (!enabled) {
|
|
13
|
+
console.warn('Redis not enabled, skipping rate limiting');
|
|
14
|
+
}
|
|
15
|
+
return next();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Skip rate limiting for health checks and other internal routes. Routes doesn't exist tbf, just incase
|
|
19
|
+
if (req.path === '/api/health' || req.path === '/api/status') {
|
|
20
|
+
return next();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
|
|
25
|
+
req.headers['x-real-ip'] ||
|
|
26
|
+
req.connection.remoteAddress ||
|
|
27
|
+
req.socket.remoteAddress ||
|
|
28
|
+
(req.connection.socket ? req.connection.socket.remoteAddress : null) ||
|
|
29
|
+
'unknown';
|
|
30
|
+
|
|
31
|
+
if (ip === 'unknown') {
|
|
32
|
+
console.warn('Could not determine client IP address');
|
|
33
|
+
return next();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const key = `rate-limit:${ip}:${Math.floor(Date.now() / 1000 / RATE_LIMIT_WINDOW)}`;
|
|
37
|
+
const currentCount = await get(key);
|
|
38
|
+
|
|
39
|
+
if (currentCount === null) {
|
|
40
|
+
// First request in this window, set counter with expiration
|
|
41
|
+
await setEx(key, RATE_LIMIT_WINDOW, '1');
|
|
42
|
+
res.setHeader('X-RateLimit-Remaining', RATE_LIMIT_MAX - 1);
|
|
43
|
+
res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX);
|
|
44
|
+
res.setHeader('X-RateLimit-Reset', new Date(Date.now() + RATE_LIMIT_WINDOW * 1000).toISOString());
|
|
45
|
+
return next();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const count = parseInt(currentCount);
|
|
49
|
+
|
|
50
|
+
if (count >= RATE_LIMIT_MAX) {
|
|
51
|
+
// Rate limit exceeded
|
|
52
|
+
res.status(429).json({
|
|
53
|
+
error: 'Rate limit exceeded. Please try again later.',
|
|
54
|
+
message: `Too many requests from this IP. Limit is ${RATE_LIMIT_MAX} requests per ${RATE_LIMIT_WINDOW} seconds.`,
|
|
55
|
+
retryAfter: RATE_LIMIT_WINDOW
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Increment counter
|
|
61
|
+
await setEx(key, RATE_LIMIT_WINDOW, (count + 1).toString());
|
|
62
|
+
res.setHeader('X-RateLimit-Remaining', RATE_LIMIT_MAX - count - 1);
|
|
63
|
+
res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX);
|
|
64
|
+
res.setHeader('X-RateLimit-Reset', new Date(Date.now() + RATE_LIMIT_WINDOW * 1000).toISOString());
|
|
65
|
+
|
|
66
|
+
return next();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Rate limiting error:', error);
|
|
69
|
+
return next();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
module.exports = rateLimiter;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const cheerio = require('cheerio');
|
|
2
|
+
const DataProcessor = require('../utils/dataProcessor');
|
|
3
|
+
const Animepahe = require('../scrapers/animepahe');
|
|
4
|
+
const { getJsVariable } = require('../utils/jsParser');
|
|
5
|
+
const { CustomError } = require('../middleware/errorHandler');
|
|
6
|
+
|
|
7
|
+
class AnimeInfoModel {
|
|
8
|
+
static async getAnimeInfo(animeId) {
|
|
9
|
+
const results = await Animepahe.getData("animeInfo", { animeId }, false);
|
|
10
|
+
|
|
11
|
+
if (results?.data) {
|
|
12
|
+
return DataProcessor.processApiData(results);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return this.scrapeInfoPage(results);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static async getAnimeReleases(animeId, sort, page) {
|
|
19
|
+
const MAX_ATTEMPTS = 3;
|
|
20
|
+
let lastError = null;
|
|
21
|
+
let results = null;
|
|
22
|
+
|
|
23
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
24
|
+
try {
|
|
25
|
+
results = await Animepahe.getData("releases", { animeId, sort, page });
|
|
26
|
+
break;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
lastError = error;
|
|
29
|
+
const status = error?.statusCode || error?.response?.status || 0;
|
|
30
|
+
const transient = status >= 500 || status === 0;
|
|
31
|
+
if (!transient || attempt === MAX_ATTEMPTS) {
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
console.warn(`[releases] transient failure for ${animeId} page=${page || 1} attempt=${attempt}/${MAX_ATTEMPTS}: ${error.message}`);
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!results) {
|
|
40
|
+
throw lastError || new CustomError('Failed to fetch anime releases', 503);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof results === 'object' && !results.data) {
|
|
44
|
+
results.data = [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!results.data) {
|
|
48
|
+
throw new CustomError('No release data available', 404);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
results._id = animeId;
|
|
52
|
+
return DataProcessor.processApiData(results, "releases");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static async scrapeInfoPage(pageHtml) {
|
|
56
|
+
if (!pageHtml) {
|
|
57
|
+
throw new CustomError('Failed to fetch anime info page', 503);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const $ = cheerio.load(pageHtml);
|
|
61
|
+
const previewUrl = getJsVariable(pageHtml, 'preview');
|
|
62
|
+
|
|
63
|
+
const animeInfo = {
|
|
64
|
+
ids: {
|
|
65
|
+
animepahe_id: $('meta[name="id"]').attr('content') || null,
|
|
66
|
+
anidb: $('meta[name="anidb"]').attr('content') || null,
|
|
67
|
+
anilist: $('meta[name="anilist"]').attr('content') || null,
|
|
68
|
+
animePlanet: $('meta[name="anime-planet"]').attr('content') || null,
|
|
69
|
+
ann: $('meta[name="ann"]').attr('content') || null,
|
|
70
|
+
kitsu: $('meta[name="kitsu"]').attr('content') || null,
|
|
71
|
+
mal: $('meta[name="myanimelist"]').attr('content') || null
|
|
72
|
+
},
|
|
73
|
+
title: $('.title-wrapper h1 span').first().text().trim() || null,
|
|
74
|
+
image: $('.poster-wrapper .anime-poster img').attr('data-src') || null,
|
|
75
|
+
preview: previewUrl || null,
|
|
76
|
+
synopsis: $('.content-wrapper .anime-synopsis').text().trim() || null,
|
|
77
|
+
synonym: $('.anime-info p:contains("Synonyms:")').text().split('Synonyms:')[1]?.trim() || null,
|
|
78
|
+
japanese: $('.anime-info p:contains("Japanese:")').text().split('Japanese:')[1]?.trim() || null,
|
|
79
|
+
type: $('.anime-info p:contains("Type:") a').text().trim() || null,
|
|
80
|
+
episodes: $('.anime-info p:contains("Episodes:")').text().replace('Episodes:', "").trim() || null,
|
|
81
|
+
status: $('.anime-info p:contains("Status:") a').text().trim() || null,
|
|
82
|
+
duration: $('.anime-info p:contains("Duration:")').text().split('Duration:')[1]?.trim() || null,
|
|
83
|
+
aired: ($('.anime-info p:contains("Aired:")').text().split('Aired:')[1] || '')
|
|
84
|
+
.replace(/\s+/g, ' ')
|
|
85
|
+
.replace(/to\s+\?/, '')
|
|
86
|
+
.trim()
|
|
87
|
+
.replace(/(\w{3} \d{2}, \d{4}) +to +(\w{3} \d{2}, \d{4})/, '$1 to $2') || null,
|
|
88
|
+
season: $('.anime-info p:contains("Season:") a').text().trim() || null,
|
|
89
|
+
studio: $('.anime-info p:contains("Studio:")').text().split('Studio:')[1]?.trim() || null,
|
|
90
|
+
themes: $('.anime-info p:contains("Themes:")')
|
|
91
|
+
.find('a')
|
|
92
|
+
.map((i, el) => $(el).text().trim())
|
|
93
|
+
.get() || [],
|
|
94
|
+
demographic: $('.anime-info p:contains("Demographic:")')
|
|
95
|
+
.find('a')
|
|
96
|
+
.map((i, el) => $(el).text().trim())
|
|
97
|
+
.get() || [],
|
|
98
|
+
external_links: $('.anime-info p.external-links a').map((i, el) => ({
|
|
99
|
+
name: $(el).text(),
|
|
100
|
+
url: $(el).attr('href').replace(/^(http:)?\/\//, 'https://').replace(/^https:\/\/https:\/\//, 'https://')
|
|
101
|
+
})).get() || [],
|
|
102
|
+
genre: $('.anime-info div.anime-genre ul li a').map((i, el) => $(el).text()).get() || []
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Add relations and recommendations only if they exist
|
|
106
|
+
const relations = await this.scrapeRelationsSection($);
|
|
107
|
+
if (Object.keys(relations).length > 0) {
|
|
108
|
+
animeInfo.relations = relations;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const recommendations = await this.scrapeRecommendationsSection($);
|
|
112
|
+
if (recommendations.length > 0) {
|
|
113
|
+
animeInfo.recommendations = recommendations;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!animeInfo.title || !animeInfo.ids.animepahe_id) {
|
|
117
|
+
throw new CustomError('Failed to extract essential anime information', 500);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return animeInfo;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static async scrapeRelationsSection($) {
|
|
124
|
+
const relations = {};
|
|
125
|
+
|
|
126
|
+
$('.anime-relation > div.col-12.col-sm-6, .anime-relation > div.col-12.col-sm-12').each((i, section) => {
|
|
127
|
+
const $section = $(section);
|
|
128
|
+
const type = $section.find('h4 span').text().trim();
|
|
129
|
+
|
|
130
|
+
if (!type || $section.find('div.col-12.col-sm-12.mb-3, div.col-12.col-sm-6.mb-3').length === 0) return;
|
|
131
|
+
|
|
132
|
+
relations[type] = [];
|
|
133
|
+
|
|
134
|
+
$section.find('div.col-12.col-sm-12.mb-3, div.col-12.col-sm-6.mb-3').each((j, entry) => {
|
|
135
|
+
const $entry = $(entry);
|
|
136
|
+
const titleLink = $entry.find('h5 a');
|
|
137
|
+
|
|
138
|
+
relations[type].push({
|
|
139
|
+
title: titleLink.text().trim(),
|
|
140
|
+
url: titleLink.attr('href'),
|
|
141
|
+
session: titleLink.attr('href')?.replace('/anime/', ''),
|
|
142
|
+
image: $entry.find('a img').attr('data-src'),
|
|
143
|
+
type: ($entry.find('strong a').first().text().trim() || '?').replace(/\s+/g, ' '),
|
|
144
|
+
episodes: ($entry.text().match(/(\d+)\s+Episode/) || [, '?'])[1],
|
|
145
|
+
status: this.extractStatusFromText($entry.text()),
|
|
146
|
+
season: $entry.find('a[href*="season"]').text().trim()
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (relations[type].length === 0) delete relations[type];
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return relations;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static async scrapeRecommendationsSection($) {
|
|
157
|
+
const recommendations = [];
|
|
158
|
+
|
|
159
|
+
$('div.tab-content.anime-recommendation > div.col-12.col-sm-6.mb-3').each((i, entry) => {
|
|
160
|
+
const $entry = $(entry);
|
|
161
|
+
const titleLink = $entry.find('h5 a');
|
|
162
|
+
|
|
163
|
+
recommendations.push({
|
|
164
|
+
title: titleLink.text().trim(),
|
|
165
|
+
url: titleLink.attr('href'),
|
|
166
|
+
image: $entry.find('a img').attr('data-src'),
|
|
167
|
+
type: ($entry.find('strong a').first().text().trim() || '?').replace(/\s+/g, ' '),
|
|
168
|
+
episodes: ($entry.text().match(/(\d+)\s+Episode/) || [, '?'])[1],
|
|
169
|
+
status: this.extractStatusFromText($entry.text()),
|
|
170
|
+
season: $entry.find('a[href*="/season/"]').text().trim()
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return recommendations;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static extractStatusFromText(text) {
|
|
178
|
+
const cleanText = text.replace(/\s+/g, ' ');
|
|
179
|
+
const episodeIndex = cleanText.toLowerCase().lastIndexOf('episode');
|
|
180
|
+
|
|
181
|
+
if (episodeIndex === -1) return 'Unknown';
|
|
182
|
+
|
|
183
|
+
const afterEpisode = cleanText.slice(episodeIndex + 'episode'.length).trim();
|
|
184
|
+
const parenStart = afterEpisode.indexOf('(');
|
|
185
|
+
const parenEnd = afterEpisode.indexOf(')');
|
|
186
|
+
|
|
187
|
+
return (parenStart > -1 && parenEnd > parenStart)
|
|
188
|
+
? afterEpisode.slice(parenStart + 1, parenEnd).trim()
|
|
189
|
+
: 'Unknown';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = AnimeInfoModel;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const cheerio = require("cheerio");
|
|
2
|
+
const DataProcessor = require("../utils/dataProcessor");
|
|
3
|
+
const Animepahe = require("../scrapers/animepahe");
|
|
4
|
+
const { CustomError } = require("../middleware/errorHandler");
|
|
5
|
+
|
|
6
|
+
class AnimeListModel {
|
|
7
|
+
static async getAnimeList(page, tab, genre) {
|
|
8
|
+
const results = await Animepahe.getData(
|
|
9
|
+
"animeList",
|
|
10
|
+
{ page, tab, genre },
|
|
11
|
+
false,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
if (results?.data) {
|
|
15
|
+
return DataProcessor.processApiData(results);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return this.scrapeAnimeListPage(results, tab);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static async scrapeAnimeListPage(pageHtml, tab) {
|
|
22
|
+
if (!pageHtml) {
|
|
23
|
+
throw new CustomError("Failed to fetch anime list page", 503);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const $ = cheerio.load(pageHtml);
|
|
27
|
+
const animeList = [];
|
|
28
|
+
|
|
29
|
+
const processPane = (pane) => {
|
|
30
|
+
$(pane)
|
|
31
|
+
.find("div.col-12.col-md-6")
|
|
32
|
+
.each((j, entry) => {
|
|
33
|
+
const $entry = $(entry);
|
|
34
|
+
const $link = $entry.find("a");
|
|
35
|
+
const $badge = $entry.find("span.badge");
|
|
36
|
+
|
|
37
|
+
animeList.push({
|
|
38
|
+
title: $link.attr("title") || $link.text().trim(),
|
|
39
|
+
url: $link.attr("href"),
|
|
40
|
+
type: $badge.length ? $badge.first().text().trim() : null,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (typeof tab !== "undefined") {
|
|
46
|
+
const targetId =
|
|
47
|
+
tab === "#" || tab === "hash" ? "hash" : tab.toUpperCase();
|
|
48
|
+
const $pane = $(`div.tab-pane#${targetId}`);
|
|
49
|
+
|
|
50
|
+
if (!$pane.length) {
|
|
51
|
+
throw new CustomError(`No content found for tab: ${tab}`, 404);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
processPane($pane);
|
|
55
|
+
} else {
|
|
56
|
+
$("div.tab-content div.tab-pane").each((i, pane) => {
|
|
57
|
+
processPane(pane);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (animeList.length === 0) {
|
|
62
|
+
throw new CustomError("No anime entries found", 404);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return animeList;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = AnimeListModel;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const DataProcessor = require('../utils/dataProcessor');
|
|
2
|
+
const Config = require('../utils/config');
|
|
3
|
+
const Animepahe = require('../scrapers/animepahe');
|
|
4
|
+
const { CustomError } = require('../middleware/errorHandler');
|
|
5
|
+
|
|
6
|
+
class HomeModel {
|
|
7
|
+
static async getAiringAnime(page) {
|
|
8
|
+
const results = await Animepahe.getData("airing", { page });
|
|
9
|
+
|
|
10
|
+
if (!results || !results.data) {
|
|
11
|
+
throw new CustomError('No airing anime data found', 404);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return DataProcessor.processApiData(results);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static async searchAnime(query, page) {
|
|
18
|
+
console.log(page);
|
|
19
|
+
if (!query) {
|
|
20
|
+
throw new CustomError('Search query is required', 400);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const results = await Animepahe.getData("search", { query, page });
|
|
24
|
+
|
|
25
|
+
if (!results || !results.data) {
|
|
26
|
+
throw new CustomError('No search results found', 404);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return DataProcessor.processApiData(results, 'search');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = HomeModel;
|