@serifpersia/dango 3.0.3

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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +247 -0
  3. package/client/dist/assets/AnimeInfo-CMxs0nRH.css +1 -0
  4. package/client/dist/assets/AnimeInfo-DVdaocCl.js +1 -0
  5. package/client/dist/assets/AnimeInfoPage-Bx5EvGt-.js +2 -0
  6. package/client/dist/assets/Asmr-C2i9GKD4.css +1 -0
  7. package/client/dist/assets/Asmr-CIjmFtRH.js +2 -0
  8. package/client/dist/assets/Asmr.module-DPG2JhDJ.js +1 -0
  9. package/client/dist/assets/AvSyncCalibrator-CIYcyon5.css +1 -0
  10. package/client/dist/assets/AvSyncCalibrator-vqErH7Cm.js +10 -0
  11. package/client/dist/assets/ErrorMessage-BwSjzblw.js +1 -0
  12. package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
  13. package/client/dist/assets/Home-D7YfGBUk.js +1 -0
  14. package/client/dist/assets/Home-DKCfEiwl.css +1 -0
  15. package/client/dist/assets/Insights-BljI64Ua.css +1 -0
  16. package/client/dist/assets/Insights-CLQGrlpX.js +1 -0
  17. package/client/dist/assets/Manga-DQ9HYnpk.js +1 -0
  18. package/client/dist/assets/Manga-H8Wb04w3.css +1 -0
  19. package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
  20. package/client/dist/assets/Map-L_ryVBTq.js +1 -0
  21. package/client/dist/assets/Mature-BeWzAJ7B.js +1 -0
  22. package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
  23. package/client/dist/assets/Player-CYpd__FP.js +30 -0
  24. package/client/dist/assets/Player-DreAhybc.css +1 -0
  25. package/client/dist/assets/PlayerRedirect-fxUAp9JV.js +1 -0
  26. package/client/dist/assets/PlayerSettings-BcsotxP9.js +1 -0
  27. package/client/dist/assets/PlayerSettings-Cqua1CDD.css +1 -0
  28. package/client/dist/assets/QueueRail-B9Pgbqc3.js +1 -0
  29. package/client/dist/assets/QueueRail-BnaltOP8.css +1 -0
  30. package/client/dist/assets/Radio-BvdY5twD.js +1 -0
  31. package/client/dist/assets/Radio-CUuYy3DY.css +1 -0
  32. package/client/dist/assets/Search-BzO-aRP7.css +1 -0
  33. package/client/dist/assets/Search-R9YLAXS1.js +1 -0
  34. package/client/dist/assets/Search.module-D8ZbQAiK.js +1 -0
  35. package/client/dist/assets/Settings-9kYcXdTR.js +21 -0
  36. package/client/dist/assets/Settings-DF8JThUL.css +1 -0
  37. package/client/dist/assets/SkeletonGrid-D5vRvX5q.js +1 -0
  38. package/client/dist/assets/SkeletonGrid-DEbYtO4H.css +1 -0
  39. package/client/dist/assets/SynopsisText-BlFw0R-0.js +1 -0
  40. package/client/dist/assets/SynopsisText-CUgOjOxt.css +1 -0
  41. package/client/dist/assets/Trackers-DfZlnnVq.js +2 -0
  42. package/client/dist/assets/Trackers-Dt9lR_uP.css +1 -0
  43. package/client/dist/assets/Tv-CVKC2YBs.css +1 -0
  44. package/client/dist/assets/Tv-Db1Wx_b7.js +15 -0
  45. package/client/dist/assets/Watchlist-B2nkNZqM.css +1 -0
  46. package/client/dist/assets/Watchlist-a77CYtzu.js +1 -0
  47. package/client/dist/assets/hls-6Owgw2Aj.js +1 -0
  48. package/client/dist/assets/hls-BOLXTnjL.js +42 -0
  49. package/client/dist/assets/index-B6qdXFtl.css +1 -0
  50. package/client/dist/assets/index-B8TPVaIS.js +30354 -0
  51. package/client/dist/assets/index-CpjgU8ou.js +178 -0
  52. package/client/dist/assets/useAnimeInfoData-BOfyi9UJ.js +1 -0
  53. package/client/dist/assets/useIsMobile-CM15uQn8.css +1 -0
  54. package/client/dist/assets/useIsMobile-Cb8HcKRj.js +1 -0
  55. package/client/dist/assets/useMatureConsent-CZMYBVXd.js +1 -0
  56. package/client/dist/assets/vendor-rLNfZm7Y.js +1 -0
  57. package/client/dist/av-sync-test.mp4 +0 -0
  58. package/client/dist/favicon.ico +0 -0
  59. package/client/dist/index.html +31 -0
  60. package/client/dist/logo-mochi.png +0 -0
  61. package/client/dist/logo-sakura.png +0 -0
  62. package/client/dist/logo.png +0 -0
  63. package/client/dist/placeholder.svg +4 -0
  64. package/client/dist/robots.txt +3 -0
  65. package/orchestrator.js +416 -0
  66. package/package.json +89 -0
  67. package/server/dist/app-auth.js +231 -0
  68. package/server/dist/config.js +133 -0
  69. package/server/dist/controllers/auth.controller.js +247 -0
  70. package/server/dist/controllers/data.controller.js +785 -0
  71. package/server/dist/controllers/insights.controller.js +197 -0
  72. package/server/dist/controllers/proxy.controller.js +665 -0
  73. package/server/dist/controllers/settings.controller.js +267 -0
  74. package/server/dist/controllers/watchlist.controller.js +967 -0
  75. package/server/dist/db.js +152 -0
  76. package/server/dist/discord-gateway.js +381 -0
  77. package/server/dist/discord-rpc.js +424 -0
  78. package/server/dist/github-sync.js +403 -0
  79. package/server/dist/google.js +549 -0
  80. package/server/dist/lib/anilist.js +1519 -0
  81. package/server/dist/lib/anischedule.js +202 -0
  82. package/server/dist/lib/ipc.js +32 -0
  83. package/server/dist/lib/kitsu.js +552 -0
  84. package/server/dist/lib/mal.js +886 -0
  85. package/server/dist/lib/migration.js +335 -0
  86. package/server/dist/lib/temp-ids.js +11 -0
  87. package/server/dist/lib/tmdb.js +71 -0
  88. package/server/dist/lib/tracker/anilist-tracker.js +250 -0
  89. package/server/dist/lib/tracker/sync.service.js +312 -0
  90. package/server/dist/logger.js +19 -0
  91. package/server/dist/providers/123anime.provider.js +240 -0
  92. package/server/dist/providers/anilight.provider.js +366 -0
  93. package/server/dist/providers/animepahe.provider.js +531 -0
  94. package/server/dist/providers/animeya.provider.js +746 -0
  95. package/server/dist/providers/base-provider.js +45 -0
  96. package/server/dist/providers/hn.provider.js +473 -0
  97. package/server/dist/providers/ht.provider.js +439 -0
  98. package/server/dist/providers/jasmr.provider.js +504 -0
  99. package/server/dist/providers/kaa.provider.js +388 -0
  100. package/server/dist/providers/manga/manga.types.js +10 -0
  101. package/server/dist/providers/manga/mangadex.provider.js +227 -0
  102. package/server/dist/providers/manga/mangapill.provider.js +232 -0
  103. package/server/dist/providers/megaplay.provider.js +583 -0
  104. package/server/dist/providers/op.provider.js +440 -0
  105. package/server/dist/providers/provider.interface.js +2 -0
  106. package/server/dist/providers/title-matching.js +234 -0
  107. package/server/dist/providers/wh.provider.js +600 -0
  108. package/server/dist/rclone.js +126 -0
  109. package/server/dist/repositories/insights.repository.js +43 -0
  110. package/server/dist/repositories/mal-cache.repository.js +66 -0
  111. package/server/dist/repositories/notifications.repository.js +40 -0
  112. package/server/dist/repositories/queue.repository.js +64 -0
  113. package/server/dist/repositories/settings.repository.js +15 -0
  114. package/server/dist/repositories/shows-meta.repository.js +43 -0
  115. package/server/dist/repositories/temp-show-ids.repository.js +54 -0
  116. package/server/dist/repositories/watched-episodes.repository.js +54 -0
  117. package/server/dist/repositories/watchlist.repository.js +74 -0
  118. package/server/dist/routes/asmr.routes.js +72 -0
  119. package/server/dist/routes/auth.routes.js +28 -0
  120. package/server/dist/routes/data.routes.js +50 -0
  121. package/server/dist/routes/discord-gateway.routes.js +57 -0
  122. package/server/dist/routes/insights.routes.js +12 -0
  123. package/server/dist/routes/lan-auth.routes.js +49 -0
  124. package/server/dist/routes/manga.routes.js +115 -0
  125. package/server/dist/routes/proxy.routes.js +15 -0
  126. package/server/dist/routes/radio.routes.js +143 -0
  127. package/server/dist/routes/settings.routes.js +27 -0
  128. package/server/dist/routes/tracker.routes.js +143 -0
  129. package/server/dist/routes/translate.routes.js +90 -0
  130. package/server/dist/routes/tv.routes.js +843 -0
  131. package/server/dist/routes/watchlist.routes.js +312 -0
  132. package/server/dist/server.js +310 -0
  133. package/server/dist/shipped-defaults.js +8 -0
  134. package/server/dist/sync-config.js +32 -0
  135. package/server/dist/sync.js +506 -0
  136. package/server/dist/utils/cookie.utils.js +17 -0
  137. package/server/dist/utils/db-utils.js +15 -0
  138. package/server/dist/utils/env.utils.js +83 -0
  139. package/server/dist/utils/machine-id.js +46 -0
  140. package/server/dist/utils/request-context.js +5 -0
  141. package/server/dist/utils/security.utils.js +99 -0
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseProvider = void 0;
4
+ const title_matching_1 = require("./title-matching");
5
+ /**
6
+ * Shared base for every scraping provider.
7
+ *
8
+ * Provides:
9
+ * - `NodeCache` lifecycle
10
+ * - Shared `resolveShowId` implementation (buildQueryVariants → search → pickBestMatch)
11
+ *
12
+ * Subclasses implement `name`, `search`, `getEpisodes`, `getStreamUrls`,
13
+ * and any other provider-specific methods.
14
+ */
15
+ class BaseProvider {
16
+ cache;
17
+ constructor(cache) {
18
+ this.cache = cache;
19
+ }
20
+ async resolveShowId(title, romaji) {
21
+ const targets = [title, romaji].filter((t) => !!t && t.trim().length > 0);
22
+ if (targets.length === 0)
23
+ return null;
24
+ for (const variant of (0, title_matching_1.buildQueryVariants)(title, romaji)) {
25
+ let results;
26
+ try {
27
+ results = await this.search({ query: variant });
28
+ }
29
+ catch {
30
+ continue;
31
+ }
32
+ if (results.length === 0)
33
+ continue;
34
+ const candidates = results.map((r) => ({
35
+ title: r.name || r.englishName || '',
36
+ id: r.id || r._id || '',
37
+ }));
38
+ const matchResult = (0, title_matching_1.pickBestMatch)(candidates, targets);
39
+ if (matchResult)
40
+ return matchResult.item.id;
41
+ }
42
+ return null;
43
+ }
44
+ }
45
+ exports.BaseProvider = BaseProvider;
@@ -0,0 +1,473 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HnProvider = void 0;
7
+ const node_cache_1 = __importDefault(require("node-cache"));
8
+ const logger_1 = __importDefault(require("../logger"));
9
+ const title_matching_1 = require("./title-matching");
10
+ const BASE_URL = 'https://hentaini.com';
11
+ const API_URL = 'https://admin.hentaini.com/api';
12
+ const CDN_URL = 'https://admin.hentaini.com/uploads';
13
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
14
+ async function fetchText(url) {
15
+ const res = await fetch(url, {
16
+ headers: {
17
+ 'User-Agent': UA,
18
+ Referer: BASE_URL + '/',
19
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
20
+ 'Accept-Language': 'en-US,en;q=0.5',
21
+ },
22
+ signal: AbortSignal.timeout(30000),
23
+ });
24
+ if (!res.ok)
25
+ throw new Error(`HTTP ${res.status}: ${url}`);
26
+ return res.text();
27
+ }
28
+ async function fetchApi(path) {
29
+ try {
30
+ const res = await fetch(`${API_URL}${path}`, {
31
+ headers: {
32
+ 'User-Agent': UA,
33
+ Referer: BASE_URL + '/',
34
+ },
35
+ signal: AbortSignal.timeout(15000),
36
+ });
37
+ if (!res.ok)
38
+ return null;
39
+ return res.json();
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function imageUrl(path) {
46
+ if (!path)
47
+ return '';
48
+ if (path.startsWith('http'))
49
+ return path;
50
+ return `${CDN_URL}/${path}`;
51
+ }
52
+ function parseNuxtData(html) {
53
+ const match = html.match(/__NUXT_DATA__">\s*(\[.*?\])\s*<\//s);
54
+ if (!match)
55
+ return null;
56
+ let raw;
57
+ try {
58
+ raw = JSON.parse(match[1]);
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ if (!Array.isArray(raw))
64
+ return null;
65
+ const visited = new Set();
66
+ function resolve(v) {
67
+ if (typeof v === 'number' && v >= 0 && v < raw.length && Number.isInteger(v)) {
68
+ if (visited.has(v))
69
+ return v;
70
+ visited.add(v);
71
+ const result = resolve(raw[v]);
72
+ visited.delete(v);
73
+ return result;
74
+ }
75
+ if (Array.isArray(v)) {
76
+ const wrapper = v[0];
77
+ if (typeof wrapper === 'string' &&
78
+ (wrapper === 'ShallowReactive' || wrapper === 'ShallowRef' || wrapper === 'EmptyRef')) {
79
+ return resolve(v[1]);
80
+ }
81
+ return v.map(resolve);
82
+ }
83
+ if (v && typeof v === 'object') {
84
+ const obj = {};
85
+ for (const [k, val] of Object.entries(v)) {
86
+ obj[k] = resolve(val);
87
+ }
88
+ return obj;
89
+ }
90
+ return v;
91
+ }
92
+ const resolved = resolve(raw);
93
+ const extractEpisode = (obj) => {
94
+ if (!obj || typeof obj !== 'object')
95
+ return null;
96
+ const root = obj;
97
+ const entries = Array.isArray(root.data)
98
+ ? root.data
99
+ : root.data && typeof root.data === 'object'
100
+ ? root.data.data
101
+ : null;
102
+ if (Array.isArray(entries)) {
103
+ for (const entry of entries) {
104
+ if (entry && typeof entry === 'object') {
105
+ const e = entry;
106
+ if (typeof e.id === 'number' && typeof e.episode_number === 'number') {
107
+ return e;
108
+ }
109
+ }
110
+ }
111
+ }
112
+ for (const val of Object.values(root)) {
113
+ const result = extractEpisode(val);
114
+ if (result)
115
+ return result;
116
+ }
117
+ return null;
118
+ };
119
+ return extractEpisode(resolved);
120
+ }
121
+ function pickPoster(images) {
122
+ if (!images || images.length === 0)
123
+ return '';
124
+ const named = (n) => images.find((i) => (i.image_type?.name || '').toLowerCase() === n)?.path || '';
125
+ return imageUrl(named('cover') ||
126
+ named('poster') ||
127
+ named('thumbnail') ||
128
+ named('backdrop') ||
129
+ images[0].path ||
130
+ '');
131
+ }
132
+ function extractSeriesCover(html) {
133
+ const imgs = html.match(/<img[^>]+>/g) || [];
134
+ for (const img of imgs) {
135
+ if (/aspect-\[2\/3\]/.test(img)) {
136
+ const src = img.match(/src="([^"]+)"/)?.[1] || '';
137
+ if (src.startsWith('http'))
138
+ return src;
139
+ }
140
+ }
141
+ const cover = html.match(/https?:\/\/[^"'\s]*uploads\/[^"'\s]*cover[^"'\s]*/i)?.[0];
142
+ if (cover)
143
+ return cover;
144
+ const anyUpload = html.match(/https?:\/\/[^"'\s]*uploads\/[^"'\s]*\.(?:jpe?g|png|webp)/i)?.[0];
145
+ return anyUpload || '';
146
+ }
147
+ function resolveNuxtRefs(raw) {
148
+ const visited = new Set();
149
+ function resolve(v) {
150
+ if (typeof v === 'number' && Number.isInteger(v) && v >= 0 && v < raw.length) {
151
+ if (visited.has(v))
152
+ return v;
153
+ visited.add(v);
154
+ const result = resolve(raw[v]);
155
+ visited.delete(v);
156
+ return result;
157
+ }
158
+ if (Array.isArray(v)) {
159
+ const wrapper = v[0];
160
+ if (typeof wrapper === 'string' &&
161
+ (wrapper === 'ShallowReactive' || wrapper === 'ShallowRef' || wrapper === 'EmptyRef')) {
162
+ return resolve(v[1]);
163
+ }
164
+ return v.map(resolve);
165
+ }
166
+ if (v && typeof v === 'object') {
167
+ const obj = {};
168
+ for (const [k, val] of Object.entries(v)) {
169
+ obj[k] = resolve(val);
170
+ }
171
+ return obj;
172
+ }
173
+ return v;
174
+ }
175
+ return resolve(raw);
176
+ }
177
+ function collectLandingSeries(resolved) {
178
+ const out = [];
179
+ const seen = new Set();
180
+ const walk = (o, depth) => {
181
+ if (depth > 12 || !o || typeof o !== 'object')
182
+ return;
183
+ if (Array.isArray(o)) {
184
+ for (const x of o)
185
+ walk(x, depth + 1);
186
+ return;
187
+ }
188
+ const r = o;
189
+ if (typeof r.url === 'string' && typeof r.title === 'string' && Array.isArray(r.images)) {
190
+ const url = r.url;
191
+ if (url && !seen.has(url)) {
192
+ seen.add(url);
193
+ const images = r.images;
194
+ const genreList = (Array.isArray(r.genreList) ? r.genreList : []);
195
+ out.push({
196
+ url,
197
+ title: r.title,
198
+ titleEnglish: typeof r.title_english === 'string' ? r.title_english : r.title,
199
+ visits: Number(r.visits) || 0,
200
+ poster: pickPoster(images),
201
+ genres: genreList
202
+ .filter((g) => g && (g.url || g.name))
203
+ .map((g) => ({ slug: String(g.url || ''), name: String(g.name || g.url || '') })),
204
+ });
205
+ }
206
+ return;
207
+ }
208
+ for (const val of Object.values(r))
209
+ walk(val, depth + 1);
210
+ };
211
+ walk(resolved, 0);
212
+ return out;
213
+ }
214
+ class HnProvider {
215
+ name = 'HN';
216
+ posterCache = new Map();
217
+ landingCache = new node_cache_1.default({ stdTTL: 1800 });
218
+ bestMatch(results, query) {
219
+ if (!results.length)
220
+ return null;
221
+ const q = query.toLowerCase().trim();
222
+ let best = results[0];
223
+ let bestScore = -1;
224
+ for (const item of results) {
225
+ const title = item.title.toLowerCase();
226
+ let score = 0;
227
+ if (title === q)
228
+ score = 3;
229
+ else if (title.startsWith(q))
230
+ score = 2;
231
+ else if (title.includes(q))
232
+ score = 1;
233
+ if (score > bestScore) {
234
+ bestScore = score;
235
+ best = item;
236
+ if (score === 3)
237
+ break;
238
+ }
239
+ }
240
+ return { ...best, score: bestScore };
241
+ }
242
+ async resolvePoster(slug, apiPoster) {
243
+ if (apiPoster)
244
+ return apiPoster;
245
+ const cached = this.posterCache.get(slug);
246
+ if (cached !== undefined)
247
+ return cached;
248
+ try {
249
+ const html = await fetchText(`${BASE_URL}/h/${slug}`);
250
+ const poster = extractSeriesCover(html);
251
+ this.posterCache.set(slug, poster);
252
+ return poster;
253
+ }
254
+ catch {
255
+ this.posterCache.set(slug, '');
256
+ return '';
257
+ }
258
+ }
259
+ async browse(options) {
260
+ try {
261
+ const query = (options.query || '').trim();
262
+ const genre = (options.genre || '').trim().toLowerCase();
263
+ const sort = options.sort || '';
264
+ if (!query) {
265
+ const cacheKey = 'hn_landing';
266
+ let landing = this.landingCache.get(cacheKey);
267
+ if (!landing) {
268
+ const html = await fetchText(`${BASE_URL}/`);
269
+ const match = html.match(/__NUXT_DATA__">\s*(\[.*?\])\s*<\//s);
270
+ if (!match)
271
+ return { shows: [], hasMore: false, genres: [] };
272
+ const raw = JSON.parse(match[1]);
273
+ landing = collectLandingSeries(resolveNuxtRefs(raw));
274
+ this.landingCache.set(cacheKey, landing, 1800);
275
+ }
276
+ const genreSet = new Map();
277
+ for (const s of landing) {
278
+ for (const g of s.genres) {
279
+ if (g.slug && !genreSet.has(g.slug))
280
+ genreSet.set(g.slug, g.name);
281
+ }
282
+ }
283
+ const genres = Array.from(genreSet.entries()).map(([slug, name]) => ({ slug, name }));
284
+ let filtered = landing;
285
+ if (genre) {
286
+ filtered = landing.filter((s) => s.genres.some((g) => g.slug.toLowerCase() === genre));
287
+ }
288
+ if (sort === 'visits:desc') {
289
+ filtered = [...filtered].sort((a, b) => b.visits - a.visits);
290
+ }
291
+ else if (sort === 'title:asc') {
292
+ filtered = [...filtered].sort((a, b) => a.title.localeCompare(b.title));
293
+ }
294
+ const shows = await Promise.all(filtered.map(async (s) => ({
295
+ _id: s.url,
296
+ id: s.url,
297
+ name: s.title,
298
+ englishName: s.titleEnglish,
299
+ thumbnail: await this.resolvePoster(s.url, s.poster),
300
+ type: 'TV',
301
+ year: null,
302
+ isAdult: true,
303
+ availableEpisodesDetail: { sub: [], dub: [] },
304
+ })));
305
+ return { shows, hasMore: false, genres };
306
+ }
307
+ const page = Math.max(1, options.page || 1);
308
+ const pageSize = Math.min(20, Math.max(1, options.pageSize || 14));
309
+ const params = new URLSearchParams();
310
+ params.set('filters[title][$containsi]', query);
311
+ params.set('pagination[page]', String(page));
312
+ params.set('pagination[pageSize]', String(pageSize));
313
+ if (sort)
314
+ params.set('sort', sort);
315
+ const fetchBrowse = () => fetchApi(`/series?${params.toString()}`);
316
+ let res = await fetchBrowse();
317
+ if (!res?.data && sort) {
318
+ params.delete('sort');
319
+ res = await fetchApi(`/series?${params.toString()}`);
320
+ }
321
+ const items = res?.data || [];
322
+ const shows = await Promise.all(items.map(async (item) => ({
323
+ _id: item.url,
324
+ id: item.url,
325
+ name: item.title,
326
+ englishName: item.title_english || item.title,
327
+ thumbnail: await this.resolvePoster(item.url, pickPoster(item.images)),
328
+ type: 'TV',
329
+ year: null,
330
+ isAdult: true,
331
+ availableEpisodesDetail: { sub: [], dub: [] },
332
+ })));
333
+ const pageCount = res?.meta?.pagination?.pageCount;
334
+ const hasMore = typeof pageCount === 'number' ? page < pageCount : shows.length >= pageSize;
335
+ return { shows, hasMore, genres: [] };
336
+ }
337
+ catch (error) {
338
+ logger_1.default.error({ error }, '[HN] Browse failed');
339
+ return { shows: [], hasMore: false, genres: [] };
340
+ }
341
+ }
342
+ async search(options) {
343
+ try {
344
+ const query = (options.query || '').trim();
345
+ if (!query)
346
+ return [];
347
+ const res = await fetchApi(`/series?filters[title][$containsi]=${encodeURIComponent(query)}&pagination[limit]=10`);
348
+ const apiResults = (res?.data || []).map((item) => ({
349
+ title: item.title,
350
+ slug: item.url,
351
+ poster: pickPoster(item.images),
352
+ score: 0,
353
+ }));
354
+ if (apiResults.length === 0)
355
+ return [];
356
+ const matched = this.bestMatch(apiResults, query) || apiResults[0];
357
+ return [
358
+ {
359
+ _id: matched.slug,
360
+ id: matched.slug,
361
+ name: matched.title,
362
+ englishName: matched.title,
363
+ thumbnail: await this.resolvePoster(matched.slug, matched.poster),
364
+ type: 'TV',
365
+ year: null,
366
+ availableEpisodesDetail: { sub: [], dub: [] },
367
+ },
368
+ ];
369
+ }
370
+ catch (error) {
371
+ logger_1.default.error({ error }, '[HN] Search failed');
372
+ return [];
373
+ }
374
+ }
375
+ async resolveShowId(title, romaji) {
376
+ const query = (romaji || title).trim();
377
+ if (!query)
378
+ return null;
379
+ const targets = [title, romaji].filter((t) => !!t);
380
+ for (const variant of (0, title_matching_1.buildQueryVariants)(title, romaji)) {
381
+ const res = await fetchApi(`/series?filters[title][$containsi]=${encodeURIComponent(variant)}&pagination[limit]=10`);
382
+ const items = (res?.data || []).map((item) => ({
383
+ title: item.title || item.title_english,
384
+ slug: item.url,
385
+ poster: '',
386
+ }));
387
+ if (items.length === 0)
388
+ continue;
389
+ const matchResult = (0, title_matching_1.pickBestMatch)(items, targets);
390
+ if (matchResult) {
391
+ return matchResult.item.slug;
392
+ }
393
+ }
394
+ return null;
395
+ }
396
+ async getEpisodes(showId) {
397
+ try {
398
+ if (!showId)
399
+ return null;
400
+ const html = await fetchText(`${BASE_URL}/h/${showId}`);
401
+ const episodeRe = new RegExp(`/h/${showId}/(\\d+)`, 'g');
402
+ const numbers = new Set();
403
+ let m;
404
+ while ((m = episodeRe.exec(html)) !== null) {
405
+ numbers.add(m[1]);
406
+ }
407
+ const episodes = [...numbers].sort((a, b) => Number(a) - Number(b));
408
+ return { episodes, description: '' };
409
+ }
410
+ catch (error) {
411
+ logger_1.default.error({ error, showId }, '[HN] getEpisodes failed');
412
+ return null;
413
+ }
414
+ }
415
+ async getStreamUrls(showId, episodeNumber, _mode) {
416
+ try {
417
+ const episodeUrl = `${BASE_URL}/h/${showId}/${episodeNumber}`;
418
+ const html = await fetchText(episodeUrl);
419
+ const m3u8Match = html.match(/https?:\/\/[^"' ]+\.m3u8[^"' ]*/i);
420
+ if (!m3u8Match)
421
+ return null;
422
+ let streamUrl = m3u8Match[0].replace(/\\+$/g, '').trim();
423
+ streamUrl = streamUrl.replace(/\\"/g, '"').replace(/"$/g, '').replace(/\\\//g, '/');
424
+ const links = [
425
+ {
426
+ resolutionStr: 'Auto',
427
+ link: streamUrl,
428
+ hls: true,
429
+ headers: {
430
+ Referer: BASE_URL + '/',
431
+ Origin: BASE_URL,
432
+ 'User-Agent': UA,
433
+ },
434
+ },
435
+ ];
436
+ const result = [
437
+ {
438
+ sourceName: 'HN (Direct)',
439
+ links,
440
+ type: 'player',
441
+ actualEpisodeNumber: episodeNumber,
442
+ },
443
+ ];
444
+ const nuxtEntry = parseNuxtData(html);
445
+ if (nuxtEntry && nuxtEntry.players) {
446
+ try {
447
+ const players = JSON.parse(nuxtEntry.players);
448
+ if (Array.isArray(players)) {
449
+ for (const p of players) {
450
+ if (p.name === 'HLS' || !p.url)
451
+ continue;
452
+ result.push({
453
+ sourceName: `HN (${p.name})`,
454
+ links: [{ resolutionStr: 'Auto', link: p.url, hls: false }],
455
+ type: 'iframe',
456
+ actualEpisodeNumber: episodeNumber,
457
+ });
458
+ }
459
+ }
460
+ }
461
+ catch {
462
+ // ignore
463
+ }
464
+ }
465
+ return result;
466
+ }
467
+ catch (error) {
468
+ logger_1.default.error({ error, showId, episodeNumber }, '[HN] getStreamUrls failed');
469
+ return null;
470
+ }
471
+ }
472
+ }
473
+ exports.HnProvider = HnProvider;