kaizoku-core 0.1.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 (54) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +105 -0
  3. package/dist/extractors/kwik.d.ts +6 -0
  4. package/dist/extractors/kwik.js +33 -0
  5. package/dist/extractors/megaplay.d.ts +2 -0
  6. package/dist/extractors/megaplay.js +32 -0
  7. package/dist/extractors/streamwish.d.ts +4 -0
  8. package/dist/extractors/streamwish.js +86 -0
  9. package/dist/extractors/vidtube.d.ts +2 -0
  10. package/dist/extractors/vidtube.js +24 -0
  11. package/dist/extractors/vidwish.d.ts +2 -0
  12. package/dist/extractors/vidwish.js +32 -0
  13. package/dist/index.d.ts +18 -0
  14. package/dist/index.js +18 -0
  15. package/dist/lib/config.d.ts +22 -0
  16. package/dist/lib/config.js +21 -0
  17. package/dist/providers/anime/anidb.d.ts +6 -0
  18. package/dist/providers/anime/anidb.js +88 -0
  19. package/dist/providers/anime/anikoto.d.ts +41 -0
  20. package/dist/providers/anime/anikoto.js +259 -0
  21. package/dist/providers/anime/animegg.d.ts +6 -0
  22. package/dist/providers/anime/animegg.js +107 -0
  23. package/dist/providers/anime/animeonsen.d.ts +6 -0
  24. package/dist/providers/anime/animeonsen.js +95 -0
  25. package/dist/providers/anime/animepahe.d.ts +6 -0
  26. package/dist/providers/anime/animepahe.js +102 -0
  27. package/dist/providers/anime/animesaturn.d.ts +8 -0
  28. package/dist/providers/anime/animesaturn.js +160 -0
  29. package/dist/providers/anime/animeunity.d.ts +4 -0
  30. package/dist/providers/anime/animeunity.js +108 -0
  31. package/dist/providers/anime/anizone.d.ts +12 -0
  32. package/dist/providers/anime/anizone.js +146 -0
  33. package/dist/providers/anime/gojo.d.ts +6 -0
  34. package/dist/providers/anime/gojo.js +83 -0
  35. package/dist/providers/meta/anilist/anilist.d.ts +28 -0
  36. package/dist/providers/meta/anilist/anilist.js +263 -0
  37. package/dist/providers/meta/anilist/queries.d.ts +22 -0
  38. package/dist/providers/meta/anilist/queries.js +405 -0
  39. package/dist/providers/meta/anilist/types.d.ts +213 -0
  40. package/dist/providers/meta/anilist/types.js +21 -0
  41. package/dist/providers/meta/anilist.d.ts +15 -0
  42. package/dist/providers/meta/anilist.js +94 -0
  43. package/dist/types/types.d.ts +88 -0
  44. package/dist/types/types.js +4 -0
  45. package/dist/utils/http.d.ts +13 -0
  46. package/dist/utils/http.js +39 -0
  47. package/dist/utils/proxy.d.ts +9 -0
  48. package/dist/utils/proxy.js +43 -0
  49. package/dist/utils/shared.d.ts +15 -0
  50. package/dist/utils/shared.js +64 -0
  51. package/dist/utils/unpack.d.ts +9 -0
  52. package/dist/utils/unpack.js +59 -0
  53. package/package.json +34 -0
  54. package/vitest.config.ts +7 -0
