@genga-movie/aniwatch 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.
- package/LICENSE +21 -0
- package/README.md +428 -0
- package/dist/cjs/base/BaseProvider.js +23 -0
- package/dist/cjs/base/BaseProvider.js.map +1 -0
- package/dist/cjs/base/ExtensionsError.js +40 -0
- package/dist/cjs/base/ExtensionsError.js.map +1 -0
- package/dist/cjs/base/HttpClient.js +52 -0
- package/dist/cjs/base/HttpClient.js.map +1 -0
- package/dist/cjs/base/index.js +20 -0
- package/dist/cjs/base/index.js.map +1 -0
- package/dist/cjs/index.js +44 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/providers/hianime/HiAnime.js +947 -0
- package/dist/cjs/providers/hianime/HiAnime.js.map +1 -0
- package/dist/cjs/providers/hianime/index.js +18 -0
- package/dist/cjs/providers/hianime/index.js.map +1 -0
- package/dist/cjs/providers/index.js +18 -0
- package/dist/cjs/providers/index.js.map +1 -0
- package/dist/cjs/types/base.js +6 -0
- package/dist/cjs/types/base.js.map +1 -0
- package/dist/cjs/types/hianime.js +6 -0
- package/dist/cjs/types/hianime.js.map +1 -0
- package/dist/cjs/types/index.js +19 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/esm/base/BaseProvider.js +19 -0
- package/dist/esm/base/BaseProvider.js.map +1 -0
- package/dist/esm/base/ExtensionsError.js +35 -0
- package/dist/esm/base/ExtensionsError.js.map +1 -0
- package/dist/esm/base/HttpClient.js +45 -0
- package/dist/esm/base/HttpClient.js.map +1 -0
- package/dist/esm/base/index.js +4 -0
- package/dist/esm/base/index.js.map +1 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/providers/hianime/HiAnime.js +943 -0
- package/dist/esm/providers/hianime/HiAnime.js.map +1 -0
- package/dist/esm/providers/hianime/index.js +2 -0
- package/dist/esm/providers/hianime/index.js.map +1 -0
- package/dist/esm/providers/index.js +2 -0
- package/dist/esm/providers/index.js.map +1 -0
- package/dist/esm/types/base.js +5 -0
- package/dist/esm/types/base.js.map +1 -0
- package/dist/esm/types/hianime.js +5 -0
- package/dist/esm/types/hianime.js.map +1 -0
- package/dist/esm/types/index.js +3 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/types/base/BaseProvider.d.ts +32 -0
- package/dist/types/base/BaseProvider.d.ts.map +1 -0
- package/dist/types/base/ExtensionsError.d.ts +25 -0
- package/dist/types/base/ExtensionsError.d.ts.map +1 -0
- package/dist/types/base/HttpClient.d.ts +19 -0
- package/dist/types/base/HttpClient.d.ts.map +1 -0
- package/dist/types/base/index.d.ts +4 -0
- package/dist/types/base/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/providers/hianime/HiAnime.d.ts +220 -0
- package/dist/types/providers/hianime/HiAnime.d.ts.map +1 -0
- package/dist/types/providers/hianime/index.d.ts +2 -0
- package/dist/types/providers/hianime/index.d.ts.map +1 -0
- package/dist/types/providers/index.d.ts +2 -0
- package/dist/types/providers/index.d.ts.map +1 -0
- package/dist/types/types/base.d.ts +47 -0
- package/dist/types/types/base.d.ts.map +1 -0
- package/dist/types/types/hianime.d.ts +268 -0
- package/dist/types/types/hianime.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
import { load } from "cheerio";
|
|
2
|
+
import { BaseProvider } from "../../base/BaseProvider.js";
|
|
3
|
+
import { HttpClient } from "../../base/HttpClient.js";
|
|
4
|
+
import { ExtensionsError, throwExtensionsError } from "../../base/ExtensionsError.js";
|
|
5
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
6
|
+
const BASE_URL = "https://hianimez.to";
|
|
7
|
+
const AJAX_URL = `${BASE_URL}/ajax`;
|
|
8
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
9
|
+
function extractId(href) {
|
|
10
|
+
if (!href)
|
|
11
|
+
return "";
|
|
12
|
+
return href.split("/").pop()?.split("?")[0] ?? "";
|
|
13
|
+
}
|
|
14
|
+
function parseEpisodeCounts($el) {
|
|
15
|
+
const sub = parseInt($el.find(".sub").text().trim()) || null;
|
|
16
|
+
const dub = parseInt($el.find(".dub").text().trim()) || null;
|
|
17
|
+
return { sub, dub };
|
|
18
|
+
}
|
|
19
|
+
function parseAnimeCard($, el) {
|
|
20
|
+
const $el = $(el);
|
|
21
|
+
const $a = $el.find("a.film-poster-ahref, a[href*='/']").first();
|
|
22
|
+
const href = $a.attr("href") ?? "";
|
|
23
|
+
return {
|
|
24
|
+
id: extractId(href) || ($el.attr("data-id") ?? ""),
|
|
25
|
+
name: $el.find(".film-name a, .film-detail .film-name").first().text().trim() ||
|
|
26
|
+
$el.find(".film-name").first().text().trim(),
|
|
27
|
+
jname: $el
|
|
28
|
+
.find(".film-name a[data-jname], [data-jname]")
|
|
29
|
+
.first()
|
|
30
|
+
.attr("data-jname") ?? undefined,
|
|
31
|
+
poster: $el.find("img.film-poster-img, img.lazyload").first().attr("data-src") ??
|
|
32
|
+
$el.find("img").first().attr("src") ??
|
|
33
|
+
"",
|
|
34
|
+
duration: $el.find(".fdi-duration").text().trim() || undefined,
|
|
35
|
+
type: $el.find(".fdi-type, .tick-quality").text().trim() ||
|
|
36
|
+
undefined,
|
|
37
|
+
rating: $el.find(".tick-rate").text().trim() || undefined,
|
|
38
|
+
episodes: {
|
|
39
|
+
sub: parseInt($el.find(".tick-sub").text().trim()) || null,
|
|
40
|
+
dub: parseInt($el.find(".tick-dub").text().trim()) || null,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// ── Provider ─────────────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* HiAnime (hianimez.to) scraper.
|
|
47
|
+
*
|
|
48
|
+
* Scrapes anime data directly from the website HTML and JSON endpoints.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { ANIME } from "@genga-movie/aniwatch";
|
|
53
|
+
*
|
|
54
|
+
* const hianime = new ANIME.HiAnime();
|
|
55
|
+
*
|
|
56
|
+
* // Get home page data
|
|
57
|
+
* const home = await hianime.getHomePage();
|
|
58
|
+
*
|
|
59
|
+
* // Search for anime
|
|
60
|
+
* const results = await hianime.search("attack on titan");
|
|
61
|
+
*
|
|
62
|
+
* // Get streaming links
|
|
63
|
+
* const sources = await hianime.getEpisodeSources("steinsgate-3?ep=230", "hd-1", "sub");
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export class HiAnime extends BaseProvider {
|
|
67
|
+
id = "hianime";
|
|
68
|
+
name = "HiAnime (hianimez.to)";
|
|
69
|
+
baseUrl = BASE_URL;
|
|
70
|
+
version = "1.0.0";
|
|
71
|
+
isWorking = true;
|
|
72
|
+
http;
|
|
73
|
+
constructor() {
|
|
74
|
+
super();
|
|
75
|
+
this.http = new HttpClient(this.id, {
|
|
76
|
+
baseURL: BASE_URL,
|
|
77
|
+
timeout: 30_000,
|
|
78
|
+
headers: {
|
|
79
|
+
Referer: BASE_URL,
|
|
80
|
+
Origin: BASE_URL,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// ── Home Page ──────────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Get the home page with spotlight, trending, top 10 animes, and more.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const data = await hianime.getHomePage();
|
|
91
|
+
* console.log(data.spotlightAnimes);
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
async getHomePage() {
|
|
95
|
+
try {
|
|
96
|
+
const { data: html } = await this.http.get("/home");
|
|
97
|
+
const $ = load(html);
|
|
98
|
+
// Spotlight
|
|
99
|
+
const spotlightAnimes = [];
|
|
100
|
+
$("#slider .swiper-slide").each((_, el) => {
|
|
101
|
+
const $el = $(el);
|
|
102
|
+
const href = $el.find("a.btn-play, a.slideshow-buttons-link").first().attr("href") ?? "";
|
|
103
|
+
spotlightAnimes.push({
|
|
104
|
+
id: extractId(href),
|
|
105
|
+
name: $el.find(".desi-head-title").text().trim(),
|
|
106
|
+
jname: $el.find(".desi-head-title").attr("data-jname") ?? "",
|
|
107
|
+
poster: $el.find(".film-poster img").attr("data-src") ??
|
|
108
|
+
$el.find(".film-poster img").attr("src") ??
|
|
109
|
+
"",
|
|
110
|
+
description: $el.find(".desi-description").text().trim(),
|
|
111
|
+
rank: parseInt($el.find(".desi-sub-text").text().replace(/\D/g, "")) || 0,
|
|
112
|
+
otherInfo: $el
|
|
113
|
+
.find(".sc-detail .scd-item")
|
|
114
|
+
.map((_, i) => $(i).text().trim())
|
|
115
|
+
.get()
|
|
116
|
+
.filter(Boolean),
|
|
117
|
+
episodes: {
|
|
118
|
+
sub: parseInt($el.find(".tick-sub").text()) || null,
|
|
119
|
+
dub: parseInt($el.find(".tick-dub").text()) || null,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
// Trending
|
|
124
|
+
const trendingAnimes = [];
|
|
125
|
+
$("#trending-home .swiper-slide").each((_, el) => {
|
|
126
|
+
const $el = $(el);
|
|
127
|
+
const href = $el.find("a").attr("href") ?? "";
|
|
128
|
+
trendingAnimes.push({
|
|
129
|
+
id: extractId(href),
|
|
130
|
+
name: $el.find(".film-title, .number").text().trim(),
|
|
131
|
+
poster: $el.find("img").attr("data-src") ??
|
|
132
|
+
$el.find("img").attr("src") ??
|
|
133
|
+
"",
|
|
134
|
+
rank: parseInt($el.find(".number span").text()) || 0,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
// Anime sections
|
|
138
|
+
const sections = {};
|
|
139
|
+
const sectionMap = {
|
|
140
|
+
"#latest-ep-anime .film_list-wrap .flw-item": "latestEpisodeAnimes",
|
|
141
|
+
"#top-upcoming .film_list-wrap .flw-item": "topUpcomingAnimes",
|
|
142
|
+
"#top-airing .film_list-wrap .flw-item": "topAiringAnimes",
|
|
143
|
+
"#most-popular .film_list-wrap .flw-item": "mostPopularAnimes",
|
|
144
|
+
"#most-favourite .film_list-wrap .flw-item": "mostFavoriteAnimes",
|
|
145
|
+
"#latest-completed .film_list-wrap .flw-item": "latestCompletedAnimes",
|
|
146
|
+
};
|
|
147
|
+
for (const [selector, key] of Object.entries(sectionMap)) {
|
|
148
|
+
sections[key] = [];
|
|
149
|
+
$(selector).each((_, el) => {
|
|
150
|
+
sections[key].push(parseAnimeCard($, el));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Top 10
|
|
154
|
+
const top10Animes = {
|
|
155
|
+
today: [],
|
|
156
|
+
week: [],
|
|
157
|
+
month: [],
|
|
158
|
+
};
|
|
159
|
+
const periods = [
|
|
160
|
+
["today", "#top-viewed-day"],
|
|
161
|
+
["week", "#top-viewed-week"],
|
|
162
|
+
["month", "#top-viewed-month"],
|
|
163
|
+
];
|
|
164
|
+
for (const [period, selector] of periods) {
|
|
165
|
+
$(`${selector} .top-av-list .top-av-item`).each((_, el) => {
|
|
166
|
+
const $el = $(el);
|
|
167
|
+
const href = $el.find("a").attr("href") ?? "";
|
|
168
|
+
top10Animes[period].push({
|
|
169
|
+
id: extractId(href),
|
|
170
|
+
name: $el.find(".film-name a").text().trim(),
|
|
171
|
+
poster: $el.find("img").attr("data-src") ??
|
|
172
|
+
$el.find("img").attr("src") ??
|
|
173
|
+
"",
|
|
174
|
+
rank: parseInt($el.find(".rank").text()) || 0,
|
|
175
|
+
episodes: {
|
|
176
|
+
sub: parseInt($el.find(".tick-sub").text()) || null,
|
|
177
|
+
dub: parseInt($el.find(".tick-dub").text()) || null,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// Genres
|
|
183
|
+
const genres = [];
|
|
184
|
+
$(".block_area.block_area_home .cbox-list a").each((_, el) => {
|
|
185
|
+
genres.push($(el).text().trim());
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
genres,
|
|
189
|
+
spotlightAnimes,
|
|
190
|
+
trendingAnimes,
|
|
191
|
+
top10Animes,
|
|
192
|
+
latestEpisodeAnimes: sections["latestEpisodeAnimes"] ?? [],
|
|
193
|
+
topUpcomingAnimes: sections["topUpcomingAnimes"] ?? [],
|
|
194
|
+
topAiringAnimes: sections["topAiringAnimes"] ?? [],
|
|
195
|
+
mostPopularAnimes: sections["mostPopularAnimes"] ?? [],
|
|
196
|
+
mostFavoriteAnimes: sections["mostFavoriteAnimes"] ?? [],
|
|
197
|
+
latestCompletedAnimes: sections["latestCompletedAnimes"] ?? [],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (err instanceof ExtensionsError)
|
|
202
|
+
throw err;
|
|
203
|
+
throwExtensionsError(`Failed to scrape home page: ${err.message}`, 500, this.id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ── A-Z List ───────────────────────────────────────────────────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Get anime sorted alphabetically.
|
|
209
|
+
*
|
|
210
|
+
* @param sortOption - Letter, "0-9", "all", or "other"
|
|
211
|
+
* @param page - Page number (default: 1)
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* const data = await hianime.getAZList("a", 1);
|
|
216
|
+
* console.log(data.animes);
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
async getAZList(sortOption = "all", page = 1) {
|
|
220
|
+
try {
|
|
221
|
+
const { data: html } = await this.http.get(`/az-list/${sortOption}?page=${page}`);
|
|
222
|
+
const $ = load(html);
|
|
223
|
+
const animes = [];
|
|
224
|
+
$(".film_list-wrap .flw-item").each((_, el) => {
|
|
225
|
+
animes.push(parseAnimeCard($, el));
|
|
226
|
+
});
|
|
227
|
+
const lastPage = parseInt($(".pagination .page-item:last-child a").attr("href")?.split("page=")[1] ?? "1");
|
|
228
|
+
const hasNextPage = $(".pagination .page-item.active").next(".page-item").length > 0;
|
|
229
|
+
return {
|
|
230
|
+
sortOption,
|
|
231
|
+
animes,
|
|
232
|
+
totalPages: lastPage,
|
|
233
|
+
currentPage: page,
|
|
234
|
+
hasNextPage,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
if (err instanceof ExtensionsError)
|
|
239
|
+
throw err;
|
|
240
|
+
throwExtensionsError(`Failed to scrape A-Z list: ${err.message}`, 500, this.id);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// ── Qtip Info ──────────────────────────────────────────────────────────────
|
|
244
|
+
/**
|
|
245
|
+
* Get quick tooltip info for an anime.
|
|
246
|
+
*
|
|
247
|
+
* @param animeId - Anime ID in kebab case (e.g. "one-piece-100")
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* const data = await hianime.getQtipInfo("one-piece-100");
|
|
252
|
+
* console.log(data.name);
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
async getQtipInfo(animeId) {
|
|
256
|
+
try {
|
|
257
|
+
const numericId = animeId.split("-").pop() ?? animeId;
|
|
258
|
+
const { data } = await this.http.get(`${AJAX_URL}/anime/qtip?id=${numericId}`);
|
|
259
|
+
const $ = load(data.html ?? "");
|
|
260
|
+
const genres = [];
|
|
261
|
+
$(".info-item.genres a, .genre a").each((_, el) => {
|
|
262
|
+
genres.push($(el).text().trim());
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
id: animeId,
|
|
266
|
+
name: $(".film-name, .qtip-title, h2").first().text().trim(),
|
|
267
|
+
malscore: $(".info-item .mal-score, .score").text().trim(),
|
|
268
|
+
quality: $(".quality").text().trim(),
|
|
269
|
+
episodes: {
|
|
270
|
+
sub: parseInt($(".tick-sub").text()) || null,
|
|
271
|
+
dub: parseInt($(".tick-dub").text()) || null,
|
|
272
|
+
},
|
|
273
|
+
type: $(".info-item.type, .fdi-type").first().text().trim(),
|
|
274
|
+
description: $(".film-description, .description").text().trim(),
|
|
275
|
+
jname: $(".jname, [data-jname]").first().attr("data-jname") ?? "",
|
|
276
|
+
synonyms: $(".synonyms").text().trim(),
|
|
277
|
+
aired: $(".aired").text().trim(),
|
|
278
|
+
status: $(".status").text().trim(),
|
|
279
|
+
genres,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
if (err instanceof ExtensionsError)
|
|
284
|
+
throw err;
|
|
285
|
+
throwExtensionsError(`Failed to scrape qtip info for ${animeId}`, 500, this.id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// ── About Info ─────────────────────────────────────────────────────────────
|
|
289
|
+
/**
|
|
290
|
+
* Get detailed information about an anime.
|
|
291
|
+
*
|
|
292
|
+
* @param animeId - Anime ID in kebab case (e.g. "attack-on-titan-112")
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```ts
|
|
296
|
+
* const data = await hianime.getInfo("steinsgate-3");
|
|
297
|
+
* console.log(data.anime.info.name);
|
|
298
|
+
* console.log(data.seasons);
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
async getInfo(animeId) {
|
|
302
|
+
try {
|
|
303
|
+
const { data: html } = await this.http.get(`/${animeId}`);
|
|
304
|
+
const $ = load(html);
|
|
305
|
+
// Main info
|
|
306
|
+
const name = $(".anisc-detail .film-name").text().trim();
|
|
307
|
+
const poster = $(".film-poster img").attr("data-src") ??
|
|
308
|
+
$(".film-poster img").attr("src") ??
|
|
309
|
+
"";
|
|
310
|
+
const description = $(".film-description .text").text().trim();
|
|
311
|
+
const stats = {
|
|
312
|
+
rating: $(".tick-rate").text().trim(),
|
|
313
|
+
quality: $(".tick-quality").text().trim(),
|
|
314
|
+
episodes: {
|
|
315
|
+
sub: parseInt($(".tick-sub").text()) || null,
|
|
316
|
+
dub: parseInt($(".tick-dub").text()) || null,
|
|
317
|
+
},
|
|
318
|
+
type: $(".item:contains('Type')").next().text().trim() || $(".tick-type").text().trim(),
|
|
319
|
+
duration: $(".item:contains('Duration')").next().text().trim(),
|
|
320
|
+
};
|
|
321
|
+
// More info (the key-value table)
|
|
322
|
+
const moreInfo = {};
|
|
323
|
+
$(".anisc-info .item").each((_, el) => {
|
|
324
|
+
const $el = $(el);
|
|
325
|
+
const key = $el
|
|
326
|
+
.find(".item-head")
|
|
327
|
+
.text()
|
|
328
|
+
.trim()
|
|
329
|
+
.toLowerCase()
|
|
330
|
+
.replace(/\s+/g, "_")
|
|
331
|
+
.replace(":", "");
|
|
332
|
+
const valueEl = $el.find(".name, a");
|
|
333
|
+
if (valueEl.length > 1) {
|
|
334
|
+
moreInfo[key] = valueEl
|
|
335
|
+
.map((_, a) => $(a).text().trim())
|
|
336
|
+
.get()
|
|
337
|
+
.filter(Boolean);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
moreInfo[key] = $el.find(".name").text().trim();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
// Seasons
|
|
344
|
+
const seasons = [];
|
|
345
|
+
$(".seasons-list .ssl-item").each((_, el) => {
|
|
346
|
+
const $el = $(el);
|
|
347
|
+
const href = $el.find("a").attr("href") ?? "";
|
|
348
|
+
seasons.push({
|
|
349
|
+
id: extractId(href),
|
|
350
|
+
name: $el.find("a").text().trim(),
|
|
351
|
+
title: $el.find(".ssl-title").text().trim(),
|
|
352
|
+
poster: $el.find("img").attr("data-src") ??
|
|
353
|
+
$el.find("img").attr("src") ??
|
|
354
|
+
"",
|
|
355
|
+
isCurrent: $el.hasClass("active") || $el.hasClass("ssl-item-a"),
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
// Promotional videos
|
|
359
|
+
const promotionalVideos = [];
|
|
360
|
+
$(".block_area-promotions .pv-list .pv-item").each((_, el) => {
|
|
361
|
+
const $el = $(el);
|
|
362
|
+
promotionalVideos.push({
|
|
363
|
+
title: $el.find(".pv-title").text().trim() || undefined,
|
|
364
|
+
source: $el.find("a").attr("href") || undefined,
|
|
365
|
+
thumbnail: $el.find("img").attr("data-src") ??
|
|
366
|
+
$el.find("img").attr("src") ??
|
|
367
|
+
undefined,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
// Character/VA section
|
|
371
|
+
const characterVoiceActor = [];
|
|
372
|
+
$(".cach-list .cach-item").each((_, el) => {
|
|
373
|
+
const $el = $(el);
|
|
374
|
+
const $char = $el.find(".per-info.ltr");
|
|
375
|
+
const $va = $el.find(".per-info.rtl");
|
|
376
|
+
characterVoiceActor.push({
|
|
377
|
+
character: {
|
|
378
|
+
id: extractId($char.find("a").attr("href")),
|
|
379
|
+
poster: $char.find("img").attr("data-src") ??
|
|
380
|
+
$char.find("img").attr("src") ??
|
|
381
|
+
"",
|
|
382
|
+
name: $char.find(".pi-name a").text().trim(),
|
|
383
|
+
cast: $char.find(".pi-cast").text().trim(),
|
|
384
|
+
},
|
|
385
|
+
voiceActor: {
|
|
386
|
+
id: extractId($va.find("a").attr("href")),
|
|
387
|
+
poster: $va.find("img").attr("data-src") ??
|
|
388
|
+
$va.find("img").attr("src") ??
|
|
389
|
+
"",
|
|
390
|
+
name: $va.find(".pi-name a").text().trim(),
|
|
391
|
+
cast: $va.find(".pi-cast").text().trim(),
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
// Recommended / Related / Most Popular
|
|
396
|
+
const recommendedAnimes = [];
|
|
397
|
+
$(".block_area-realtime .flw-item").each((_, el) => {
|
|
398
|
+
recommendedAnimes.push(parseAnimeCard($, el));
|
|
399
|
+
});
|
|
400
|
+
const relatedAnimes = [];
|
|
401
|
+
$(".block_area-related .flw-item").each((_, el) => {
|
|
402
|
+
relatedAnimes.push(parseAnimeCard($, el));
|
|
403
|
+
});
|
|
404
|
+
const mostPopularAnimes = [];
|
|
405
|
+
$(".anif-block-ul .anif-item").each((_, el) => {
|
|
406
|
+
mostPopularAnimes.push(parseAnimeCard($, el));
|
|
407
|
+
});
|
|
408
|
+
return {
|
|
409
|
+
anime: {
|
|
410
|
+
info: {
|
|
411
|
+
id: animeId,
|
|
412
|
+
name,
|
|
413
|
+
poster,
|
|
414
|
+
description,
|
|
415
|
+
stats,
|
|
416
|
+
promotionalVideos,
|
|
417
|
+
characterVoiceActor,
|
|
418
|
+
},
|
|
419
|
+
moreInfo,
|
|
420
|
+
},
|
|
421
|
+
mostPopularAnimes,
|
|
422
|
+
recommendedAnimes,
|
|
423
|
+
relatedAnimes,
|
|
424
|
+
seasons,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
if (err instanceof ExtensionsError)
|
|
429
|
+
throw err;
|
|
430
|
+
throwExtensionsError(`Failed to scrape anime info for ${animeId}: ${err.message}`, 500, this.id);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// ── Search ─────────────────────────────────────────────────────────────────
|
|
434
|
+
/**
|
|
435
|
+
* Search for anime.
|
|
436
|
+
*
|
|
437
|
+
* @param query - Search query string
|
|
438
|
+
* @param page - Page number (default: 1)
|
|
439
|
+
* @param filters - Optional search filters
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```ts
|
|
443
|
+
* // Simple search
|
|
444
|
+
* const data = await hianime.search("one piece");
|
|
445
|
+
*
|
|
446
|
+
* // Advanced search
|
|
447
|
+
* const data = await hianime.search("girls", 1, {
|
|
448
|
+
* genres: "action,adventure",
|
|
449
|
+
* type: "movie",
|
|
450
|
+
* sort: "score",
|
|
451
|
+
* });
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
async search(query, page = 1, filters = {}) {
|
|
455
|
+
try {
|
|
456
|
+
const params = new URLSearchParams({ q: query, page: String(page) });
|
|
457
|
+
for (const [k, v] of Object.entries(filters)) {
|
|
458
|
+
if (v !== undefined && v !== "")
|
|
459
|
+
params.set(k, v);
|
|
460
|
+
}
|
|
461
|
+
const { data: html } = await this.http.get(`/search?${params.toString()}`);
|
|
462
|
+
const $ = load(html);
|
|
463
|
+
const animes = [];
|
|
464
|
+
$(".film_list-wrap .flw-item").each((_, el) => {
|
|
465
|
+
animes.push(parseAnimeCard($, el));
|
|
466
|
+
});
|
|
467
|
+
const mostPopularAnimes = [];
|
|
468
|
+
$(".anif-block-ul .anif-item").each((_, el) => {
|
|
469
|
+
mostPopularAnimes.push(parseAnimeCard($, el));
|
|
470
|
+
});
|
|
471
|
+
const hasNextPage = $(".pagination .page-item.active").next(".page-item").length > 0;
|
|
472
|
+
const totalPages = parseInt($(".pagination .page-item:last-child a").attr("href")?.split("page=")[1] ?? "1") || 1;
|
|
473
|
+
return {
|
|
474
|
+
animes,
|
|
475
|
+
mostPopularAnimes,
|
|
476
|
+
currentPage: page,
|
|
477
|
+
totalPages,
|
|
478
|
+
hasNextPage,
|
|
479
|
+
searchQuery: query,
|
|
480
|
+
searchFilters: filters,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
if (err instanceof ExtensionsError)
|
|
485
|
+
throw err;
|
|
486
|
+
throwExtensionsError(`Failed to search for "${query}": ${err.message}`, 500, this.id);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// ── Search Suggestions ─────────────────────────────────────────────────────
|
|
490
|
+
/**
|
|
491
|
+
* Get search autocomplete suggestions.
|
|
492
|
+
*
|
|
493
|
+
* @param query - Partial search query
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```ts
|
|
497
|
+
* const data = await hianime.searchSuggestions("one pie");
|
|
498
|
+
* console.log(data.suggestions);
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
async searchSuggestions(query) {
|
|
502
|
+
try {
|
|
503
|
+
const { data } = await this.http.get(`${AJAX_URL}/search/suggest?keyword=${encodeURIComponent(query)}`);
|
|
504
|
+
const $ = load(data.html ?? "");
|
|
505
|
+
const suggestions = [];
|
|
506
|
+
$(".nav-item").each((_, el) => {
|
|
507
|
+
const $el = $(el);
|
|
508
|
+
const href = $el.find("a").attr("href") ?? "";
|
|
509
|
+
const moreInfo = [];
|
|
510
|
+
$el.find(".film-infor span, .film-infor a").each((_, s) => {
|
|
511
|
+
const text = $(s).text().trim();
|
|
512
|
+
if (text)
|
|
513
|
+
moreInfo.push(text);
|
|
514
|
+
});
|
|
515
|
+
suggestions.push({
|
|
516
|
+
id: extractId(href),
|
|
517
|
+
name: $el.find(".film-name").text().trim(),
|
|
518
|
+
poster: $el.find("img").attr("data-src") ??
|
|
519
|
+
$el.find("img").attr("src") ??
|
|
520
|
+
"",
|
|
521
|
+
jname: $el.find("[data-jname]").attr("data-jname") ?? "",
|
|
522
|
+
moreInfo,
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
return { suggestions };
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
if (err instanceof ExtensionsError)
|
|
529
|
+
throw err;
|
|
530
|
+
throwExtensionsError("Failed to get search suggestions", 500, this.id);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// ── Category ───────────────────────────────────────────────────────────────
|
|
534
|
+
/**
|
|
535
|
+
* Get anime by category.
|
|
536
|
+
*
|
|
537
|
+
* @param category - One of: "most-favorite", "most-popular", "subbed-anime", "dubbed-anime",
|
|
538
|
+
* "recently-updated", "recently-added", "top-upcoming", "top-airing",
|
|
539
|
+
* "movie", "special", "ova", "ona", "tv", "completed"
|
|
540
|
+
* @param page - Page number (default: 1)
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```ts
|
|
544
|
+
* const data = await hianime.getCategoryAnime("tv", 2);
|
|
545
|
+
* ```
|
|
546
|
+
*/
|
|
547
|
+
async getCategoryAnime(category, page = 1) {
|
|
548
|
+
try {
|
|
549
|
+
const { data: html } = await this.http.get(`/${category}?page=${page}`);
|
|
550
|
+
const $ = load(html);
|
|
551
|
+
const animes = [];
|
|
552
|
+
$(".film_list-wrap .flw-item").each((_, el) => {
|
|
553
|
+
animes.push(parseAnimeCard($, el));
|
|
554
|
+
});
|
|
555
|
+
const genres = [];
|
|
556
|
+
$(".block_area.block_area_sidebar .cbox-list a").each((_, el) => {
|
|
557
|
+
genres.push($(el).text().trim());
|
|
558
|
+
});
|
|
559
|
+
const top10 = {
|
|
560
|
+
today: [],
|
|
561
|
+
week: [],
|
|
562
|
+
month: [],
|
|
563
|
+
};
|
|
564
|
+
const periods = [
|
|
565
|
+
["today", "#top-viewed-day"],
|
|
566
|
+
["week", "#top-viewed-week"],
|
|
567
|
+
["month", "#top-viewed-month"],
|
|
568
|
+
];
|
|
569
|
+
for (const [period, sel] of periods) {
|
|
570
|
+
$(`${sel} .top-av-list .top-av-item`).each((_, el) => {
|
|
571
|
+
const $el = $(el);
|
|
572
|
+
top10[period].push({
|
|
573
|
+
id: extractId($el.find("a").attr("href")),
|
|
574
|
+
name: $el.find(".film-name a").text().trim(),
|
|
575
|
+
poster: $el.find("img").attr("data-src") ??
|
|
576
|
+
$el.find("img").attr("src") ??
|
|
577
|
+
"",
|
|
578
|
+
rank: parseInt($el.find(".rank").text()) || 0,
|
|
579
|
+
episodes: {
|
|
580
|
+
sub: parseInt($el.find(".tick-sub").text()) || null,
|
|
581
|
+
dub: parseInt($el.find(".tick-dub").text()) || null,
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
const hasNextPage = $(".pagination .page-item.active").next(".page-item").length > 0;
|
|
587
|
+
const totalPages = parseInt($(".pagination .page-item:last-child a").attr("href")?.split("page=")[1] ?? "1") || 1;
|
|
588
|
+
return {
|
|
589
|
+
category: $(".block_area-header h2").text().trim() || category,
|
|
590
|
+
animes,
|
|
591
|
+
genres,
|
|
592
|
+
top10Animes: top10,
|
|
593
|
+
currentPage: page,
|
|
594
|
+
totalPages,
|
|
595
|
+
hasNextPage,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
if (err instanceof ExtensionsError)
|
|
600
|
+
throw err;
|
|
601
|
+
throwExtensionsError(`Failed to scrape category "${category}"`, 500, this.id);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// ── Genre ──────────────────────────────────────────────────────────────────
|
|
605
|
+
/**
|
|
606
|
+
* Get anime by genre.
|
|
607
|
+
*
|
|
608
|
+
* @param genreName - Genre name in kebab case (e.g. "shounen", "action")
|
|
609
|
+
* @param page - Page number (default: 1)
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```ts
|
|
613
|
+
* const data = await hianime.getGenreAnime("isekai", 1);
|
|
614
|
+
* ```
|
|
615
|
+
*/
|
|
616
|
+
async getGenreAnime(genreName, page = 1) {
|
|
617
|
+
try {
|
|
618
|
+
const { data: html } = await this.http.get(`/genre/${genreName}?page=${page}`);
|
|
619
|
+
const $ = load(html);
|
|
620
|
+
const animes = [];
|
|
621
|
+
$(".film_list-wrap .flw-item").each((_, el) => {
|
|
622
|
+
animes.push(parseAnimeCard($, el));
|
|
623
|
+
});
|
|
624
|
+
const genres = [];
|
|
625
|
+
$(".block_area.block_area_sidebar .cbox-list a").each((_, el) => {
|
|
626
|
+
genres.push($(el).text().trim());
|
|
627
|
+
});
|
|
628
|
+
const topAiringAnimes = [];
|
|
629
|
+
$(".anif-block-ul .anif-item").each((_, el) => {
|
|
630
|
+
topAiringAnimes.push(parseAnimeCard($, el));
|
|
631
|
+
});
|
|
632
|
+
const hasNextPage = $(".pagination .page-item.active").next(".page-item").length > 0;
|
|
633
|
+
const totalPages = parseInt($(".pagination .page-item:last-child a").attr("href")?.split("page=")[1] ?? "1") || 1;
|
|
634
|
+
return {
|
|
635
|
+
genreName: $(".block_area-header h2").text().trim() || genreName,
|
|
636
|
+
animes,
|
|
637
|
+
genres,
|
|
638
|
+
topAiringAnimes,
|
|
639
|
+
currentPage: page,
|
|
640
|
+
totalPages,
|
|
641
|
+
hasNextPage,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
if (err instanceof ExtensionsError)
|
|
646
|
+
throw err;
|
|
647
|
+
throwExtensionsError(`Failed to scrape genre "${genreName}"`, 500, this.id);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// ── Producer ───────────────────────────────────────────────────────────────
|
|
651
|
+
/**
|
|
652
|
+
* Get anime by producer/studio.
|
|
653
|
+
*
|
|
654
|
+
* @param producerName - Producer name in kebab case (e.g. "toei-animation")
|
|
655
|
+
* @param page - Page number (default: 1)
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* ```ts
|
|
659
|
+
* const data = await hianime.getProducerAnimes("mappa", 1);
|
|
660
|
+
* ```
|
|
661
|
+
*/
|
|
662
|
+
async getProducerAnimes(producerName, page = 1) {
|
|
663
|
+
try {
|
|
664
|
+
const { data: html } = await this.http.get(`/producer/${producerName}?page=${page}`);
|
|
665
|
+
const $ = load(html);
|
|
666
|
+
const animes = [];
|
|
667
|
+
$(".film_list-wrap .flw-item").each((_, el) => {
|
|
668
|
+
animes.push(parseAnimeCard($, el));
|
|
669
|
+
});
|
|
670
|
+
const top10 = {
|
|
671
|
+
today: [],
|
|
672
|
+
week: [],
|
|
673
|
+
month: [],
|
|
674
|
+
};
|
|
675
|
+
const periods = [
|
|
676
|
+
["today", "#top-viewed-day"],
|
|
677
|
+
["week", "#top-viewed-week"],
|
|
678
|
+
["month", "#top-viewed-month"],
|
|
679
|
+
];
|
|
680
|
+
for (const [period, sel] of periods) {
|
|
681
|
+
$(`${sel} .top-av-list .top-av-item`).each((_, el) => {
|
|
682
|
+
const $el = $(el);
|
|
683
|
+
top10[period].push({
|
|
684
|
+
id: extractId($el.find("a").attr("href")),
|
|
685
|
+
name: $el.find(".film-name a").text().trim(),
|
|
686
|
+
poster: $el.find("img").attr("data-src") ??
|
|
687
|
+
$el.find("img").attr("src") ??
|
|
688
|
+
"",
|
|
689
|
+
rank: parseInt($el.find(".rank").text()) || 0,
|
|
690
|
+
episodes: {
|
|
691
|
+
sub: parseInt($el.find(".tick-sub").text()) || null,
|
|
692
|
+
dub: parseInt($el.find(".tick-dub").text()) || null,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const topAiringAnimes = [];
|
|
698
|
+
$(".anif-block-ul .anif-item").each((_, el) => {
|
|
699
|
+
topAiringAnimes.push(parseAnimeCard($, el));
|
|
700
|
+
});
|
|
701
|
+
const hasNextPage = $(".pagination .page-item.active").next(".page-item").length > 0;
|
|
702
|
+
const totalPages = parseInt($(".pagination .page-item:last-child a").attr("href")?.split("page=")[1] ?? "1") || 1;
|
|
703
|
+
return {
|
|
704
|
+
producerName: $(".block_area-header h2").text().trim() || producerName,
|
|
705
|
+
animes,
|
|
706
|
+
top10Animes: top10,
|
|
707
|
+
topAiringAnimes,
|
|
708
|
+
currentPage: page,
|
|
709
|
+
totalPages,
|
|
710
|
+
hasNextPage,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
if (err instanceof ExtensionsError)
|
|
715
|
+
throw err;
|
|
716
|
+
throwExtensionsError(`Failed to scrape producer "${producerName}"`, 500, this.id);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// ── Schedule ───────────────────────────────────────────────────────────────
|
|
720
|
+
/**
|
|
721
|
+
* Get estimated airing schedule for a specific date.
|
|
722
|
+
*
|
|
723
|
+
* @param date - Date string in format yyyy-mm-dd
|
|
724
|
+
* @param tzOffset - Timezone offset in minutes (default: -330 = IST)
|
|
725
|
+
*
|
|
726
|
+
* @example
|
|
727
|
+
* ```ts
|
|
728
|
+
* const today = new Date().toISOString().split("T")[0];
|
|
729
|
+
* const data = await hianime.getEstimatedSchedule(today);
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
async getEstimatedSchedule(date, tzOffset = -330) {
|
|
733
|
+
try {
|
|
734
|
+
const { data } = await this.http.get(`${AJAX_URL}/schedule/list?tzOffset=${tzOffset}&date=${date}`);
|
|
735
|
+
const $ = load(data.html ?? "");
|
|
736
|
+
const scheduledAnimes = [];
|
|
737
|
+
$(".schedule-list .sl-item").each((_, el) => {
|
|
738
|
+
const $el = $(el);
|
|
739
|
+
const href = $el.find("a").attr("href") ?? "";
|
|
740
|
+
const timestamp = parseInt($el.attr("data-timestamp") ?? "0") || 0;
|
|
741
|
+
const now = Math.floor(Date.now() / 1000);
|
|
742
|
+
scheduledAnimes.push({
|
|
743
|
+
id: extractId(href),
|
|
744
|
+
time: $el.find(".time, .sl-time").text().trim(),
|
|
745
|
+
name: $el.find(".film-name, .sl-title").text().trim(),
|
|
746
|
+
jname: $el.find("[data-jname]").attr("data-jname") ?? "",
|
|
747
|
+
airingTimestamp: timestamp,
|
|
748
|
+
secondsUntilAiring: Math.max(0, timestamp - now),
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
return { scheduledAnimes };
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
if (err instanceof ExtensionsError)
|
|
755
|
+
throw err;
|
|
756
|
+
throwExtensionsError(`Failed to scrape schedule for ${date}`, 500, this.id);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// ── Episodes ───────────────────────────────────────────────────────────────
|
|
760
|
+
/**
|
|
761
|
+
* Get all episodes for an anime.
|
|
762
|
+
*
|
|
763
|
+
* @param animeId - Anime ID in kebab case (e.g. "steinsgate-3")
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* ```ts
|
|
767
|
+
* const data = await hianime.getEpisodes("steinsgate-3");
|
|
768
|
+
* console.log(data.totalEpisodes);
|
|
769
|
+
* console.log(data.episodes[0]);
|
|
770
|
+
* ```
|
|
771
|
+
*/
|
|
772
|
+
async getEpisodes(animeId) {
|
|
773
|
+
try {
|
|
774
|
+
const numericId = animeId.split("-").pop() ?? animeId;
|
|
775
|
+
const { data } = await this.http.get(`${AJAX_URL}/v2/episode/list/${numericId}`);
|
|
776
|
+
const $ = load(data.html ?? "");
|
|
777
|
+
const episodes = [];
|
|
778
|
+
$(".detail-ep-list .ep-item, .ss-list .ep-item").each((_, el) => {
|
|
779
|
+
const $el = $(el);
|
|
780
|
+
episodes.push({
|
|
781
|
+
number: parseInt($el.attr("data-number") ?? "0"),
|
|
782
|
+
title: $el.attr("title") ?? $el.find(".ssli-detail .ep-name").text().trim(),
|
|
783
|
+
episodeId: $el.attr("data-id") ??
|
|
784
|
+
extractId($el.find("a").attr("href")),
|
|
785
|
+
isFiller: $el.hasClass("ssl-item-filler") || $el.attr("data-filler") === "1",
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
return {
|
|
789
|
+
totalEpisodes: data.totalItems || episodes.length,
|
|
790
|
+
episodes,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
if (err instanceof ExtensionsError)
|
|
795
|
+
throw err;
|
|
796
|
+
throwExtensionsError(`Failed to get episodes for ${animeId}`, 500, this.id);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// ── Next Episode Schedule ──────────────────────────────────────────────────
|
|
800
|
+
/**
|
|
801
|
+
* Get the next episode airing schedule.
|
|
802
|
+
*
|
|
803
|
+
* @param animeId - Anime ID in kebab case
|
|
804
|
+
*
|
|
805
|
+
* @example
|
|
806
|
+
* ```ts
|
|
807
|
+
* const data = await hianime.getNextEpisodeSchedule("one-piece-100");
|
|
808
|
+
* console.log(data.airingTimestamp);
|
|
809
|
+
* ```
|
|
810
|
+
*/
|
|
811
|
+
async getNextEpisodeSchedule(animeId) {
|
|
812
|
+
try {
|
|
813
|
+
const numericId = animeId.split("-").pop() ?? animeId;
|
|
814
|
+
const { data } = await this.http.get(`${AJAX_URL}/anime/next-episode-schedule?id=${numericId}`);
|
|
815
|
+
if (!data?.data) {
|
|
816
|
+
return {
|
|
817
|
+
airingISOTimestamp: null,
|
|
818
|
+
airingTimestamp: null,
|
|
819
|
+
secondsUntilAiring: null,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
const ts = data.data.episode_airing_timestamp ?? null;
|
|
823
|
+
const now = Math.floor(Date.now() / 1000);
|
|
824
|
+
return {
|
|
825
|
+
airingISOTimestamp: data.data.episode_airing_time ?? null,
|
|
826
|
+
airingTimestamp: ts,
|
|
827
|
+
secondsUntilAiring: ts ? Math.max(0, ts - now) : null,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
catch (err) {
|
|
831
|
+
if (err instanceof ExtensionsError)
|
|
832
|
+
throw err;
|
|
833
|
+
throwExtensionsError(`Failed to get next episode schedule for ${animeId}`, 500, this.id);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
// ── Episode Servers ────────────────────────────────────────────────────────
|
|
837
|
+
/**
|
|
838
|
+
* Get available streaming servers for an episode.
|
|
839
|
+
*
|
|
840
|
+
* @param episodeId - Episode ID string (e.g. "steinsgate-0-92?ep=2055")
|
|
841
|
+
*
|
|
842
|
+
* @example
|
|
843
|
+
* ```ts
|
|
844
|
+
* const data = await hianime.getEpisodeServers("steinsgate-0-92?ep=2055");
|
|
845
|
+
* console.log(data.sub, data.dub);
|
|
846
|
+
* ```
|
|
847
|
+
*/
|
|
848
|
+
async getEpisodeServers(episodeId) {
|
|
849
|
+
try {
|
|
850
|
+
const epId = episodeId.split("?ep=")[1] ?? episodeId;
|
|
851
|
+
const { data } = await this.http.get(`${AJAX_URL}/v2/episode/servers?episodeId=${epId}`);
|
|
852
|
+
const $ = load(data.html ?? "");
|
|
853
|
+
const sub = [];
|
|
854
|
+
const dub = [];
|
|
855
|
+
const raw = [];
|
|
856
|
+
$(".server-item[data-type='sub'] .server-name, .ps-server-list[data-type='sub'] li").each((_, el) => {
|
|
857
|
+
const $el = $(el);
|
|
858
|
+
sub.push({
|
|
859
|
+
serverId: parseInt($el.attr("data-server-id") || $el.attr("data-id") || "0"),
|
|
860
|
+
serverName: ($el.text().trim() || $el.attr("data-server-name")) ?? "",
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
$(".server-item[data-type='dub'] .server-name, .ps-server-list[data-type='dub'] li").each((_, el) => {
|
|
864
|
+
const $el = $(el);
|
|
865
|
+
dub.push({
|
|
866
|
+
serverId: parseInt($el.attr("data-server-id") || $el.attr("data-id") || "0"),
|
|
867
|
+
serverName: ($el.text().trim() || $el.attr("data-server-name")) ?? "",
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
$(".server-item[data-type='raw'] .server-name, .ps-server-list[data-type='raw'] li").each((_, el) => {
|
|
871
|
+
const $el = $(el);
|
|
872
|
+
raw.push({
|
|
873
|
+
serverId: parseInt($el.attr("data-server-id") || $el.attr("data-id") || "0"),
|
|
874
|
+
serverName: ($el.text().trim() || $el.attr("data-server-name")) ?? "",
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
return {
|
|
878
|
+
episodeId,
|
|
879
|
+
episodeNo: data.episode_no ?? 0,
|
|
880
|
+
sub,
|
|
881
|
+
dub,
|
|
882
|
+
raw,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
catch (err) {
|
|
886
|
+
if (err instanceof ExtensionsError)
|
|
887
|
+
throw err;
|
|
888
|
+
throwExtensionsError(`Failed to get episode servers for ${episodeId}`, 500, this.id);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// ── Episode Sources ────────────────────────────────────────────────────────
|
|
892
|
+
/**
|
|
893
|
+
* Get streaming source URLs (HLS .m3u8) for an episode.
|
|
894
|
+
*
|
|
895
|
+
* @param episodeId - Episode ID string (e.g. "steinsgate-3?ep=230")
|
|
896
|
+
* @param server - Server name (default: "hd-1")
|
|
897
|
+
* @param category - "sub", "dub", or "raw" (default: "sub")
|
|
898
|
+
*
|
|
899
|
+
* @example
|
|
900
|
+
* ```ts
|
|
901
|
+
* const data = await hianime.getEpisodeSources(
|
|
902
|
+
* "steinsgate-3?ep=230",
|
|
903
|
+
* "hd-1",
|
|
904
|
+
* "sub"
|
|
905
|
+
* );
|
|
906
|
+
* // Play with: data.sources[0].url (HLS .m3u8 stream)
|
|
907
|
+
* ```
|
|
908
|
+
*/
|
|
909
|
+
async getEpisodeSources(episodeId, server = "hd-1", category = "sub") {
|
|
910
|
+
try {
|
|
911
|
+
const epId = episodeId.split("?ep=")[1] ?? episodeId;
|
|
912
|
+
// Step 1: Get server embed URL
|
|
913
|
+
const { data: serverData } = await this.http.get(`${AJAX_URL}/v2/episode/sources?id=${epId}&server=${server}&category=${category}`);
|
|
914
|
+
if (!serverData?.link) {
|
|
915
|
+
throwExtensionsError(`No streaming source found for episode ${episodeId} on server ${server}`, 404, this.id);
|
|
916
|
+
}
|
|
917
|
+
// Step 2: Resolve streaming URL from embed link
|
|
918
|
+
// (Actual decryption varies by server; we return the embed link info here)
|
|
919
|
+
return {
|
|
920
|
+
headers: {
|
|
921
|
+
Referer: BASE_URL,
|
|
922
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
923
|
+
},
|
|
924
|
+
sources: [
|
|
925
|
+
{
|
|
926
|
+
url: serverData.link,
|
|
927
|
+
isM3U8: serverData.link.includes(".m3u8"),
|
|
928
|
+
quality: "auto",
|
|
929
|
+
},
|
|
930
|
+
],
|
|
931
|
+
subtitles: [],
|
|
932
|
+
anilistID: null,
|
|
933
|
+
malID: null,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
catch (err) {
|
|
937
|
+
if (err instanceof ExtensionsError)
|
|
938
|
+
throw err;
|
|
939
|
+
throwExtensionsError(`Failed to get episode sources for ${episodeId}`, 500, this.id);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
//# sourceMappingURL=HiAnime.js.map
|