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.
Files changed (90) hide show
  1. package/anime/LICENSE +21 -0
  2. package/anime/api/index.js +110 -0
  3. package/anime/app.js +79 -0
  4. package/anime/controllers/animeInfoController.js +48 -0
  5. package/anime/controllers/animeListController.js +21 -0
  6. package/anime/controllers/homeController.js +42 -0
  7. package/anime/controllers/playController.js +140 -0
  8. package/anime/controllers/queueController.js +20 -0
  9. package/anime/controllers/testController.js +667 -0
  10. package/anime/index.js +139 -0
  11. package/anime/middleware/cache.js +64 -0
  12. package/anime/middleware/errorHandler.js +33 -0
  13. package/anime/middleware/rateLimiter.js +73 -0
  14. package/anime/models/animeInfoModel.js +193 -0
  15. package/anime/models/animeListModel.js +69 -0
  16. package/anime/models/homeModel.js +33 -0
  17. package/anime/models/playModel.js +528 -0
  18. package/anime/models/queueModel.js +22 -0
  19. package/anime/package.json +27 -0
  20. package/anime/pnpm-lock.yaml +1774 -0
  21. package/anime/readme.md +236 -0
  22. package/anime/routes/animeInfoRoutes.js +9 -0
  23. package/anime/routes/animeListRoutes.js +9 -0
  24. package/anime/routes/homeRoutes.js +10 -0
  25. package/anime/routes/playRoutes.js +11 -0
  26. package/anime/routes/queueRoutes.js +8 -0
  27. package/anime/routes/testRoutes.js +325 -0
  28. package/anime/routes/webhookRoutes.js +23 -0
  29. package/anime/scrapers/animepahe.js +1053 -0
  30. package/anime/test-server.js +10 -0
  31. package/anime/test.sh +275 -0
  32. package/anime/utils/browser.js +172 -0
  33. package/anime/utils/config.js +209 -0
  34. package/anime/utils/dataProcessor.js +172 -0
  35. package/anime/utils/diskCache.js +228 -0
  36. package/anime/utils/flaresolverr.js +361 -0
  37. package/anime/utils/jsParser.js +19 -0
  38. package/anime/utils/redis.js +64 -0
  39. package/anime/utils/requestManager.js +706 -0
  40. package/anime/utils/urlConverter.js +88 -0
  41. package/anime/utils/webhookNotifier.js +190 -0
  42. package/cookiereader.py +291 -0
  43. package/package.json +14 -6
  44. package/src/anilist.js +19 -2
  45. package/src/api/anilist.js +32 -0
  46. package/src/api/anime.js +181 -0
  47. package/src/api/cf-bypass.js +131 -0
  48. package/src/api/config.js +134 -0
  49. package/src/api/daemon.js +62 -0
  50. package/src/api/download.js +98 -0
  51. package/src/api/match.js +58 -0
  52. package/src/api/runs.js +15 -0
  53. package/src/api/update.js +96 -0
  54. package/src/cli.js +18 -2
  55. package/src/config.js +14 -2
  56. package/src/daemon.js +1 -1
  57. package/src/db.js +38 -11
  58. package/src/engine.js +66 -13
  59. package/src/notify.js +82 -7
  60. package/src/scraper.js +2 -2
  61. package/src/server.js +108 -0
  62. package/utils.js +21 -21
  63. package/web/index.html +13 -0
  64. package/web/package-lock.json +1420 -0
  65. package/web/package.json +24 -0
  66. package/web/src/App.vue +200 -0
  67. package/web/src/api/client.js +75 -0
  68. package/web/src/assets/styles/base.css +961 -0
  69. package/web/src/components/UpdateModal.vue +157 -0
  70. package/web/src/components/sidebar/SidebarResizable.vue +170 -0
  71. package/web/src/components/sidebar/sidebarConfig.js +24 -0
  72. package/web/src/main.js +104 -0
  73. package/web/src/router/index.js +44 -0
  74. package/web/src/stores/config.js +27 -0
  75. package/web/src/stores/download.js +107 -0
  76. package/web/src/stores/update.js +73 -0
  77. package/web/src/views/About.vue +42 -0
  78. package/web/src/views/AniListSync.vue +122 -0
  79. package/web/src/views/AnimeDetail.vue +202 -0
  80. package/web/src/views/AnimeList.vue +110 -0
  81. package/web/src/views/CFResult.vue +312 -0
  82. package/web/src/views/DaemonControl.vue +83 -0
  83. package/web/src/views/Dashboard.vue +187 -0
  84. package/web/src/views/DownloadCenter.vue +153 -0
  85. package/web/src/views/MatchFinder.vue +131 -0
  86. package/web/src/views/OAuthCallback.vue +44 -0
  87. package/web/src/views/Onboarding.vue +93 -0
  88. package/web/src/views/RunHistory.vue +122 -0
  89. package/web/src/views/Settings.vue +410 -0
  90. 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;