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