@@ -0,0 +1,259 @@
1
+ import { load } from 'cheerio';
2
+ import { httpGet, getText } from '../../utils/http.js';
3
+ import { extractMegaPlay } from '../../extractors/megaplay.js';
4
+ import { extractVidWish } from '../../extractors/vidwish.js';
5
+ const BASE_URL = 'https://anikototv.to';
6
+ // --- Internal Parsing Helpers ---
7
+ function parseHomeAnimeSections(html, selector) {
8
+ const $ = load(html);
9
+ const items = [];
10
+ $(selector).each((_, element) => {
11
+ const ep = $(element).find('.ep-status');
12
+ const href = $(element).find('div.info > a').attr('href');
13
+ const id = href?.includes('ep') ? href.split('/').at(-2) : href?.split('/').at(-1);
14
+ items.push({
15
+ id: id || '',
16
+ title: $(element).find('div.info > a').text().trim() || '',
17
+ romaji: $(element).find('div.info > a').attr('data-jp') || '',
18
+ image: $(element).find('img').attr('src') || '',
19
+ type: $(element).find('.meta .right').text().trim() || '',
20
+ episodes_sub: Number(ep.filter('.sub').text().trim()) || 0,
21
+ episodes_dub: Number(ep.filter('.dub').text().trim()) || 0,
22
+ });
23
+ });
24
+ return items;
25
+ }
26
+ function parseNewAddedItems(html, selector) {
27
+ const $ = load(html);
28
+ const items = [];
29
+ $(selector).each((_, element) => {
30
+ items.push({
31
+ id: $(element).attr('href')?.split('/').filter(Boolean).at(-1) || '',
32
+ title: $(element).find('.name.d-title').text().trim() || '',
33
+ romaji: $(element).find('.name.d-title').attr('data-jp') || '',
34
+ image: $(element).find('.poster img').attr('src') || '',
35
+ type: $(element).find('.meta.one-line .dot:nth-child(2)').text().trim() || '',
36
+ });
37
+ });
38
+ return items;
39
+ }
40
+ function parseTopAnimeSegment(html, tabName) {
41
+ const $ = load(html);
42
+ const selector = `#top-anime .tab-content[data-name="${tabName}"] .scaff.items .item`;
43
+ const results = [];
44
+ $(selector).each((_, element) => {
45
+ results.push({
46
+ id: $(element).attr('href')?.split('/').at(-1) || '',
47
+ title: $(element).find('.name').text().trim() || '',
48
+ romaji: $(element).find('.name').attr('data-jp')?.trim() || '',
49
+ image: $(element).find('.poster img').attr('src') ?? '',
50
+ type: $(element).find('.meta .dot:not(.ep-wrap)').first().text().trim() || '',
51
+ });
52
+ });
53
+ return results;
54
+ }
55
+ function parsePaginatedSections(html) {
56
+ const $ = load(html);
57
+ const selector = 'div#list-items > div.item';
58
+ const anime = [];
59
+ $(selector).each((_, element) => {
60
+ const href = $(element).find('div.ani.poster.tip > a').attr('href');
61
+ const id = href?.includes('ep') ? href.split('/').at(-2) : href?.split('/').at(-1);
62
+ anime.push({
63
+ id: id || '',
64
+ title: $(element).find('div.b1 > a.name.d-title').text().trim() || '',
65
+ romaji: $(element).find('div.b1 > a.name.d-title').attr('data-jp') || '',
66
+ image: $(element).find('div.ani.poster.tip img').attr('src') || '',
67
+ type: $(element).find('div.meta div.right').text().trim() || '',
68
+ });
69
+ });
70
+ const activeText = $('.pagination .page-item.active .page-link').first().text().trim();
71
+ const currentPage = Number(activeText) || 1;
72
+ const lastPageText = $('.pagination .page-item a[title="Last"]').attr('href')?.match(/page=(\d+)/)?.[1];
73
+ const lastPage = lastPageText ? Number(lastPageText) : null;
74
+ const nextHref = $('.pagination .page-item a[rel="next"]').attr('href');
75
+ const hasNextPage = Boolean(nextHref) || (lastPage !== null && Number.isFinite(lastPage) && currentPage < lastPage);
76
+ return { hasNextPage, currentPage, totalPages: lastPage || currentPage, results: anime };
77
+ }
78
+ // --- Exported Methods ---
79
+ export async function fetchHome() {
80
+ const result = await getText(`${BASE_URL}/home`);
81
+ const $ = load(result);
82
+ const spotlight = [];
83
+ $('div#hotest > div.swiper-wrapper > div.swiper-slide.item').each((_, element) => {
84
+ const icons = $(element).find('div.meta.icons');
85
+ spotlight.push({
86
+ id: $(element).find('div.actions > a').attr('href')?.split('/').at(-1) || '',
87
+ title: $(element).find('div.info > h2.title.d-title').text().trim() || '',
88
+ romaji: $(element).find('div.info > h2.title.d-title').attr('data-jp') || '',
89
+ image: $(element).find('div.image > div').attr('style')?.match(/url\(['"]?(.*?)['"]?\)/)?.[1] ?? '',
90
+ description: $(element).find('div.synopsis').text().trim() || '',
91
+ });
92
+ });
93
+ return {
94
+ spotlight,
95
+ recentlyUpdated: parseHomeAnimeSections(result, 'section#recent-update div.ani.items > div.item'),
96
+ upcoming: parseHomeAnimeSections(result, 'section#upcoming-anime div.ani.items > div.item'),
97
+ sections: {
98
+ recentlyAdded: parseNewAddedItems(result, 'section.top-table[data-name="new-added"] div.body .scaff.items a.item'),
99
+ recentlyReleased: parseNewAddedItems(result, 'section.top-table[data-name="new-release"] div.body .scaff.items a.item'),
100
+ recentlyCompleted: parseNewAddedItems(result, 'section.top-table[data-name="completed"] div.body .scaff.items a.item'),
101
+ },
102
+ topAnime: {
103
+ day: parseTopAnimeSegment(result, 'day'),
104
+ week: parseTopAnimeSegment(result, 'week'),
105
+ month: parseTopAnimeSegment(result, 'month'),
106
+ }
107
+ };
108
+ }
109
+ export async function fetchSchedule(timezone) {
110
+ const res = await httpGet(`${BASE_URL}/ajax/schedule?tz=${timezone}`, { 'X-Requested-With': 'XMLHttpRequest' });
111
+ const json = await res.json();
112
+ const $ = load(json.result);
113
+ const days = [];
114
+ const dayHeaders = [];
115
+ $('#schedule .days .day').each((_, element) => {
116
+ const $day = $(element);
117
+ const dayName = $day.find('.inner .wday').text().trim() || $day.find('.wday').text().trim();
118
+ const date = $day.find('.inner .date').text().trim() || $day.find('.date').text().trim();
119
+ if (!dayName && !date)
120
+ return;
121
+ dayHeaders.push({ day: dayName, date });
122
+ });
123
+ const animes = [];
124
+ $('#schedule .body .items .item').each((_, el) => {
125
+ const $el = $(el);
126
+ const href = $el.attr('href') ?? '';
127
+ const slug = href.replace(/^https?:\/\/[^/]+/, '').replace(/^\/watch\//, '').replace(/\/ep-\d+$/, '').replace(/\/$/, '');
128
+ animes.push({
129
+ id: slug,
130
+ title: $el.find('.title, .d-title').text().trim(),
131
+ episode: $el.find('.ep span').text().trim() || '',
132
+ time: $el.find('.time').text().trim() || '',
133
+ });
134
+ });
135
+ const perDay = Math.ceil(animes.length / Math.max(1, dayHeaders.length));
136
+ dayHeaders.forEach((d, i) => {
137
+ days.push({ ...d, anime: animes.slice(i * perDay, (i + 1) * perDay) });
138
+ });
139
+ return { data: days };
140
+ }
141
+ export async function search(query, page = 1) {
142
+ const url = page > 1 ? `${BASE_URL}/filter?keyword=${query}?page=${page}` : `${BASE_URL}/filter?keyword=${query}`;
143
+ const html = await getText(url);
144
+ return parsePaginatedSections(html);
145
+ }
146
+ export async function searchSuggestions(query) {
147
+ const res = await httpGet(`${BASE_URL}/ajax/anime/search?keyword=${query}`, { 'X-Requested-With': 'XMLHttpRequest' });
148
+ const result = await res.json();
149
+ const $ = load(result.result.html);
150
+ const items = [];
151
+ $('.scaff.items a.item').each((_, element) => {
152
+ const href = $(element).attr('href') || '';
153
+ items.push({
154
+ id: href.split('/watch/').pop() || '',
155
+ title: $(element).find('.name.d-title').text().trim() || '',
156
+ romaji: $(element).find('.name.d-title').attr('data-jp') || '',
157
+ image: $(element).find('.poster img').attr('src') || '',
158
+ });
159
+ });
160
+ return { data: items };
161
+ }
162
+ export async function fetchAnimeInfo(id) {
163
+ const html = await getText(`${BASE_URL}/watch/${id}`);
164
+ const $ = load(html);
165
+ const root = $('div#w-info div.binfo');
166
+ const numericId = $('div#watch-main').attr('data-id');
167
+ const seriesId = $('div#watch-main').attr('data-url')?.split('/').at(-1);
168
+ const info = {
169
+ id: seriesId || id,
170
+ title: root.find('div.info > h1.title.d-title').text().trim() || '',
171
+ romaji: root.find('div.info > h1.title.d-title').attr('data-jp') || '',
172
+ image: root.find('div.poster > span > img').attr('src') || '',
173
+ description: root.find('div.content').text().trim() || '',
174
+ genres: root.find('div.meta > div').filter((_, el) => $(el).text().includes('Genres:')).find('a').map((_, el) => $(el).text().trim()).get(),
175
+ status: root.find('div.meta > div').filter((_, el) => $(el).text().includes('Status:')).find('span').text().trim() || '',
176
+ episodes: []
177
+ };
178
+ if (numericId) {
179
+ const epRes = await httpGet(`${BASE_URL}/ajax/episode/list/${numericId}?vrf=`, { 'X-Requested-With': 'XMLHttpRequest' });
180
+ const epJson = await epRes.json();
181
+ const $ep = load(epJson.result);
182
+ $ep('div.body ul.ep-range li').each((_, element) => {
183
+ const $a = $ep(element).find('a');
184
+ info.episodes.push({
185
+ id: $a.attr('data-ids')?.replace('&eps=', '-episode-') || '',
186
+ number: Number($a.attr('data-num')) || 0,
187
+ title: $ep(element).attr('title') || '',
188
+ hasSub: Number($a.attr('data-sub')) === 1,
189
+ hasDub: Number($a.attr('data-dub')) === 1,
190
+ });
191
+ });
192
+ }
193
+ return info;
194
+ }
195
+ export async function fetchMostPopular(page = 1) { return parsePaginatedSections(await getText(`${BASE_URL}/most-viewed${page > 1 ? '?page=' + page : ''}`)); }
196
+ export async function fetchRecentlyUpdated(page = 1) { return parsePaginatedSections(await getText(`${BASE_URL}/latest-updated${page > 1 ? '?page=' + page : ''}`)); }
197
+ export async function fetchRecentlyAdded(page = 1) { return parsePaginatedSections(await getText(`${BASE_URL}/new-release${page > 1 ? '?page=' + page : ''}`)); }
198
+ export async function fetchUpcoming(page = 1) { return parsePaginatedSections(await getText(`${BASE_URL}/status/not-yet-aired${page > 1 ? '?page=' + page : ''}`)); }
199
+ export async function fetchReleasing(page = 1) { return parsePaginatedSections(await getText(`${BASE_URL}/status/currently-airing${page > 1 ? '?page=' + page : ''}`)); }
200
+ export async function fetchRecentlyCompleted(page = 1) { return parsePaginatedSections(await getText(`${BASE_URL}/status/finished-airing${page > 1 ? '?page=' + page : ''}`)); }
201
+ export async function fetchAtoZList(sort, page = 1) {
202
+ const cat = sort ? (sort.length === 1 ? sort.toUpperCase() : '0-9') : undefined;
203
+ const base = cat ? `az-list/${cat}` : `az-list`;
204
+ return parsePaginatedSections(await getText(`${BASE_URL}/${base}${page > 1 ? '?page=' + page : ''}`));
205
+ }
206
+ export async function fetchServers(episodeId) {
207
+ const res = await httpGet(`${BASE_URL}/ajax/server/list?servers=${episodeId}`, { 'X-Requested-With': 'XMLHttpRequest' });
208
+ const json = await res.json();
209
+ const $ = load(json.result);
210
+ const servers = { sub: [], dub: [], raw: [], episodeNumber: 0 };
211
+ const episodeText = $('div.tip b').first().text().trim();
212
+ const match = episodeText.match(/(\d+)/);
213
+ if (match)
214
+ servers.episodeNumber = parseInt(match[1], 10);
215
+ $('div.servers div.type').each((_, element) => {
216
+ const $type = $(element);
217
+ const type = $type.attr('data-type');
218
+ $type.find('ul li').each((_, li) => {
219
+ const $li = $(li);
220
+ const server = {
221
+ serverId: $li.attr('data-sv-id'),
222
+ serverName: $li.text().trim().toLowerCase(),
223
+ mediaId: $li.attr('data-cmid'),
224
+ eid: $li.attr('data-link-id'),
225
+ };
226
+ if (type === 'sub')
227
+ servers.sub.push(server);
228
+ else if (type === 'dub')
229
+ servers.dub.push(server);
230
+ else
231
+ servers.raw.push(server);
232
+ });
233
+ });
234
+ return servers;
235
+ }
236
+ export async function fetchSources(episodeId, version = 'sub', serverName = 'vidstream-2') {
237
+ if (episodeId.startsWith('http')) {
238
+ const serverUrl = new URL(episodeId);
239
+ if (serverName === 'vidcloud-1') {
240
+ return extractVidWish(serverUrl, `${BASE_URL}/`);
241
+ }
242
+ return extractMegaPlay(serverUrl, `${BASE_URL}/`);
243
+ }
244
+ const serverInfo = await fetchServers(episodeId);
245
+ let targetEid = null;
246
+ const list = serverInfo[version] || [];
247
+ for (const s of list) {
248
+ if (s.serverName === serverName || targetEid === null) {
249
+ targetEid = s.eid;
250
+ if (s.serverName === serverName)
251
+ break;
252
+ }
253
+ }
254
+ if (!targetEid)
255
+ throw new Error(`No server found for version ${version}`);
256
+ const res = await httpGet(`${BASE_URL}/ajax/server?get=${targetEid}`, { 'X-Requested-With': 'XMLHttpRequest' });
257
+ const result = await res.json();
258
+ return fetchSources(result.result.url, version, serverName);
259
+ }
@@ -0,0 +1,6 @@
1
+ import { AnimeResult, AnimeInfo, VideoStream } from '../../types/types.js';
2
+ export declare function search(query: string): Promise<{
3
+ results: AnimeResult[];
4
+ }>;
5
+ export declare function fetchAnimeInfo(aliasId: string): Promise<AnimeInfo>;
6
+ export declare function fetchEpisodeSources(episodeId: string, dub?: boolean, malId?: number, episodeNumber?: number): Promise<VideoStream>;
@@ -0,0 +1,107 @@
1
+ import { load } from 'cheerio';
2
+ import { httpGet, getText } from '../../utils/http.js';
3
+ import { encodeAnikuro } from '../../utils/proxy.js';
4
+ import { applyAniSkip, getVideoType } from '../../utils/shared.js';
5
+ const BASE_URL = 'https://www.animegg.org';
6
+ export async function search(query) {
7
+ const res = await httpGet(`${BASE_URL}/search/auto/?q=${encodeURIComponent(query)}`);
8
+ const items = await res.json();
9
+ const results = items.map(it => {
10
+ const img = (it.thumbnailUrl?.startsWith('//')) ? `https:${it.thumbnailUrl}` : it.thumbnailUrl;
11
+ return {
12
+ id: it.url?.startsWith('/') ? `${BASE_URL}${it.url}` : '', // Using full URL as alias/id based on original logic
13
+ title: it.name,
14
+ image: img || '',
15
+ };
16
+ }).filter(it => it.id !== '');
17
+ return { results };
18
+ }
19
+ export async function fetchAnimeInfo(aliasId) {
20
+ // aliasId is the full URL in AnimeGG logic
21
+ const html = await getText(aliasId);
22
+ const $ = load(html);
23
+ const info = {
24
+ id: aliasId,
25
+ title: $('.title-english').text().trim() || $('.title').text().trim(),
26
+ episodes: [],
27
+ };
28
+ const tab = $('.newmanga').first();
29
+ if (!tab.length)
30
+ throw new Error('Could not find the episodes section.');
31
+ tab.children().each((i, elem) => {
32
+ const $div = $(elem).children().first();
33
+ const $a = $div.children().first();
34
+ if (!$div.length || !$a.length)
35
+ return;
36
+ const title = $div.find('.anititle').first().text().trim();
37
+ const href = $a.attr('href');
38
+ const url = href ? `${BASE_URL}${href}` : '';
39
+ if (url) {
40
+ info.episodes.push({
41
+ id: url,
42
+ number: tab.children().length - i,
43
+ title: title.replace('[Filler]', '').trim(),
44
+ hasDub: $div.find('.btn-xs.btn-dubbed').length > 0,
45
+ filler: title.startsWith('[Filler]'),
46
+ });
47
+ }
48
+ });
49
+ info.totalEpisodes = info.episodes.length;
50
+ return info;
51
+ }
52
+ export async function fetchEpisodeSources(episodeId, dub = false, malId, episodeNumber) {
53
+ const html = await getText(episodeId);
54
+ const $ = load(html);
55
+ const videos = $('#videos');
56
+ if (!videos.length)
57
+ throw new Error('Could not find streams!');
58
+ const stream = { sources: [], subtitles: [] };
59
+ let foundStream = false;
60
+ // We use a regular for loop to handle async sequentially or we can use Promise.all
61
+ const promises = [];
62
+ videos.children().each((_, elem) => {
63
+ $(elem).children('a').each((_, item) => {
64
+ const isSub = $(item).attr('data-version') === 'subbed';
65
+ if (dub !== !isSub)
66
+ return;
67
+ const id = $(item).attr('data-id');
68
+ if (!id)
69
+ return;
70
+ const streamPageUrl = `${BASE_URL}/embed/${id}`;
71
+ promises.push((async () => {
72
+ const streamHtml = await getText(streamPageUrl);
73
+ const $stream = load(streamHtml);
74
+ $stream('script').each((_, script) => {
75
+ const body = $(script).html() || '';
76
+ const match = body.match(/var\s+videoSources\s*=\s*(\[\s*[\s\S]*?\s*\]);/m);
77
+ if (!match)
78
+ return;
79
+ const raw = match[1];
80
+ const cleaned = raw.replace(/(\w+):/g, '"$1":').replace(/'/g, '"');
81
+ try {
82
+ const sourceList = JSON.parse(cleaned);
83
+ for (const src of sourceList) {
84
+ const videoUrl = `${BASE_URL}${src.file}`;
85
+ const proxiedUrl = encodeAnikuro(videoUrl, streamPageUrl);
86
+ stream.sources.push({
87
+ url: videoUrl,
88
+ quality: src.label || 'default',
89
+ isM3U8: src.file.includes('.m3u8'),
90
+ type: getVideoType(videoUrl),
91
+ proxiedUrl,
92
+ });
93
+ }
94
+ }
95
+ catch (e) {
96
+ // ignore parse errors
97
+ console.log("error in getting episode sources for animegg", e);
98
+ }
99
+ });
100
+ })());
101
+ });
102
+ });
103
+ await Promise.all(promises);
104
+ if (stream.sources.length === 0)
105
+ throw new Error('No video sources found');
106
+ return applyAniSkip(stream, malId, episodeNumber);
107
+ }
@@ -0,0 +1,6 @@
1
+ import { AnimeResult, AnimeInfo, VideoStream } from '../../types/types.js';
2
+ export declare function search(query: string): Promise<{
3
+ results: AnimeResult[];
4
+ }>;
5
+ export declare function fetchAnimeInfo(aliasId: string): Promise<AnimeInfo>;
6
+ export declare function fetchEpisodeSources(episodeId: string, malId?: number, episodeNumber?: number): Promise<VideoStream>;
@@ -0,0 +1,95 @@
1
+ import { httpGet } from '../../utils/http.js';
2
+ import { encodeAnikuro } from '../../utils/proxy.js';
3
+ import { applyAniSkip, getVideoType } from '../../utils/shared.js';
4
+ let animeOnsenToken = null;
5
+ let tokenExpiration = 0;
6
+ async function checkAndUpdateToken() {
7
+ const currentTime = Date.now() / 1000;
8
+ if (!animeOnsenToken || tokenExpiration < (currentTime + 3600)) {
9
+ const res = await fetch('https://auth.animeonsen.xyz/oauth/token', {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/x-www-form-urlencoded',
13
+ },
14
+ body: new URLSearchParams({
15
+ client_id: 'f296be26-28b5-4358-b5a1-6259575e23b7',
16
+ client_secret: '349038c4157d0480784753841217270c3c5b35f4281eaee029de21cb04084235',
17
+ grant_type: 'client_credentials'
18
+ }).toString()
19
+ });
20
+ if (!res.ok)
21
+ throw new Error('Could not generate AO token');
22
+ const json = await res.json();
23
+ animeOnsenToken = json.access_token;
24
+ tokenExpiration = json.expires_in + currentTime;
25
+ }
26
+ }
27
+ export async function search(query) {
28
+ await checkAndUpdateToken();
29
+ const cleanQuery = query.replace(/-/g, '');
30
+ const url = `https://api.animeonsen.xyz/v4/search/${encodeURIComponent(cleanQuery)}`;
31
+ const res = await httpGet(url, {
32
+ 'Authorization': `Bearer ${animeOnsenToken}`
33
+ });
34
+ const json = await res.json();
35
+ const results = json.result.map(item => ({
36
+ id: item.content_id,
37
+ title: item.content_title_en ?? item.content_title,
38
+ image: `https://api.animeonsen.xyz/v4/image/210x300/${item.content_id}`,
39
+ }));
40
+ return { results };
41
+ }
42
+ export async function fetchAnimeInfo(aliasId) {
43
+ await checkAndUpdateToken();
44
+ const url = `https://api.animeonsen.xyz/v4/content/${aliasId}/episodes`;
45
+ const res = await httpGet(url, {
46
+ 'Authorization': `Bearer ${animeOnsenToken}`
47
+ });
48
+ const json = await res.json();
49
+ const info = {
50
+ id: aliasId,
51
+ title: '',
52
+ episodes: []
53
+ };
54
+ let i = 1;
55
+ for (const [key, item] of Object.entries(json)) {
56
+ const epTitle = item.contentTitle_episode_en;
57
+ info.episodes.push({
58
+ id: `${key}+${aliasId}`,
59
+ number: parseInt(key, 10) || i,
60
+ title: epTitle || '',
61
+ });
62
+ i++;
63
+ }
64
+ info.totalEpisodes = info.episodes.length;
65
+ return info;
66
+ }
67
+ export async function fetchEpisodeSources(episodeId, malId, episodeNumber) {
68
+ const [episodeNum, animeId] = episodeId.split('+');
69
+ if (!animeId || !episodeNum)
70
+ throw new Error('Invalid episodeId format for AnimeOnsen (expected epNum+animeId)');
71
+ const baseUrl = `https://cdn.animeonsen.xyz/video/mp4-dash/${animeId}/${episodeNum}/manifest.mpd`;
72
+ const subtitleUrl = `https://api.animeonsen.xyz/v4/subtitles/${animeId}/en-US/${episodeNum}`;
73
+ const proxiedUrl = encodeAnikuro(baseUrl, 'https://www.animeonsen.xyz/');
74
+ const stream = {
75
+ sources: [
76
+ {
77
+ url: baseUrl,
78
+ quality: 'single',
79
+ isM3U8: false,
80
+ proxiedUrl,
81
+ type: getVideoType(baseUrl)
82
+ }
83
+ ],
84
+ subtitles: [
85
+ {
86
+ url: subtitleUrl,
87
+ lang: 'English',
88
+ }
89
+ ],
90
+ headers: {
91
+ 'Referer': 'https://www.animeonsen.xyz/'
92
+ }
93
+ };
94
+ return applyAniSkip(stream, malId, episodeNumber);
95
+ }
@@ -0,0 +1,6 @@
1
+ import { AnimeResult, AnimeInfo, VideoStream } from '../../types/types.js';
2
+ export declare function search(query: string): Promise<{
3
+ results: AnimeResult[];
4
+ }>;
5
+ export declare function fetchAnimeInfo(aliasId: string): Promise<AnimeInfo>;
6
+ export declare function fetchEpisodeSources(episodeId: string, dub?: boolean, malId?: number, episodeNumber?: number): Promise<VideoStream>;
@@ -0,0 +1,102 @@
1
+ import { load } from 'cheerio';
2
+ import { httpGet, getText } from '../../utils/http.js';
3
+ import { extractKwik } from '../../extractors/kwik.js';
4
+ import { applyAniSkip } from '../../utils/shared.js';
5
+ const BASE_URL = 'https://animepahe.pw';
6
+ const HEADERS = {
7
+ 'Cookie': '__ddg1=;__ddg2_=',
8
+ 'Referer': `${BASE_URL}/`,
9
+ };
10
+ export async function search(query) {
11
+ const cleanQuery = query.replace(/-/g, '');
12
+ const url = `${BASE_URL}/api?m=search&q=${encodeURIComponent(cleanQuery)}`;
13
+ const res = await httpGet(url, HEADERS);
14
+ const json = await res.json();
15
+ const results = (json.data || []).map(result => ({
16
+ id: result.session,
17
+ title: result.title || '',
18
+ image: result.poster || '',
19
+ }));
20
+ return { results };
21
+ }
22
+ export async function fetchAnimeInfo(aliasId) {
23
+ const info = {
24
+ id: aliasId,
25
+ title: '',
26
+ episodes: [],
27
+ };
28
+ let list = [];
29
+ const url = `${BASE_URL}/api?m=release&id=${aliasId}&sort=episode_asc`;
30
+ const firstRes = await httpGet(url, HEADERS);
31
+ const firstJson = await firstRes.json();
32
+ list = list.concat(firstJson.data || []);
33
+ const totalPages = firstJson.last_page || 1;
34
+ for (let i = 1; i < totalPages; i++) {
35
+ if (i > 6)
36
+ break; // limit to 6 pages based on original code
37
+ const res = await httpGet(`${url}&page=${i + 1}`, HEADERS);
38
+ const json = await res.json();
39
+ list = list.concat(json.data || []);
40
+ }
41
+ const totalEps = list.length;
42
+ for (let i = 0; i < totalEps; i++) {
43
+ const item = list[i];
44
+ const episodeLink = `${BASE_URL}/play/${aliasId}/${item.session}`;
45
+ info.episodes.push({
46
+ id: episodeLink,
47
+ number: totalEps - i,
48
+ title: item.title || '',
49
+ image: item.snapshot || '',
50
+ hasDub: item.audio !== 'jpn',
51
+ filler: item.filler !== 0,
52
+ });
53
+ }
54
+ // Return episodes reversed (latest first) as per the original dart code
55
+ info.episodes = info.episodes.reverse();
56
+ info.totalEpisodes = info.episodes.length;
57
+ return info;
58
+ }
59
+ export async function fetchEpisodeSources(episodeId, dub = false, malId, episodeNumber) {
60
+ const html = await getText(episodeId, HEADERS);
61
+ const $ = load(html);
62
+ const links = [];
63
+ $('div#resolutionMenu > button').each((_, el) => {
64
+ const link = $(el).attr('data-src') || '';
65
+ const text = $(el).text();
66
+ const parts = text.split('·');
67
+ if (parts.length < 2)
68
+ return;
69
+ const server = parts[0].trim();
70
+ const quality = parts[1].trim();
71
+ const hasDub = quality.split(' ').includes('eng');
72
+ if (dub === hasDub) {
73
+ links.push({ link, server, quality });
74
+ }
75
+ });
76
+ const stream = { sources: [], subtitles: [] };
77
+ const promises = links.map(async (e) => {
78
+ try {
79
+ const extracted = await extractKwik(e.link);
80
+ if (extracted.sources.length > 0) {
81
+ // Usually Kwik only returns one source but we map it just in case
82
+ for (const src of extracted.sources) {
83
+ stream.sources.push({
84
+ url: src.url,
85
+ quality: e.quality,
86
+ isM3U8: src.isM3U8,
87
+ proxiedUrl: src.proxiedUrl,
88
+ type: src.type
89
+ });
90
+ }
91
+ }
92
+ }
93
+ catch (error) {
94
+ // Ignore kwik extraction errors for specific servers
95
+ console.log("error in fetching animepahe sources", error);
96
+ }
97
+ });
98
+ await Promise.all(promises);
99
+ if (stream.sources.length === 0)
100
+ throw new Error('No video sources found');
101
+ return applyAniSkip(stream, malId, episodeNumber);
102
+ }
@@ -0,0 +1,8 @@
1
+ import { AnimeResult, AnimeInfo, PageResult, VideoStream } from '../../types/types.js';
2
+ export declare function search(query: string): Promise<PageResult<AnimeResult>>;
3
+ export declare function fetchAnimeInfo(id: string): Promise<AnimeInfo>;
4
+ export declare function fetchEpisodeServers(episodeId: string): Promise<{
5
+ name: string;
6
+ url: string;
7
+ }[]>;
8
+ export declare function fetchEpisodeSources(episodeId: string, malId?: number, episodeNumber?: number): Promise<VideoStream>;