shirayuki-anime-scraper-api 1.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 (39) hide show
  1. package/Dockerfile +14 -0
  2. package/LICENSE +24 -0
  3. package/README.md +539 -0
  4. package/config/database.js +37 -0
  5. package/index.js +63 -0
  6. package/models/Episode.js +49 -0
  7. package/models/Schedule.js +50 -0
  8. package/package.json +46 -0
  9. package/routes/anime-list.js +67 -0
  10. package/routes/episodeStream.js +64 -0
  11. package/routes/genre.js +67 -0
  12. package/routes/home.js +30 -0
  13. package/routes/monthly.js +37 -0
  14. package/routes/schedule.js +174 -0
  15. package/routes/search.js +79 -0
  16. package/routes/top10.js +37 -0
  17. package/routes/weekly.js +37 -0
  18. package/save.txt +431 -0
  19. package/scrapeanime/A-Z/AnimeList/filter.js +43 -0
  20. package/scrapeanime/A-Z/Genre/genre.js +42 -0
  21. package/scrapeanime/AnimeDetails/animedetails.js +73 -0
  22. package/scrapeanime/Browse/Search/search.js +119 -0
  23. package/scrapeanime/Browse/Suggestion/suggestion.js +50 -0
  24. package/scrapeanime/Leaderboard/Monthly/scrapeHiAnimeMonthlyTop10.js +137 -0
  25. package/scrapeanime/Leaderboard/Top/scrapeHiAnimeTop10.js +125 -0
  26. package/scrapeanime/Leaderboard/Weekly/scrapeHiAnimeWeeklyTop10.js +188 -0
  27. package/scrapeanime/Schedule/schedule.js +174 -0
  28. package/scrapeanime/SingleEpisode/scrapeSingleEpisode.js +496 -0
  29. package/scrapeanime/homepage/latest/latest.js +118 -0
  30. package/scrapeanime/homepage/most_favorite/mostFavorite.js +55 -0
  31. package/scrapeanime/homepage/most_popular/mostPopular.js +55 -0
  32. package/scrapeanime/homepage/recently_updated/recentlyUpdated.js +56 -0
  33. package/scrapeanime/homepage/scrapeAnimeDetails.js +128 -0
  34. package/scrapeanime/homepage/scrapehomepage.js +2 -0
  35. package/scrapeanime/homepage/scrapeservice.js +158 -0
  36. package/scrapeanime/homepage/slider/slider.js +151 -0
  37. package/scrapeanime/homepage/top_airing/topAiring.js +55 -0
  38. package/scrapeanime/homepage/trending/trending.js +59 -0
  39. package/service/scraperService.js +38 -0
