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.
- package/LICENSE +0 -0
- package/README.md +105 -0
- package/dist/extractors/kwik.d.ts +6 -0
- package/dist/extractors/kwik.js +33 -0
- package/dist/extractors/megaplay.d.ts +2 -0
- package/dist/extractors/megaplay.js +32 -0
- package/dist/extractors/streamwish.d.ts +4 -0
- package/dist/extractors/streamwish.js +86 -0
- package/dist/extractors/vidtube.d.ts +2 -0
- package/dist/extractors/vidtube.js +24 -0
- package/dist/extractors/vidwish.d.ts +2 -0
- package/dist/extractors/vidwish.js +32 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/lib/config.d.ts +22 -0
- package/dist/lib/config.js +21 -0
- package/dist/providers/anime/anidb.d.ts +6 -0
- package/dist/providers/anime/anidb.js +88 -0
- package/dist/providers/anime/anikoto.d.ts +41 -0
- package/dist/providers/anime/anikoto.js +259 -0
- package/dist/providers/anime/animegg.d.ts +6 -0
- package/dist/providers/anime/animegg.js +107 -0
- package/dist/providers/anime/animeonsen.d.ts +6 -0
- package/dist/providers/anime/animeonsen.js +95 -0
- package/dist/providers/anime/animepahe.d.ts +6 -0
- package/dist/providers/anime/animepahe.js +102 -0
- package/dist/providers/anime/animesaturn.d.ts +8 -0
- package/dist/providers/anime/animesaturn.js +160 -0
- package/dist/providers/anime/animeunity.d.ts +4 -0
- package/dist/providers/anime/animeunity.js +108 -0
- package/dist/providers/anime/anizone.d.ts +12 -0
- package/dist/providers/anime/anizone.js +146 -0
- package/dist/providers/anime/gojo.d.ts +6 -0
- package/dist/providers/anime/gojo.js +83 -0
- package/dist/providers/meta/anilist/anilist.d.ts +28 -0
- package/dist/providers/meta/anilist/anilist.js +263 -0
- package/dist/providers/meta/anilist/queries.d.ts +22 -0
- package/dist/providers/meta/anilist/queries.js +405 -0
- package/dist/providers/meta/anilist/types.d.ts +213 -0
- package/dist/providers/meta/anilist/types.js +21 -0
- package/dist/providers/meta/anilist.d.ts +15 -0
- package/dist/providers/meta/anilist.js +94 -0
- package/dist/types/types.d.ts +88 -0
- package/dist/types/types.js +4 -0
- package/dist/utils/http.d.ts +13 -0
- package/dist/utils/http.js +39 -0
- package/dist/utils/proxy.d.ts +9 -0
- package/dist/utils/proxy.js +43 -0
- package/dist/utils/shared.d.ts +15 -0
- package/dist/utils/shared.js +64 -0
- package/dist/utils/unpack.d.ts +9 -0
- package/dist/utils/unpack.js +59 -0
- package/package.json +34 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { load } from 'cheerio';
|
|
2
|
+
import { 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.animesaturn.net/';
|
|
6
|
+
export async function search(query) {
|
|
7
|
+
const html = await getText(`${BASE_URL}animelist?search=${encodeURIComponent(query)}`);
|
|
8
|
+
const $ = load(html);
|
|
9
|
+
const results = [];
|
|
10
|
+
$('ul.list-group li').each((_, element) => {
|
|
11
|
+
const id = $(element).find('a.thumb').attr('href')?.split('/').pop();
|
|
12
|
+
if (id) {
|
|
13
|
+
results.push({
|
|
14
|
+
id,
|
|
15
|
+
title: $(element).find('h3 a').text().trim(),
|
|
16
|
+
image: $(element).find('img.copertina-archivio').attr('src') || '',
|
|
17
|
+
url: $(element).find('h3 a').attr('href') || '',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return { hasNextPage: false, results };
|
|
22
|
+
}
|
|
23
|
+
export async function fetchAnimeInfo(id) {
|
|
24
|
+
const html = await getText(`${BASE_URL}anime/${id}`);
|
|
25
|
+
const $ = load(html);
|
|
26
|
+
const info = {
|
|
27
|
+
id,
|
|
28
|
+
title: $('div.container.anime-title-as > b').text().trim(),
|
|
29
|
+
genres: $('div.container a.badge.badge-light').map((_, el) => $(el).text().trim()).get(),
|
|
30
|
+
image: $('img.img-fluid').attr('src') || '',
|
|
31
|
+
coverImage: $('div.banner').attr('style')?.match(/background:\s*url\(['"]?([^'")]+)['"]?\)/i)?.[1] || '',
|
|
32
|
+
description: $('#full-trama').text().trim(),
|
|
33
|
+
episodes: [],
|
|
34
|
+
};
|
|
35
|
+
$('.tab-pane.fade').each((_, element) => {
|
|
36
|
+
$(element).find('.bottone-ep').each((_, epElement) => {
|
|
37
|
+
const link = $(epElement).attr('href');
|
|
38
|
+
const episodeNumber = $(epElement).text().trim().replace('Episodio ', '').trim();
|
|
39
|
+
info.episodes.push({
|
|
40
|
+
id: link?.split('/').pop() || '',
|
|
41
|
+
number: parseInt(episodeNumber, 10) || 0,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
info.episodes.sort((a, b) => a.number - b.number);
|
|
46
|
+
info.totalEpisodes = info.episodes.length;
|
|
47
|
+
return info;
|
|
48
|
+
}
|
|
49
|
+
export async function fetchEpisodeServers(episodeId) {
|
|
50
|
+
const html = await getText(`${BASE_URL}ep/${episodeId}`, {
|
|
51
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0',
|
|
52
|
+
Referer: BASE_URL,
|
|
53
|
+
});
|
|
54
|
+
const $ = load(html);
|
|
55
|
+
const servers = [];
|
|
56
|
+
const mainWatchUrl = $("a:contains('Guarda lo streaming')").attr('href');
|
|
57
|
+
if (mainWatchUrl) {
|
|
58
|
+
servers.push({ name: 'Server 1', url: mainWatchUrl });
|
|
59
|
+
const watchHtml = await getText(mainWatchUrl, {
|
|
60
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0',
|
|
61
|
+
Referer: `${BASE_URL}ep/${episodeId}`,
|
|
62
|
+
});
|
|
63
|
+
const $watch = load(watchHtml);
|
|
64
|
+
$watch('.dropdown-menu .dropdown-item').each((_, el) => {
|
|
65
|
+
const serverUrl = $watch(el).attr('href');
|
|
66
|
+
const serverName = $watch(el).text().trim();
|
|
67
|
+
if (serverUrl && serverName && !servers.some(s => s.url === serverUrl)) {
|
|
68
|
+
servers.push({ name: serverName, url: serverUrl });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const altPlayerUrl = $watch("a:contains('Player alternativo')").attr('href');
|
|
72
|
+
if (altPlayerUrl && !servers.some(s => s.url === altPlayerUrl)) {
|
|
73
|
+
servers.push({ name: 'Player Alternativo', url: altPlayerUrl });
|
|
74
|
+
}
|
|
75
|
+
$watch('iframe').each((_, el) => {
|
|
76
|
+
const src = $watch(el).attr('src');
|
|
77
|
+
if (src && (src.includes('streamtape') || src.includes('mixdrop') || src.includes('doodstream'))) {
|
|
78
|
+
const serverName = src.includes('streamtape') ? 'StreamTape' : src.includes('mixdrop') ? 'MixDrop' : 'DoodStream';
|
|
79
|
+
if (!servers.some(s => s.url === src)) {
|
|
80
|
+
servers.push({ name: serverName, url: src });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return servers;
|
|
86
|
+
}
|
|
87
|
+
export async function fetchEpisodeSources(episodeId, malId, episodeNumber) {
|
|
88
|
+
const html = await getText(`${BASE_URL}ep/${episodeId}`, {
|
|
89
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0',
|
|
90
|
+
Referer: BASE_URL,
|
|
91
|
+
});
|
|
92
|
+
const $episode = load(html);
|
|
93
|
+
let watchUrl = $episode("a:contains('Guarda lo streaming')").attr('href') ||
|
|
94
|
+
$episode("div:contains('Guarda lo streaming')").parent('a').attr('href') ||
|
|
95
|
+
$episode("a[href*='watch']").attr('href');
|
|
96
|
+
if (!watchUrl)
|
|
97
|
+
throw new Error('Watch URL not found');
|
|
98
|
+
const watchHtml = await getText(watchUrl, {
|
|
99
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0',
|
|
100
|
+
Referer: `${BASE_URL}ep/${episodeId}`,
|
|
101
|
+
});
|
|
102
|
+
const $watch = load(watchHtml);
|
|
103
|
+
const stream = {
|
|
104
|
+
sources: [],
|
|
105
|
+
subtitles: [],
|
|
106
|
+
headers: {
|
|
107
|
+
Referer: watchUrl,
|
|
108
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
$watch('video source').each((_, element) => {
|
|
112
|
+
const src = $watch(element).attr('src');
|
|
113
|
+
const proxiedUrl = encodeAnikuro(src, watchUrl);
|
|
114
|
+
if (src && (src.includes('.mp4') || src.includes('.m3u8'))) {
|
|
115
|
+
stream.sources.push({ url: src, isM3U8: src.includes('.m3u8'), quality: 'default', proxiedUrl, type: getVideoType(src) });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
const videoSrc = $watch('video#myvideo').attr('src');
|
|
119
|
+
const proxiedUrl = encodeAnikuro(videoSrc, watchUrl);
|
|
120
|
+
if (videoSrc && (videoSrc.includes('.mp4') || videoSrc.includes('.m3u8')) && !stream.sources.some(s => s.url === videoSrc)) {
|
|
121
|
+
stream.sources.push({ url: videoSrc, isM3U8: videoSrc.includes('.m3u8'), quality: 'default', proxiedUrl, type: getVideoType(videoSrc) });
|
|
122
|
+
}
|
|
123
|
+
$watch('script').each((_, element) => {
|
|
124
|
+
const scriptText = $watch(element).text();
|
|
125
|
+
if (scriptText.includes('jwplayer') || scriptText.includes('file:')) {
|
|
126
|
+
const lines = scriptText.split('\n');
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
if (line.includes('file:')) {
|
|
129
|
+
const url = line.split('file:')[1].trim().replace(/['"]/g, '').replace(/,/g, '').trim();
|
|
130
|
+
const proxiedUrl = encodeAnikuro(url, watchUrl);
|
|
131
|
+
if (url && (url.includes('.mp4') || url.includes('.m3u8')) && !stream.sources.some(s => s.url === url)) {
|
|
132
|
+
stream.sources.push({ url, isM3U8: url.includes('.m3u8'), quality: 'default', proxiedUrl, type: getVideoType(url) });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const mp4Match = scriptText.match(/https?:\/\/[^"'\s]+\.mp4[^"'\s]*/g);
|
|
138
|
+
mp4Match?.forEach(url => {
|
|
139
|
+
const proxiedUrl = encodeAnikuro(url, watchUrl);
|
|
140
|
+
if (!stream.sources.some(s => s.url === url))
|
|
141
|
+
stream.sources.push({ url, isM3U8: false, quality: 'default', proxiedUrl, type: "mp4" });
|
|
142
|
+
});
|
|
143
|
+
const m3u8Match = scriptText.match(/https?:\/\/[^"'\s]+\.m3u8[^"'\s]*/g);
|
|
144
|
+
m3u8Match?.forEach(url => {
|
|
145
|
+
const proxiedUrl = encodeAnikuro(url, watchUrl);
|
|
146
|
+
if (!stream.sources.some(s => s.url === url))
|
|
147
|
+
stream.sources.push({ url, isM3U8: true, quality: 'default', proxiedUrl, type: "hls" });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
if (stream.sources.length === 0)
|
|
151
|
+
throw new Error('No video sources found');
|
|
152
|
+
const m3u8Source = stream.sources.find(s => s.isM3U8);
|
|
153
|
+
if (m3u8Source && m3u8Source.url.includes('playlist.m3u8')) {
|
|
154
|
+
stream.subtitles.push({
|
|
155
|
+
url: m3u8Source.url.replace('playlist.m3u8', 'subtitles.vtt'),
|
|
156
|
+
lang: 'Italian',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return applyAniSkip(stream, malId, episodeNumber);
|
|
160
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
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, page?: number): Promise<AnimeInfo>;
|
|
4
|
+
export declare function fetchEpisodeSources(episodeId: string, malId?: number, episodeNumber?: number): Promise<VideoStream>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { load } from 'cheerio';
|
|
2
|
+
import { 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.animeunity.to';
|
|
6
|
+
export async function search(query) {
|
|
7
|
+
const html = await getText(`${BASE_URL}/archivio?title=${encodeURIComponent(query)}`);
|
|
8
|
+
const $ = load(html);
|
|
9
|
+
const records = $('archivio').attr('records');
|
|
10
|
+
if (!records)
|
|
11
|
+
return { hasNextPage: false, results: [] };
|
|
12
|
+
const items = JSON.parse(records);
|
|
13
|
+
const results = [];
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
results.push({
|
|
16
|
+
id: `${item.id}-${item.slug}`,
|
|
17
|
+
title: item.title ?? item.title_eng,
|
|
18
|
+
url: `${BASE_URL}/anime/${item.id}-${item.slug}`,
|
|
19
|
+
image: item.imageurl,
|
|
20
|
+
coverImage: item.imageurl_cover,
|
|
21
|
+
rating: parseFloat(item.score),
|
|
22
|
+
releaseDate: item.date,
|
|
23
|
+
hasDub: item.dub,
|
|
24
|
+
hasSub: !item.dub,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return { hasNextPage: false, results };
|
|
28
|
+
}
|
|
29
|
+
export async function fetchAnimeInfo(id, page = 1) {
|
|
30
|
+
const url = `${BASE_URL}/anime/${id}`;
|
|
31
|
+
const html = await getText(url);
|
|
32
|
+
const $ = load(html);
|
|
33
|
+
const totalEpisodes = parseInt($('video-player')?.attr('episodes_count') || '0', 10);
|
|
34
|
+
const totalPages = Math.max(1, Math.round(totalEpisodes / 120) + 1);
|
|
35
|
+
if (page < 1 || page > totalPages) {
|
|
36
|
+
throw new Error(`Argument 'page' for ${id} must be between 1 and ${totalPages}! (You passed ${page})`);
|
|
37
|
+
}
|
|
38
|
+
const animeInfo = {
|
|
39
|
+
id,
|
|
40
|
+
title: $('h1.title')?.text().trim() || '',
|
|
41
|
+
url,
|
|
42
|
+
genres: $('.info-wrapper.pt-3.pb-3 small').map((_, el) => $(el).text().replace(',', '').trim()).get(),
|
|
43
|
+
totalEpisodes,
|
|
44
|
+
image: $('img.cover').attr('src') || '',
|
|
45
|
+
coverImage: $('.banner').attr('src') ?? $('.banner').attr('style')?.replace('background: url(', '').replace(')', ''),
|
|
46
|
+
description: $('.description').text().trim() || '',
|
|
47
|
+
episodes: [],
|
|
48
|
+
};
|
|
49
|
+
const episodesPerPage = 120;
|
|
50
|
+
const lastPageEpisode = page * episodesPerPage;
|
|
51
|
+
const firstPageEpisode = lastPageEpisode - 119;
|
|
52
|
+
const apiHtml = await getText(`${BASE_URL}/info_api/${id}/1?start_range=${firstPageEpisode}&end_range=${lastPageEpisode}`);
|
|
53
|
+
// The response might be JSON depending on how their backend is structured, but consumet parses it as res2.data.episodes
|
|
54
|
+
// In our case getText returns a string, so we must parse it
|
|
55
|
+
try {
|
|
56
|
+
const json = JSON.parse(apiHtml);
|
|
57
|
+
const items = json.episodes || [];
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
animeInfo.episodes.push({
|
|
60
|
+
id: `${id}/${item.id}`,
|
|
61
|
+
number: parseInt(item.number, 10) || 0,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
// Silently fail if JSON parsing fails, meaning no episodes returned or wrong endpoint
|
|
67
|
+
}
|
|
68
|
+
return animeInfo;
|
|
69
|
+
}
|
|
70
|
+
export async function fetchEpisodeSources(episodeId, malId, episodeNumber) {
|
|
71
|
+
const html = await getText(`${BASE_URL}/anime/${episodeId}`);
|
|
72
|
+
const $ = load(html);
|
|
73
|
+
const streamUrl = $('video-player').attr('embed_url');
|
|
74
|
+
if (!streamUrl)
|
|
75
|
+
throw new Error('Stream URL not found');
|
|
76
|
+
const embedHtml = await getText(streamUrl);
|
|
77
|
+
const $embed = load(embedHtml);
|
|
78
|
+
const scriptText = $embed('script:contains("window.video")').text();
|
|
79
|
+
const domain = scriptText.match(/url:\s*'(.*?)'/)?.[1];
|
|
80
|
+
const token = scriptText.match(/token':\s*'(.*?)'/)?.[1] || scriptText.match(/token":\s*"(.*?)"/)?.[1];
|
|
81
|
+
const expires = scriptText.match(/expires':\s*'(.*?)'/)?.[1] || scriptText.match(/expires":\s*"(.*?)"/)?.[1];
|
|
82
|
+
if (!domain || !token || !expires)
|
|
83
|
+
throw new Error('Failed to extract token/domain/expires');
|
|
84
|
+
const defaultUrl = `${domain}${domain.includes('?') ? '&' : '?'}token=${token}&referer=&expires=${expires}&h=1`;
|
|
85
|
+
const m3u8Content = await getText(defaultUrl);
|
|
86
|
+
const stream = {
|
|
87
|
+
sources: [],
|
|
88
|
+
subtitles: [],
|
|
89
|
+
download: $embed('script:contains("window.downloadUrl ")').text().match(/downloadUrl\s*=\s*'(.*?)'/)?.[1],
|
|
90
|
+
};
|
|
91
|
+
if (m3u8Content.includes('EXTM3U')) {
|
|
92
|
+
const videoList = m3u8Content.split('#EXT-X-STREAM-INF:');
|
|
93
|
+
for (const video of videoList) {
|
|
94
|
+
if (video.includes('BANDWIDTH')) {
|
|
95
|
+
const url = video.split('\n')[1]?.trim();
|
|
96
|
+
const resolutionMatch = video.match(/RESOLUTION=\d+x(\d+)/);
|
|
97
|
+
const quality = resolutionMatch ? `${resolutionMatch[1]}p` : 'auto';
|
|
98
|
+
if (url) {
|
|
99
|
+
const proxiedUrl = encodeAnikuro(url, streamUrl);
|
|
100
|
+
stream.sources.push({ url, quality, isM3U8: true, proxiedUrl, type: getVideoType(url) });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const defaultProxiedUrl = encodeAnikuro(defaultUrl, streamUrl);
|
|
106
|
+
stream.sources.push({ url: defaultUrl, quality: 'default', isM3U8: true, proxiedUrl: defaultProxiedUrl, type: "hls" });
|
|
107
|
+
return applyAniSkip(stream, malId, episodeNumber);
|
|
108
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AnimeResult, AnimeInfo, VideoStream } from '../../types/types.js';
|
|
2
|
+
export declare function search(query: string): Promise<{
|
|
3
|
+
data: AnimeResult[];
|
|
4
|
+
}>;
|
|
5
|
+
export declare function fetchUpdates(): Promise<{
|
|
6
|
+
data: any[];
|
|
7
|
+
recentlyAdded: AnimeResult[];
|
|
8
|
+
}>;
|
|
9
|
+
export declare function fetchAnimeInfo(animeId: string): Promise<{
|
|
10
|
+
data: AnimeInfo;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function fetchSources(episodeId: string, malId?: number, episodeNumber?: number): Promise<VideoStream>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { load } from 'cheerio';
|
|
2
|
+
import { getText } from '../../utils/http.js';
|
|
3
|
+
import { encodeAnikuro } from '../../utils/proxy.js';
|
|
4
|
+
import { applyAniSkip } from '../../utils/shared.js';
|
|
5
|
+
const BASE_URL = 'https://anizone.to';
|
|
6
|
+
function createSlug(title) {
|
|
7
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
8
|
+
}
|
|
9
|
+
export async function search(query) {
|
|
10
|
+
const url = new URL(`${BASE_URL}/anime`);
|
|
11
|
+
url.searchParams.append('search', query.trim());
|
|
12
|
+
const html = await getText(url.toString());
|
|
13
|
+
const $ = load(html);
|
|
14
|
+
const selector = 'div.grid.grid-cols-1.gap-4 > div.relative.overflow-hidden.h-26.rounded-lg.px-4.py-3.bg-slate-900.drop-shadow-lg';
|
|
15
|
+
const anime = [];
|
|
16
|
+
$(selector).each((_, element) => {
|
|
17
|
+
const title = $(element).find('div.h-6.inline.truncate > a').text().trim() ||
|
|
18
|
+
$(element).find('div.absolute.-inset-y-0.-right-0.w-80 > img').attr('alt') || '';
|
|
19
|
+
const id = $(element).find('div.h-6.inline.truncate > a').attr('href')?.split('/').at(-1) ||
|
|
20
|
+
$(element).attr('wire:key')?.split('-').at(-1) || '';
|
|
21
|
+
const infoSpans = $(element).find('div.inline.text-xs.h-4.line-clamp-1 span').map((_, el) => $(el).text().trim()).get();
|
|
22
|
+
const genres = $(element).find('div.flex.flex-wrap.gap-2.line-clamp-1.h-6 a').map((_, el) => $(el).text().trim()).get().filter(g => g.toLowerCase() !== 'manga');
|
|
23
|
+
anime.push({
|
|
24
|
+
id: title ? `${createSlug(title)}-${id}` : id,
|
|
25
|
+
title,
|
|
26
|
+
image: $(element).find('div.absolute.-inset-y-0.-right-0.w-80 > img').attr('src') || '',
|
|
27
|
+
type: infoSpans[0] ? (infoSpans[0].toLowerCase().includes('tv') ? 'TV' : infoSpans[0]) : '',
|
|
28
|
+
releaseDate: infoSpans[1] || '',
|
|
29
|
+
status: infoSpans[3] || '',
|
|
30
|
+
genres: genres,
|
|
31
|
+
totalEpisodes: infoSpans[2] ? parseInt(infoSpans[2].replace(/\D/g, ''), 10) : 0,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
return { data: anime };
|
|
35
|
+
}
|
|
36
|
+
export async function fetchUpdates() {
|
|
37
|
+
const html = await getText(`${BASE_URL}/`);
|
|
38
|
+
const $ = load(html);
|
|
39
|
+
const recentlyAdded = [];
|
|
40
|
+
$('div.swiper-wrapper.flex div.space-y-3.pb-6.swiper-slide').each((_, el) => {
|
|
41
|
+
const id = $(el).find('a').first().attr('href')?.split('/').at(-1) || '';
|
|
42
|
+
const title = $(el).find('a[title]').attr('title') || $(el).find('img').attr('alt') || '';
|
|
43
|
+
recentlyAdded.push({
|
|
44
|
+
id: title ? `${createSlug(title)}-${id}` : '',
|
|
45
|
+
title,
|
|
46
|
+
image: $(el).find('a > img').attr('src') || '',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
const latestEpisodes = [];
|
|
50
|
+
$('div.md\\:w-2\\/3.lg\\:w-3\\/4 ul li').each((_, el) => {
|
|
51
|
+
const $el = $(el);
|
|
52
|
+
const episodeNumber = $el.find('a.group').attr('href')?.split('/').at(-1);
|
|
53
|
+
const animeId = $el.find('div .title').first().attr('href')?.split('/').at(-1);
|
|
54
|
+
const title = $el.find('div .title').first().text().trim() || '';
|
|
55
|
+
const teaserMatch = $el.find('img').attr(':src')?.match(/'([^']*teaser\.webp)'/);
|
|
56
|
+
latestEpisodes.push({
|
|
57
|
+
id: title ? `${createSlug(title)}-${animeId}-episode-${episodeNumber}` : '',
|
|
58
|
+
number: episodeNumber ? Number(episodeNumber) : 0,
|
|
59
|
+
title: $el.find('div .title').last().text().trim() || '',
|
|
60
|
+
image: $el.find('img').attr('src') || '',
|
|
61
|
+
teaser: teaserMatch ? teaserMatch[1] : '',
|
|
62
|
+
airDate: $el.find('.flex.flex-row.text-xs span').eq(0).text().trim() || '',
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return { data: latestEpisodes, recentlyAdded };
|
|
66
|
+
}
|
|
67
|
+
export async function fetchAnimeInfo(animeId) {
|
|
68
|
+
const id = animeId.split('-').at(-1);
|
|
69
|
+
const html = await getText(`${BASE_URL}/anime/${id}`);
|
|
70
|
+
const $ = load(html);
|
|
71
|
+
const synopsisHtml = $('.text-sm.md\\:text-base.xl\\:text-lg > div').html() || '';
|
|
72
|
+
const infoSpans = $('.text-slate-100.text-xs.lg\\:text-base.flex.flex-wrap > span');
|
|
73
|
+
const title = $('div.mx-auto img').attr('alt') || $('h1').text().trim();
|
|
74
|
+
const fetchedId = $('div.flex.mt-8 a').attr('href')?.split('/')[4];
|
|
75
|
+
const typeText = $(infoSpans[0]).find('.inline-block').text().trim();
|
|
76
|
+
const epText = $(infoSpans[2]).find('.inline-block').text().trim();
|
|
77
|
+
const animeInfo = {
|
|
78
|
+
id: `${createSlug(title)}-${fetchedId}`,
|
|
79
|
+
title,
|
|
80
|
+
type: typeText.toLowerCase().includes('tv') ? 'TV' : typeText,
|
|
81
|
+
status: $(infoSpans[1]).find('.inline-block').text().trim(),
|
|
82
|
+
image: $('div.mx-auto img').attr('src') || '',
|
|
83
|
+
coverImage: $('div.absolute img').attr('src') || '',
|
|
84
|
+
totalEpisodes: epText ? parseInt(epText.replace(/\D/g, ''), 10) : 0,
|
|
85
|
+
releaseDate: $(infoSpans[3]).find('.inline-block').text().trim(),
|
|
86
|
+
description: synopsisHtml.replace(/<br\s*\/?>/g, '\n').replace(/\n\s*\n/g, '\n').trim(),
|
|
87
|
+
genres: $('.flex-wrap.gap-2.justify-center.lg\\:justify-start a').map((_, el) => $(el).text().trim()).get().filter(g => g.toLowerCase() !== 'manga'),
|
|
88
|
+
episodes: []
|
|
89
|
+
};
|
|
90
|
+
$('ul.grid > li').each((_, el) => {
|
|
91
|
+
const $el = $(el);
|
|
92
|
+
const url = $el.find('a').attr('href');
|
|
93
|
+
const epTitle = $el.find('h3').text().trim();
|
|
94
|
+
const episodeNumber = url ? url.split('/').at(-1) : null;
|
|
95
|
+
animeInfo.episodes.push({
|
|
96
|
+
id: `${animeInfo.id}-episode-${episodeNumber}`,
|
|
97
|
+
number: episodeNumber ? Number(episodeNumber) : 0,
|
|
98
|
+
title: epTitle,
|
|
99
|
+
image: $el.find('div.absolute img').attr('src') || '',
|
|
100
|
+
airDate: $el.find('span').filter((_, span) => /^\d{4}-\d{2}-\d{2}$/.test($(span).text().trim())).first().text().trim()
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
return { data: animeInfo };
|
|
104
|
+
}
|
|
105
|
+
export async function fetchSources(episodeId, malId, episodeNumber) {
|
|
106
|
+
const match = episodeId.match(/([a-z0-9]+)-episode-(\d+)/i);
|
|
107
|
+
if (!match)
|
|
108
|
+
throw new Error('Invalid episodeId format');
|
|
109
|
+
const id = `${match[1]}/${match[2]}`;
|
|
110
|
+
const html = await getText(`${BASE_URL}/anime/${id}`);
|
|
111
|
+
const $ = load(html);
|
|
112
|
+
const player = $('media-player');
|
|
113
|
+
const videoUrl = player.attr('src');
|
|
114
|
+
const poster = player.find('media-poster').attr('src');
|
|
115
|
+
const subtitles = [];
|
|
116
|
+
player.find('track[kind="subtitles"]').each((_, el) => {
|
|
117
|
+
const $el = $(el);
|
|
118
|
+
subtitles.push({
|
|
119
|
+
url: $el.attr('src') || '',
|
|
120
|
+
lang: $el.attr('label') || '',
|
|
121
|
+
isDefault: $el.is('[default]'),
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
const chapters = player.find('track[kind="chapters"]').attr('src');
|
|
125
|
+
const thumbnails = player.find('media-video-layout').attr('thumbnails');
|
|
126
|
+
const proxiedUrl = encodeAnikuro(videoUrl, `${BASE_URL}/`);
|
|
127
|
+
const stream = {
|
|
128
|
+
sources: [],
|
|
129
|
+
subtitles,
|
|
130
|
+
tracks: [],
|
|
131
|
+
headers: { Referer: `${BASE_URL}/` }
|
|
132
|
+
};
|
|
133
|
+
if (videoUrl) {
|
|
134
|
+
stream.sources.push({
|
|
135
|
+
url: videoUrl,
|
|
136
|
+
isM3U8: videoUrl.includes('m3u8'),
|
|
137
|
+
quality: 'auto',
|
|
138
|
+
proxiedUrl
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (chapters)
|
|
142
|
+
stream.tracks.push({ url: chapters, type: 'chapters' });
|
|
143
|
+
if (thumbnails)
|
|
144
|
+
stream.tracks.push({ url: thumbnails, type: 'thumbnails' });
|
|
145
|
+
return applyAniSkip(stream, malId, episodeNumber);
|
|
146
|
+
}
|
|
@@ -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(animeId: string, episodeNumber: number, dub?: boolean, malId?: number): Promise<VideoStream>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { httpGet } from '../../utils/http.js';
|
|
2
|
+
import { applyAniSkip, getVideoType } from '../../utils/shared.js';
|
|
3
|
+
const API_URL = 'https://animetsu.live/v2/api/anime';
|
|
4
|
+
const PROXY_URL = 'https://swiftstream.top/proxy';
|
|
5
|
+
const HEADERS = {
|
|
6
|
+
'Origin': 'https://animetsu.live',
|
|
7
|
+
'Referer': 'https://animetsu.live/',
|
|
8
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
|
|
9
|
+
};
|
|
10
|
+
export async function search(query) {
|
|
11
|
+
const res = await httpGet(`${API_URL}/search/?query=${encodeURIComponent(query)}`, HEADERS);
|
|
12
|
+
const json = await res.json();
|
|
13
|
+
const results = json.results.map(item => ({
|
|
14
|
+
id: item.id.toString(),
|
|
15
|
+
title: item.title?.english || item.title?.romaji || '',
|
|
16
|
+
image: item.cover_image?.medium || '',
|
|
17
|
+
}));
|
|
18
|
+
return { results };
|
|
19
|
+
}
|
|
20
|
+
export async function fetchAnimeInfo(aliasId) {
|
|
21
|
+
const res = await httpGet(`${API_URL}/eps/${aliasId}`, HEADERS);
|
|
22
|
+
const json = await res.json();
|
|
23
|
+
const info = {
|
|
24
|
+
id: aliasId,
|
|
25
|
+
title: '', // The API doesn't return the anime title here, just episodes
|
|
26
|
+
episodes: [],
|
|
27
|
+
};
|
|
28
|
+
info.episodes = json.map(item => {
|
|
29
|
+
let img = item.img;
|
|
30
|
+
if (img?.startsWith('/')) {
|
|
31
|
+
img = `${PROXY_URL}${img}`;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
id: aliasId, // the animeId is used as the episode identifier along with the number
|
|
35
|
+
number: item.ep_num,
|
|
36
|
+
title: item.name || '',
|
|
37
|
+
image: img || '',
|
|
38
|
+
hasDub: true,
|
|
39
|
+
filler: item.is_filler,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
info.totalEpisodes = info.episodes.length;
|
|
43
|
+
return info;
|
|
44
|
+
}
|
|
45
|
+
export async function fetchEpisodeSources(animeId, episodeNumber, dub = false, malId) {
|
|
46
|
+
const serverListRes = await httpGet(`${API_URL}/servers/${animeId}/${episodeNumber}`, HEADERS);
|
|
47
|
+
const serversJson = await serverListRes.json();
|
|
48
|
+
const stream = { sources: [], subtitles: [] };
|
|
49
|
+
const sourceType = dub ? 'dub' : 'sub';
|
|
50
|
+
const promises = serversJson.map(async (server) => {
|
|
51
|
+
try {
|
|
52
|
+
const url = `${API_URL}/oppai/${animeId}/${episodeNumber}?server=${server.id}&source_type=${sourceType}`;
|
|
53
|
+
const res = await httpGet(url, HEADERS);
|
|
54
|
+
const json = await res.json();
|
|
55
|
+
if (!json.sources || json.sources.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
const englishSub = json.subs?.find(it => it.lang === 'English')?.url || json.subs?.[0]?.url;
|
|
58
|
+
if (englishSub && stream.subtitles.length === 0) {
|
|
59
|
+
stream.subtitles.push({
|
|
60
|
+
url: englishSub.startsWith('/') ? `${PROXY_URL}${englishSub}` : englishSub,
|
|
61
|
+
lang: 'English',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
for (const src of json.sources) {
|
|
65
|
+
const srcUrl = src.url.startsWith('/') ? `${PROXY_URL}${src.url}` : src.url;
|
|
66
|
+
stream.sources.push({
|
|
67
|
+
url: srcUrl,
|
|
68
|
+
quality: src.quality?.trim() === 'master' ? 'multi-quality' : src.quality,
|
|
69
|
+
isM3U8: srcUrl.includes('.m3u8'),
|
|
70
|
+
type: getVideoType(srcUrl)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
// ignore server failures
|
|
76
|
+
console.log("error fetching animetsu sources", e);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
await Promise.all(promises);
|
|
80
|
+
if (stream.sources.length === 0)
|
|
81
|
+
throw new Error('No video sources found');
|
|
82
|
+
return applyAniSkip(stream, malId, episodeNumber);
|
|
83
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Episode, ProviderName } from '../../../types/types.js';
|
|
2
|
+
import { PagedMedia, MediaBase, SearchFilters, MediaDetail } from './types.js';
|
|
3
|
+
export declare const getTrending: (page: number, perPage: number) => Promise<PagedMedia>;
|
|
4
|
+
export declare const getPopularThisSeason: (page: number, perPage: number) => Promise<PagedMedia>;
|
|
5
|
+
export declare const getUpcoming: (page: number, perPage: number) => Promise<PagedMedia>;
|
|
6
|
+
export declare const getAllTimePopular: (page: number, perPage: number) => Promise<PagedMedia>;
|
|
7
|
+
export declare const getMostFavorite: (page: number, perPage: number) => Promise<PagedMedia>;
|
|
8
|
+
export declare const getTopRated: (page: number, perPage: number) => Promise<PagedMedia>;
|
|
9
|
+
export declare const getCurrentlyAiring: (page: number, perPage: number) => Promise<PagedMedia>;
|
|
10
|
+
export declare const getHeroAnime: (count?: number) => Promise<MediaBase[]>;
|
|
11
|
+
export declare const searchAnime: (filters?: SearchFilters) => Promise<PagedMedia>;
|
|
12
|
+
export declare const getAnimeDetail: (id: number) => Promise<MediaDetail>;
|
|
13
|
+
export declare const getGenres: () => Promise<string[]>;
|
|
14
|
+
export declare const getTags: () => Promise<string[]>;
|
|
15
|
+
export declare const getSettingsAssets: () => Promise<{
|
|
16
|
+
banners: unknown[];
|
|
17
|
+
avatars: any;
|
|
18
|
+
}>;
|
|
19
|
+
interface AniZipEpisode {
|
|
20
|
+
title?: Record<string, string>;
|
|
21
|
+
image?: string;
|
|
22
|
+
airdate?: string;
|
|
23
|
+
length?: number;
|
|
24
|
+
overview?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function fetchAniZipData(anilistId: string): Promise<Record<string, AniZipEpisode> | null>;
|
|
27
|
+
export declare function fetchEpisodesByProvider(anilistId: string, providerName: ProviderName): Promise<Episode[]>;
|
|
28
|
+
export {};
|