@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,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