@@ -0,0 +1,119 @@
1
+ import axios from 'axios';
2
+ import * as cheerio from 'cheerio';
3
+
4
+ export async function scrapeAnimeSearch(query) {
5
+ const url = `https://123animehub.cc/search?keyword=${encodeURIComponent(query)}`;
6
+
7
+ try {
8
+ const { data } = await axios.get(url);
9
+ const $ = cheerio.load(data);
10
+ const results = [];
11
+
12
+ const selectors = [
13
+ '.film-list .item',
14
+ '.film_list-wrap .item',
15
+ '.flw-item',
16
+ '.anime-list .item',
17
+ '.items .item'
18
+ ];
19
+
20
+ let itemsFound = false;
21
+
22
+ for (const selector of selectors) {
23
+ const items = $(selector);
24
+ if (items.length > 0) {
25
+ itemsFound = true;
26
+
27
+ items.each((i, el) => {
28
+ const $el = $(el);
29
+
30
+ // Extract title
31
+ const title = $el.find('.name a, .film-name a, .dynamic-name, .title a, h3 a').first().text().trim() ||
32
+ $el.find('a[data-jtitle]').attr('data-jtitle') ||
33
+ $el.find('img').attr('alt') || '';
34
+
35
+ // Extract image
36
+ const imgElement = $el.find('.film-poster img, .poster img, img').first();
37
+ let image = imgElement.attr('data-src') ||
38
+ imgElement.attr('src') ||
39
+ imgElement.attr('data-lazy') || '';
40
+
41
+ if (image && !image.startsWith('http')) {
42
+ image = image.startsWith('/') ? 'https://123animehub.cc' + image : 'https://123animehub.cc/' + image;
43
+ }
44
+
45
+ if (!image || image.includes('no_poster.jpg')) {
46
+ image = '';
47
+ }
48
+
49
+ const episodeSelectors = [
50
+ '.fa-tv',
51
+ '.ep-num',
52
+ '.episode',
53
+ '[class*="ep"]',
54
+ '.item-head .is-sub'
55
+ ];
56
+
57
+ let episodeText = '';
58
+ for (const epSelector of episodeSelectors) {
59
+ const epElement = $el.find(epSelector);
60
+ if (epElement.length > 0) {
61
+ episodeText = epElement.parent().text().trim() || epElement.text().trim();
62
+ break;
63
+ }
64
+ }
65
+
66
+ const episode = episodeText.replace(/[^\d]/g, '') || '';
67
+
68
+ const statusSelectors = [
69
+ '.dot',
70
+ '.status',
71
+ '.film-infor .fdi-item',
72
+ '.is-sub',
73
+ '.is-dub'
74
+ ];
75
+
76
+ let statusText = '';
77
+ for (const statusSelector of statusSelectors) {
78
+ const statusElement = $el.find(statusSelector);
79
+ if (statusElement.length > 0) {
80
+ statusText = statusElement.parent().text().trim() || statusElement.text().trim();
81
+ break;
82
+ }
83
+ }
84
+
85
+ // Extract sub/dub
86
+ const hasSub = statusText.toLowerCase().includes('sub') ||
87
+ $el.find('.is-sub').length > 0 ||
88
+ $el.find('[class*="sub"]').length > 0;
89
+
90
+ const hasDub = statusText.toLowerCase().includes('dub') ||
91
+ $el.find('.is-dub').length > 0 ||
92
+ $el.find('[class*="dub"]').length > 0;
93
+
94
+ if (title && title.length > 0) {
95
+ results.push({
96
+ title,
97
+ sub: hasSub,
98
+ dub: hasDub,
99
+ image,
100
+ episodes: episode || null
101
+ });
102
+ }
103
+ });
104
+
105
+ break;
106
+ }
107
+ }
108
+
109
+ if (!itemsFound) {
110
+ console.log('No anime items found with any selector');
111
+ }
112
+
113
+ return results;
114
+
115
+ } catch (error) {
116
+ console.error('Error scraping anime search:', error);
117
+ throw new Error(`Failed to scrape search results: ${error.message}`);
118
+ }
119
+ }
@@ -0,0 +1,50 @@
1
+ import axios from 'axios';
2
+ import * as cheerio from 'cheerio';
3
+
4
+ export async function scrapeSearchSuggestions(query) {
5
+ const url = `https://123animehub.cc/search?keyword=${encodeURIComponent(query)}`;
6
+ const { data } = await axios.get(url);
7
+ const $ = cheerio.load(data);
8
+ const suggestions = [];
9
+
10
+ $('.suggestions .item, .film-list .item').each((i, el) => {
11
+ const title = $(el).find('.name, a[data-jtitle]').first().text().trim() ||
12
+ $(el).find('a[data-jtitle]').attr('data-jtitle') || '';
13
+
14
+ // Extract image
15
+ const imgElement = $(el).find('.film-poster img, .poster img, img').first();
16
+ let image = imgElement.attr('data-src') ||
17
+ imgElement.attr('src') ||
18
+ imgElement.attr('data-lazy') || '';
19
+
20
+ if (image && !image.startsWith('http')) {
21
+ image = image.startsWith('/') ? 'https://123animehub.cc' + image : 'https://123animehub.cc/' + image;
22
+ }
23
+
24
+ if (!image || image.includes('no_poster.jpg')) {
25
+ image = '';
26
+ }
27
+
28
+ // Extract episode
29
+ const episodeText = $(el).find('.fa-tv').parent().text().trim() ||
30
+ $(el).find('[class*="ep"]').text().trim() || '';
31
+ const episode = episodeText.replace(/[^\d]/g, '') || '';
32
+
33
+ // Extract sub/dub
34
+ const isSubbed = $(el).find('.sub').length > 0;
35
+ const isDubbed = $(el).find('.dub').length > 0;
36
+ const type = isDubbed ? 'dub' : (isSubbed ? 'sub' : 'sub');
37
+
38
+ if (title) {
39
+ suggestions.push({
40
+ index: i + 1,
41
+ title,
42
+ image,
43
+ episode,
44
+ type
45
+ });
46
+ }
47
+ });
48
+
49
+ return suggestions;
50
+ }
@@ -0,0 +1,137 @@
1
+ import axios from 'axios';
2
+ import * as cheerio from 'cheerio';
3
+
4
+ export const scrapeHiAnimeMonthlyTop10 = async () => {
5
+ try {
6
+ console.log('🌐 Loading HiAnime home page...');
7
+ const response = await axios.get('https://hianime.to/home', {
8
+ headers: {
9
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
10
+ },
11
+ timeout: 30000
12
+ });
13
+
14
+ const $ = cheerio.load(response.data);
15
+ const results = [];
16
+ const processedTitles = new Set();
17
+
18
+ let monthlySection = $('#top-viewed-month.anif-block-ul.anif-block-chart.tab-pane');
19
+ if (!monthlySection.length) {
20
+ monthlySection = $('.anif-block-ul.anif-block-chart.tab-pane').eq(2);
21
+ }
22
+
23
+ if (monthlySection.length) {
24
+ console.log('✅ Found monthly section');
25
+
26
+ const topItems = monthlySection.find('.item-top');
27
+ topItems.each((index, item) => {
28
+ if (results.length >= 10) return false;
29
+ try {
30
+ const titleElement = $(item).find('.film-name a');
31
+ const title = titleElement.text().trim();
32
+ const imageElement = $(item).find('.film-poster img');
33
+ const image = imageElement.attr('data-src') || imageElement.attr('src') || null;
34
+
35
+ const subEpisodes = [];
36
+ const dubEpisodes = [];
37
+ $(item).find('.tick-item').each((_, tick) => {
38
+ if ($(tick).hasClass('tick-sub')) {
39
+ const ep = $(tick).text().trim();
40
+ if (ep) subEpisodes.push(ep);
41
+ }
42
+ if ($(tick).hasClass('tick-dub')) {
43
+ const ep = $(tick).text().trim();
44
+ if (ep) dubEpisodes.push(ep);
45
+ }
46
+ });
47
+
48
+ if (title && !processedTitles.has(title)) {
49
+ processedTitles.add(title);
50
+ results.push({
51
+ title: title,
52
+ image: image,
53
+ rank: index + 1,
54
+ sub: subEpisodes,
55
+ dub: dubEpisodes
56
+ });
57
+ console.log(`📊 Monthly Top ${index + 1}: ${title}`);
58
+ }
59
+ } catch (error) {
60
+ console.log(`⚠️ Error processing top item ${index + 1}:`, error.message);
61
+ }
62
+ });
63
+
64
+ const listItems = monthlySection.find('li:not(.item-top)');
65
+ listItems.each((index, item) => {
66
+ if (results.length >= 10) return false;
67
+ try {
68
+ const titleElement = $(item).find('.film-name a, .dynamic-name, a[title], a');
69
+ const title = titleElement.text().trim() || titleElement.attr('title');
70
+ const imageElement = $(item).find('.film-poster img');
71
+ const image = imageElement.attr('data-src') || imageElement.attr('src') || null;
72
+
73
+ const subEpisodes = [];
74
+ const dubEpisodes = [];
75
+ $(item).find('.tick-item').each((_, tick) => {
76
+ if ($(tick).hasClass('tick-sub')) {
77
+ const ep = $(tick).text().trim();
78
+ if (ep) subEpisodes.push(ep);
79
+ }
80
+ if ($(tick).hasClass('tick-dub')) {
81
+ const ep = $(tick).text().trim();
82
+ if (ep) dubEpisodes.push(ep);
83
+ }
84
+ });
85
+
86
+ if (title && title.length > 3 && !processedTitles.has(title)) {
87
+ processedTitles.add(title);
88
+ results.push({
89
+ title: title,
90
+ image: image,
91
+ rank: results.length + 1,
92
+ sub: subEpisodes,
93
+ dub: dubEpisodes
94
+ });
95
+ console.log(`📊 Monthly #${results.length}: ${title}`);
96
+ }
97
+ } catch (error) {
98
+ console.log(`⚠️ Error processing list item ${index + 1}:`, error.message);
99
+ }
100
+ });
101
+ } else {
102
+ console.log('❌ Monthly section not found');
103
+ }
104
+
105
+ console.log(`✅ Found ${results.length} monthly anime titles`);
106
+
107
+ const finalResults = results.slice(0, 10).map((anime, idx) => ({
108
+ index: idx + 1,
109
+ rank: anime.rank,
110
+ title: anime.title,
111
+ img: anime.image ? `${anime.image}?title=${encodeURIComponent(anime.title)}` : '',
112
+ dub: anime.dub,
113
+ sub: anime.sub
114
+ }));
115
+
116
+ const resultObj = {};
117
+ finalResults.forEach(anime => {
118
+ resultObj[anime.index] = anime;
119
+ });
120
+
121
+ console.log(`📋 Returning ${Object.keys(resultObj).length} monthly top anime`);
122
+ return resultObj;
123
+
124
+ } catch (error) {
125
+ console.error('❌ Error scraping HiAnime Monthly:', error.message);
126
+ return {
127
+ 1: {
128
+ index: 1,
129
+ rank: 1,
130
+ title: "Monthly Scraping Error",
131
+ img: null,
132
+ dub: [],
133
+ sub: []
134
+ }
135
+ };
136
+ }
137
+ };
@@ -0,0 +1,125 @@
1
+ import axios from 'axios';
2
+ import * as cheerio from 'cheerio';
3
+
4
+ export const scrapeHiAnimeTop10 = async () => {
5
+ try {
6
+ const response = await axios.get('https://hianime.to/home', {
7
+ headers: {
8
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
9
+ },
10
+ timeout: 30000
11
+ });
12
+
13
+ const $ = cheerio.load(response.data);
14
+ const results = [];
15
+ const processedTitles = new Set();
16
+
17
+ let trendingSection = $('.anif-block-ul.anif-block-chart.tab-pane.active');
18
+ if (!trendingSection.length) {
19
+ trendingSection = $('.anif-block-ul.anif-block-chart.tab-pane');
20
+ }
21
+
22
+ if (trendingSection.length) {
23
+ const topItems = trendingSection.find('.item-top');
24
+ topItems.each((index, item) => {
25
+ if (results.length >= 10) return false;
26
+ try {
27
+ const titleElement = $(item).find('.film-name a');
28
+ const title = titleElement.text().trim();
29
+ const imageElement = $(item).find('.film-poster img');
30
+ const image = imageElement.attr('data-src') || imageElement.attr('src') || null;
31
+
32
+ const subEpisodes = [];
33
+ const dubEpisodes = [];
34
+ $(item).find('.tick-item').each((_, tick) => {
35
+ if ($(tick).hasClass('tick-sub')) {
36
+ const ep = $(tick).text().trim();
37
+ if (ep) subEpisodes.push(ep);
38
+ }
39
+ if ($(tick).hasClass('tick-dub')) {
40
+ const ep = $(tick).text().trim();
41
+ if (ep) dubEpisodes.push(ep);
42
+ }
43
+ });
44
+
45
+ if (title && !processedTitles.has(title)) {
46
+ processedTitles.add(title);
47
+ results.push({
48
+ title: title,
49
+ image: image,
50
+ rank: index + 1,
51
+ sub: subEpisodes,
52
+ dub: dubEpisodes
53
+ });
54
+ }
55
+ } catch (error) {
56
+ }
57
+ });
58
+
59
+ const listItems = trendingSection.find('li:not(.item-top)');
60
+ listItems.each((index, item) => {
61
+ if (results.length >= 10) return false;
62
+ try {
63
+ const titleElement = $(item).find('.film-name a, .dynamic-name, a[title], a');
64
+ const title = titleElement.text().trim() || titleElement.attr('title');
65
+ const imageElement = $(item).find('.film-poster img');
66
+ const image = imageElement.attr('data-src') || imageElement.attr('src') || null;
67
+
68
+ const subEpisodes = [];
69
+ const dubEpisodes = [];
70
+ $(item).find('.tick-item').each((_, tick) => {
71
+ if ($(tick).hasClass('tick-sub')) {
72
+ const ep = $(tick).text().trim();
73
+ if (ep) subEpisodes.push(ep);
74
+ }
75
+ if ($(tick).hasClass('tick-dub')) {
76
+ const ep = $(tick).text().trim();
77
+ if (ep) dubEpisodes.push(ep);
78
+ }
79
+ });
80
+
81
+ if (title && title.length > 3 && !processedTitles.has(title)) {
82
+ processedTitles.add(title);
83
+ results.push({
84
+ title: title,
85
+ image: image,
86
+ rank: results.length + 1,
87
+ sub: subEpisodes,
88
+ dub: dubEpisodes
89
+ });
90
+ }
91
+ } catch (error) {
92
+ }
93
+ });
94
+ }
95
+
96
+ const finalResults = results.slice(0, 10).map((anime, idx) => ({
97
+ index: idx + 1,
98
+ rank: anime.rank,
99
+ title: anime.title,
100
+ img: anime.image ? `${anime.image}?title=${encodeURIComponent(anime.title)}` : '',
101
+ dub: anime.dub,
102
+ sub: anime.sub
103
+ }));
104
+
105
+ const resultObj = {};
106
+ finalResults.forEach(anime => {
107
+ resultObj[anime.index] = anime;
108
+ });
109
+ return resultObj;
110
+
111
+ } catch (error) {
112
+ console.error('Error scraping HiAnime:', error.message);
113
+ return {
114
+ 1: {
115
+ index: 1,
116
+ rank: 1,
117
+ title: "Scraping Error",
118
+ img: null,
119
+ dub: [],
120
+ sub: []
121
+ }
122
+ };
123
+ }
124
+ };
125
+
@@ -0,0 +1,188 @@
1
+ import axios from 'axios';
2
+ import * as cheerio from 'cheerio';
3
+
4
+ export const scrapeHiAnimeWeeklyTop10 = async () => {
5
+ try {
6
+ console.log('🌐 Loading HiAnime home page...');
7
+ const response = await axios.get('https://hianime.to/home', {
8
+ headers: {
9
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
10
+ },
11
+ timeout: 30000
12
+ });
13
+
14
+ const $ = cheerio.load(response.data);
15
+ const results = [];
16
+ const processedTitles = new Set();
17
+
18
+ let weeklySection = $('#top-viewed-week.anif-block-ul.anif-block-chart.tab-pane');
19
+ if (!weeklySection.length) {
20
+ weeklySection = $('.anif-block-ul.anif-block-chart.tab-pane').eq(1);
21
+ }
22
+
23
+ if (weeklySection.length) {
24
+ console.log('✅ Found weekly section');
25
+
26
+ const topItems = weeklySection.find('.item-top');
27
+ topItems.each((index, item) => {
28
+ if (results.length >= 10) return false;
29
+ try {
30
+ const titleElement = $(item).find('.film-name a');
31
+ const title = titleElement.text().trim();
32
+ const imageElement = $(item).find('.film-poster img');
33
+ const image = imageElement.attr('data-src') || imageElement.attr('src') || null;
34
+
35
+ const subEpisodes = [];
36
+ const dubEpisodes = [];
37
+ $(item).find('.tick-item').each((_, tick) => {
38
+ if ($(tick).hasClass('tick-sub')) {
39
+ const ep = $(tick).text().trim();
40
+ if (ep) subEpisodes.push(ep);
41
+ }
42
+ if ($(tick).hasClass('tick-dub')) {
43
+ const ep = $(tick).text().trim();
44
+ if (ep) dubEpisodes.push(ep);
45
+ }
46
+ });
47
+
48
+ if (title && !processedTitles.has(title)) {
49
+ processedTitles.add(title);
50
+ results.push({
51
+ title: title,
52
+ image: image,
53
+ rank: index + 1,
54
+ sub: subEpisodes,
55
+ dub: dubEpisodes,
56
+ category: 'top'
57
+ });
58
+ console.log(`📅 Weekly Top ${index + 1}: ${title}`);
59
+ }
60
+ } catch (error) {
61
+ console.log(`⚠️ Error processing weekly top item ${index + 1}:`, error.message);
62
+ }
63
+ });
64
+
65
+ const listItems = weeklySection.find('li:not(.item-top)');
66
+ listItems.each((index, item) => {
67
+ if (results.length >= 10) return false;
68
+ try {
69
+ const titleElement = $(item).find('.film-name a, .dynamic-name, a[title], a');
70
+ const title = titleElement.text().trim() || titleElement.attr('title');
71
+ const imageElement = $(item).find('.film-poster img');
72
+ const image = imageElement.attr('data-src') || imageElement.attr('src') || null;
73
+
74
+ const subEpisodes = [];
75
+ const dubEpisodes = [];
76
+ $(item).find('.tick-item').each((_, tick) => {
77
+ if ($(tick).hasClass('tick-sub')) {
78
+ const ep = $(tick).text().trim();
79
+ if (ep) subEpisodes.push(ep);
80
+ }
81
+ if ($(tick).hasClass('tick-dub')) {
82
+ const ep = $(tick).text().trim();
83
+ if (ep) dubEpisodes.push(ep);
84
+ }
85
+ });
86
+
87
+ if (title && title.length > 3 && !processedTitles.has(title)) {
88
+ processedTitles.add(title);
89
+ results.push({
90
+ title: title,
91
+ image: image,
92
+ rank: results.length + 1,
93
+ sub: subEpisodes,
94
+ dub: dubEpisodes,
95
+ category: 'regular'
96
+ });
97
+ console.log(`📅 Weekly #${results.length}: ${title}`);
98
+ }
99
+ } catch (error) {
100
+ console.log(`⚠️ Error processing weekly list item ${index + 1}:`, error.message);
101
+ }
102
+ });
103
+ }
104
+
105
+ if (results.length === 0) {
106
+ console.log('🔍 Trying alternative weekly selector...');
107
+ const alternativeWeeklySection = $('[id*="top-viewed-week"]');
108
+ if (alternativeWeeklySection.length) {
109
+ console.log('✅ Found alternative weekly section');
110
+ const allItems = alternativeWeeklySection.find('li');
111
+ allItems.each((index, item) => {
112
+ if (results.length >= 10) return false;
113
+ try {
114
+ const titleElement = $(item).find('.film-name a, a[title], a');
115
+ const title = titleElement.text().trim() || titleElement.attr('title');
116
+ const imageElement = $(item).find('.film-poster img, img');
117
+ const image = imageElement.attr('data-src') || imageElement.attr('src') || null;
118
+
119
+ const subEpisodes = [];
120
+ const dubEpisodes = [];
121
+ $(item).find('.tick-item').each((_, tick) => {
122
+ if ($(tick).hasClass('tick-sub')) {
123
+ const ep = $(tick).text().trim();
124
+ if (ep) subEpisodes.push(ep);
125
+ }
126
+ if ($(tick).hasClass('tick-dub')) {
127
+ const ep = $(tick).text().trim();
128
+ if (ep) dubEpisodes.push(ep);
129
+ }
130
+ });
131
+
132
+ if (title && title.length > 3 && !processedTitles.has(title)) {
133
+ processedTitles.add(title);
134
+ results.push({
135
+ title: title,
136
+ image: image,
137
+ rank: results.length + 1,
138
+ sub: subEpisodes,
139
+ dub: dubEpisodes,
140
+ category: 'alternative'
141
+ });
142
+ console.log(`📅 Weekly Alt #${results.length}: ${title}`);
143
+ }
144
+ } catch (error) {
145
+ console.log(`⚠️ Error processing alternative weekly item ${index + 1}:`, error.message);
146
+ }
147
+ });
148
+ } else {
149
+ console.log('❌ No weekly section found with alternative selector');
150
+ }
151
+ }
152
+
153
+ console.log(`✅ Found ${results.length} weekly anime titles`);
154
+
155
+ // Only return top 10
156
+ const finalResults = results.slice(0, 10).map((anime, idx) => ({
157
+ index: idx + 1,
158
+ rank: anime.rank,
159
+ title: anime.title,
160
+ img: anime.image ? `${anime.image}?title=${encodeURIComponent(anime.title)}` : '',
161
+ dub: anime.dub,
162
+ sub: anime.sub,
163
+ category: anime.category
164
+ }));
165
+
166
+ const resultObj = {};
167
+ finalResults.forEach(anime => {
168
+ resultObj[anime.index] = anime;
169
+ });
170
+
171
+ console.log(`📋 Returning ${Object.keys(resultObj).length} weekly top anime`);
172
+ return resultObj;
173
+
174
+ } catch (error) {
175
+ console.error('❌ Error scraping HiAnime Weekly:', error.message);
176
+ return {
177
+ 1: {
178
+ index: 1,
179
+ rank: 1,
180
+ title: "Weekly Scraping Error",
181
+ img: null,
182
+ dub: [],
183
+ sub: [],
184
+ category: 'error'
185
+ }
186
+ };
187
+ }
188
+ };