@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,440 @@
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.OpProvider = exports.OP_TAGS = exports.OP_ORDERS = void 0;
7
+ const logger_1 = __importDefault(require("../logger"));
8
+ const title_matching_1 = require("./title-matching");
9
+ const base_provider_1 = require("./base-provider");
10
+ const BASE_URL = 'https://oppai.stream';
11
+ const SEARCH_URL = `${BASE_URL}/actions/search.php`;
12
+ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
13
+ async function fetchText(url) {
14
+ const res = await fetch(url, {
15
+ headers: {
16
+ 'User-Agent': UA,
17
+ Referer: `${BASE_URL}/`,
18
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
19
+ 'Accept-Language': 'en-US,en;q=0.5',
20
+ },
21
+ redirect: 'manual',
22
+ signal: AbortSignal.timeout(30000),
23
+ });
24
+ const text = await res.text();
25
+ const isRedirect = res.status === 301 || res.status === 302 || res.status === 307 || res.status === 308;
26
+ if (!res.ok && !isRedirect)
27
+ throw new Error(`HTTP ${res.status}: ${url}`);
28
+ return text;
29
+ }
30
+ function parseSearchResults(html) {
31
+ const entries = [];
32
+ const re = /<div\s+class='in-grid episode-shown'\s+id='([^']+)'[^>]*idgt='([^']+)'[^>]*folder='([^']+)'[^>]*ep='([^']+)'[^>]*name='([^']+)'[^>]*desc='([^']*)'[^>]*>[\s\S]*?<a\s+href='(https?:\/\/oppai\.stream\/watch\?e=[^']+)'/g;
33
+ let m;
34
+ while ((m = re.exec(html)) !== null) {
35
+ entries.push({
36
+ id: m[1],
37
+ idgt: m[2],
38
+ folder: m[3],
39
+ ep: m[4],
40
+ name: m[5],
41
+ desc: m[6],
42
+ watchUrl: m[7],
43
+ });
44
+ }
45
+ return entries;
46
+ }
47
+ function folderToTitleSlug(folder) {
48
+ return folder
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9]+/g, '-')
51
+ .replace(/^-|-$/g, '');
52
+ }
53
+ function titleSlugToFolder(slug) {
54
+ return slug.replace(/-/g, ' ');
55
+ }
56
+ exports.OP_ORDERS = ['recent', 'popular', 'views', 'rating', 'random'];
57
+ exports.OP_TAGS = [
58
+ '4k',
59
+ 'ahegao',
60
+ 'anal',
61
+ 'armpitmasturbation',
62
+ 'bdsm',
63
+ 'bigboobs',
64
+ 'blackhair',
65
+ 'blondehair',
66
+ 'blowjob',
67
+ 'bluehair',
68
+ 'bondage',
69
+ 'boobjob',
70
+ 'brownhair',
71
+ 'censored',
72
+ 'comedy',
73
+ 'cosplay',
74
+ 'cowgirl',
75
+ 'creampie',
76
+ 'darkskin',
77
+ 'demon',
78
+ 'doggy',
79
+ 'dominantgirl',
80
+ 'doublepenetration',
81
+ 'elf',
82
+ 'facial',
83
+ 'fantasy',
84
+ 'filmed',
85
+ 'footjob',
86
+ 'futanari',
87
+ 'gangbang',
88
+ 'glasses',
89
+ 'greenhair',
90
+ 'gyaru',
91
+ 'handjob',
92
+ 'harem',
93
+ 'hd',
94
+ 'incest',
95
+ 'inflation',
96
+ 'loli',
97
+ 'maid',
98
+ 'masturbation',
99
+ 'milf',
100
+ 'mindbreak',
101
+ 'mindcontrol',
102
+ 'missionary',
103
+ 'monster',
104
+ 'nekomimi',
105
+ 'ntr',
106
+ 'nurse',
107
+ 'old',
108
+ 'orgy',
109
+ 'pinkhair',
110
+ 'plot',
111
+ 'ponytail',
112
+ 'pov',
113
+ 'pregnant',
114
+ 'publicsex',
115
+ 'purplehair',
116
+ 'rape',
117
+ 'redhair',
118
+ 'reverserape',
119
+ 'rimjob',
120
+ 'scat',
121
+ 'schoolgirl',
122
+ 'shorthair',
123
+ 'shota',
124
+ 'smallboobs',
125
+ 'softcore',
126
+ 'succubus',
127
+ 'swimsuit',
128
+ 'teacher',
129
+ 'tentacle',
130
+ 'threesome',
131
+ 'toys',
132
+ 'uglybastard',
133
+ 'uncensored',
134
+ 'vanilla',
135
+ 'virgin',
136
+ 'whitehair',
137
+ 'x-ray',
138
+ 'yuri',
139
+ ];
140
+ function buildSeriesMap(entries) {
141
+ const map = new Map();
142
+ for (const e of entries) {
143
+ const key = e.folder.toLowerCase();
144
+ const existing = map.get(key) || [];
145
+ existing.push(e);
146
+ map.set(key, existing);
147
+ }
148
+ return map;
149
+ }
150
+ function bestMatch(series, query) {
151
+ if (!series.length)
152
+ return null;
153
+ const q = query.toLowerCase().trim();
154
+ let best = series[0];
155
+ let bestScore = -1;
156
+ for (const item of series) {
157
+ const title = item.title.toLowerCase();
158
+ let score = 0;
159
+ if (title === q)
160
+ score = 3;
161
+ else if (title.startsWith(q))
162
+ score = 2;
163
+ else if (title.includes(q))
164
+ score = 1;
165
+ if (score > bestScore) {
166
+ bestScore = score;
167
+ best = item;
168
+ if (score === 3)
169
+ break;
170
+ }
171
+ }
172
+ return { ...best, score: bestScore };
173
+ }
174
+ class OpProvider extends base_provider_1.BaseProvider {
175
+ name = 'Op';
176
+ async browse(options) {
177
+ try {
178
+ const query = (options.query || '').trim();
179
+ const page = Math.max(1, options.page || 1);
180
+ const limit = Math.min(50, Math.max(1, options.limit || 24));
181
+ const order = exports.OP_ORDERS.includes(options.order || '') ? options.order : 'recent';
182
+ const genres = (options.genres || '').trim();
183
+ const blacklist = (options.blacklist || '').trim();
184
+ const studio = (options.studio || '').trim();
185
+ const cacheKey = `op_browse_${query}_${page}_${limit}_${order}_${genres}_${blacklist}_${studio}`;
186
+ const cached = this.cache.get(cacheKey);
187
+ if (cached)
188
+ return cached;
189
+ const params = new URLSearchParams({
190
+ text: query,
191
+ order,
192
+ page: String(page),
193
+ limit: String(limit),
194
+ genres,
195
+ blacklist,
196
+ studio,
197
+ ibt: '0',
198
+ swa: '1',
199
+ });
200
+ const html = await fetchText(`${SEARCH_URL}?${params.toString()}`);
201
+ const entries = parseSearchResults(html);
202
+ const totalMatch = html.match(/amo='(\d+)'/);
203
+ const total = totalMatch ? parseInt(totalMatch[1]) : entries.length;
204
+ const seriesMap = buildSeriesMap(entries);
205
+ const shows = Array.from(seriesMap.entries()).map(([key, eps]) => {
206
+ const sorted = [...eps].sort((a, b) => parseFloat(a.ep) - parseFloat(b.ep));
207
+ const first = sorted[0];
208
+ return {
209
+ _id: key,
210
+ id: key,
211
+ name: first.name,
212
+ englishName: first.name,
213
+ thumbnail: `https://myspacecat.pictures/${encodeURIComponent(first.folder)}/thumbnail_${first.ep}.png`,
214
+ type: 'TV',
215
+ year: null,
216
+ isAdult: true,
217
+ availableEpisodesDetail: {
218
+ sub: sorted.map((e) => e.ep),
219
+ dub: [],
220
+ },
221
+ };
222
+ });
223
+ const output = { shows, total, hasMore: page * limit < total };
224
+ this.cache.set(cacheKey, output, 300);
225
+ return output;
226
+ }
227
+ catch (error) {
228
+ logger_1.default.error({ error }, '[OP] Browse failed');
229
+ return { shows: [], total: 0, hasMore: false };
230
+ }
231
+ }
232
+ async search(options) {
233
+ try {
234
+ const query = (options.query || '').trim();
235
+ if (!query)
236
+ return [];
237
+ const cacheKey = `op_search_${query}`;
238
+ const cached = this.cache.get(cacheKey);
239
+ if (cached)
240
+ return cached;
241
+ const url = `${SEARCH_URL}?text=${encodeURIComponent(query)}&order=recent&page=1&limit=23&genres=&blacklist=&studio=&ibt=0&swa=1`;
242
+ const html = await fetchText(url);
243
+ const entries = parseSearchResults(html);
244
+ if (entries.length === 0)
245
+ return [];
246
+ const seriesMap = buildSeriesMap(entries);
247
+ const uniqueSeries = Array.from(seriesMap.entries()).map(([key, eps]) => ({
248
+ title: eps[0].name,
249
+ key,
250
+ }));
251
+ const match = bestMatch(uniqueSeries, query) || uniqueSeries[0];
252
+ const episodes = seriesMap.get(match.key) || [];
253
+ const first = episodes[0];
254
+ const sortedEps = episodes
255
+ .sort((a, b) => parseFloat(a.ep) - parseFloat(b.ep))
256
+ .map((e) => e.ep);
257
+ const thumbnailUrl = `https://myspacecat.pictures/${encodeURIComponent(first.folder)}/thumbnail_${first.ep}.png`;
258
+ const result = [
259
+ {
260
+ _id: match.key,
261
+ id: match.key,
262
+ name: first.name,
263
+ englishName: first.name,
264
+ thumbnail: thumbnailUrl,
265
+ type: 'TV',
266
+ year: null,
267
+ availableEpisodesDetail: {
268
+ sub: sortedEps,
269
+ dub: [],
270
+ },
271
+ },
272
+ ];
273
+ this.cache.set(cacheKey, result, 300);
274
+ return result;
275
+ }
276
+ catch (error) {
277
+ logger_1.default.error({ error }, '[OP] Search failed');
278
+ return [];
279
+ }
280
+ }
281
+ async resolveShowId(title, romaji) {
282
+ const query = (romaji || title).trim();
283
+ if (!query)
284
+ return null;
285
+ const targets = [title, romaji].filter((t) => !!t);
286
+ for (const variant of (0, title_matching_1.buildQueryVariants)(title, romaji)) {
287
+ const url = `${SEARCH_URL}?text=${encodeURIComponent(variant)}&order=recent&page=1&limit=23&genres=&blacklist=&studio=&ibt=0&swa=1`;
288
+ const html = await fetchText(url);
289
+ const entries = parseSearchResults(html);
290
+ if (entries.length === 0)
291
+ continue;
292
+ const seriesMap = buildSeriesMap(entries);
293
+ const nameCandidates = [];
294
+ for (const [key, eps] of seriesMap.entries()) {
295
+ const seenNames = new Set();
296
+ for (const ep of eps) {
297
+ if (ep.name && !seenNames.has(ep.name.toLowerCase())) {
298
+ seenNames.add(ep.name.toLowerCase());
299
+ nameCandidates.push({ title: ep.name, key });
300
+ }
301
+ }
302
+ }
303
+ const match = (0, title_matching_1.pickBestMatch)(nameCandidates, targets);
304
+ if (match) {
305
+ return match.item.key;
306
+ }
307
+ }
308
+ return null;
309
+ }
310
+ async getEpisodes(showId) {
311
+ try {
312
+ if (!showId)
313
+ return null;
314
+ const cacheKey = `op_eps_${showId}`;
315
+ const cached = this.cache.get(cacheKey);
316
+ if (cached)
317
+ return cached;
318
+ const folder = titleSlugToFolder(showId);
319
+ const compact = folder.toLowerCase().replace(/[^a-z0-9]/g, '');
320
+ const searchEntries = async (text, limit) => {
321
+ const url = `${SEARCH_URL}?text=${encodeURIComponent(text)}&order=recent&page=1&limit=${limit}&genres=&blacklist=&studio=&ibt=0&swa=1`;
322
+ const html = await fetchText(url);
323
+ return parseSearchResults(html);
324
+ };
325
+ const words = folder
326
+ .split(/\s+/)
327
+ .filter((w) => w.length >= 3)
328
+ .sort((a, b) => b.length - a.length);
329
+ let matched = [];
330
+ for (const text of [folder, ...words]) {
331
+ const entries = await searchEntries(text, 50);
332
+ matched = entries.filter((e) => e.folder.toLowerCase().replace(/[^a-z0-9]/g, '') === compact);
333
+ if (matched.length > 0)
334
+ break;
335
+ }
336
+ if (matched.length === 0)
337
+ return null;
338
+ matched.sort((a, b) => parseFloat(a.ep) - parseFloat(b.ep));
339
+ const episodes = matched.map((e) => e.ep);
340
+ const watchMap = {};
341
+ for (const e of matched) {
342
+ if (e.watchUrl)
343
+ watchMap[e.ep] = e.watchUrl;
344
+ }
345
+ this.cache.set(`op_watch_${showId}`, watchMap, 3600);
346
+ const desc = matched[0]?.desc || '';
347
+ const result = { episodes, description: desc };
348
+ this.cache.set(cacheKey, result, 3600);
349
+ return result;
350
+ }
351
+ catch (error) {
352
+ logger_1.default.error({ error, showId }, '[OP] getEpisodes failed');
353
+ return null;
354
+ }
355
+ }
356
+ async getStreamUrls(showId, episodeNumber, _mode) {
357
+ try {
358
+ const cacheKey = `op_stream_${showId}_${episodeNumber}`;
359
+ const cached = this.cache.get(cacheKey);
360
+ if (cached)
361
+ return cached;
362
+ let watchMap = this.cache.get(`op_watch_${showId}`);
363
+ if (!watchMap) {
364
+ await this.getEpisodes(showId);
365
+ watchMap = this.cache.get(`op_watch_${showId}`);
366
+ }
367
+ const watchUrl = watchMap?.[episodeNumber];
368
+ const folder = titleSlugToFolder(showId);
369
+ const url = watchUrl ??
370
+ `${BASE_URL}/watch?e=${encodeURIComponent(folderToTitleSlug(folder))}-${episodeNumber}`;
371
+ const html = await fetchText(url);
372
+ // Parse availableres JS object: {"1080":"https://...webm","4k":"https://...webm","720":"https://...mp4"}
373
+ const availResMatch = html.match(/var\s+availableres\s*=\s*(\{[^;]+\})/);
374
+ const links = [];
375
+ if (availResMatch) {
376
+ try {
377
+ const raw = availResMatch[1].replace(/\\\//g, '/');
378
+ const availRes = JSON.parse(raw);
379
+ const qualityMap = {
380
+ '720': '720p',
381
+ '1080': '1080p',
382
+ '4k': '4K',
383
+ };
384
+ for (const [key, videoUrl] of Object.entries(availRes)) {
385
+ if (!videoUrl || !key)
386
+ continue;
387
+ const label = qualityMap[key] || key;
388
+ links.push({ resolutionStr: label, link: videoUrl, hls: false });
389
+ }
390
+ const resOrder = { '720p': 1, '1080p': 2, '4K': 3 };
391
+ links.sort((a, b) => (resOrder[a.resolutionStr] || 99) - (resOrder[b.resolutionStr] || 99));
392
+ }
393
+ catch {
394
+ // ignore
395
+ }
396
+ }
397
+ // Fallback: try <source> tags
398
+ if (links.length === 0) {
399
+ const sourceRe = /src="(https?:\/\/[^"]+\.(mp4|webm)[^"]*)"/gi;
400
+ let sourceMatch;
401
+ while ((sourceMatch = sourceRe.exec(html)) !== null) {
402
+ const ext = sourceMatch[2].toLowerCase();
403
+ links.push({
404
+ resolutionStr: ext === 'mp4' ? '720p' : '1080p',
405
+ link: sourceMatch[1],
406
+ hls: false,
407
+ });
408
+ }
409
+ }
410
+ if (links.length === 0)
411
+ return null;
412
+ const subtitles = [];
413
+ const subRe = /src='(https?:\/\/[^']+\.vtt[^']*)'[^>]*kind='subtitles'[^>]*srclang='([^']+)'/g;
414
+ let subMatch;
415
+ while ((subMatch = subRe.exec(html)) !== null) {
416
+ subtitles.push({
417
+ language: subMatch[2],
418
+ label: subMatch[2].toUpperCase(),
419
+ url: subMatch[1],
420
+ });
421
+ }
422
+ const result = [
423
+ {
424
+ sourceName: 'OP (Direct)',
425
+ links,
426
+ subtitles: subtitles.length > 0 ? subtitles : undefined,
427
+ type: 'player',
428
+ actualEpisodeNumber: episodeNumber,
429
+ },
430
+ ];
431
+ this.cache.set(cacheKey, result, 3600);
432
+ return result;
433
+ }
434
+ catch (error) {
435
+ logger_1.default.error({ error, showId, episodeNumber }, '[OP] getStreamUrls failed');
436
+ return null;
437
+ }
438
+ }
439
+ }
440
+ exports.OpProvider = OpProvider;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeCompact = normalizeCompact;
4
+ exports.tokenize = tokenize;
5
+ exports.bigramDice = bigramDice;
6
+ exports.distinctiveTokens = distinctiveTokens;
7
+ exports.titleSimilarity = titleSimilarity;
8
+ exports.pickBestMatch = pickBestMatch;
9
+ exports.buildQueryVariants = buildQueryVariants;
10
+ function normalizeCompact(s) {
11
+ return s.toLowerCase().replace(/[^a-z0-9]/g, '');
12
+ }
13
+ function tokenize(s) {
14
+ return s
15
+ .toLowerCase()
16
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')
17
+ .split(/\s+/)
18
+ .filter((w) => w.length >= 2);
19
+ }
20
+ function bigramDice(a, b) {
21
+ if (a.length < 2 || b.length < 2)
22
+ return a === b ? 1 : 0;
23
+ const grams = (s) => {
24
+ const map = new Map();
25
+ for (let i = 0; i < s.length - 1; i++) {
26
+ const g = s.slice(i, i + 2);
27
+ map.set(g, (map.get(g) || 0) + 1);
28
+ }
29
+ return map;
30
+ };
31
+ const A = grams(a);
32
+ const B = grams(b);
33
+ let totalA = 0;
34
+ let totalB = 0;
35
+ let shared = 0;
36
+ A.forEach((c) => (totalA += c));
37
+ B.forEach((c) => (totalB += c));
38
+ A.forEach((c, g) => {
39
+ const other = B.get(g);
40
+ if (other)
41
+ shared += Math.min(c, other);
42
+ });
43
+ return (2 * shared) / (totalA + totalB);
44
+ }
45
+ const STOPWORDS = new Set([
46
+ 'the',
47
+ 'animation',
48
+ 'no',
49
+ 'wa',
50
+ 'ga',
51
+ 'wo',
52
+ 'ni',
53
+ 'de',
54
+ 'to',
55
+ 'e',
56
+ 'o',
57
+ 'x',
58
+ 'ii',
59
+ 'yo',
60
+ 'ova',
61
+ 'oad',
62
+ 'hd',
63
+ 'ep',
64
+ 'episode',
65
+ 'season',
66
+ 'part',
67
+ 'hen',
68
+ 'a',
69
+ ]);
70
+ const SEASON_TOKENS = new Set([
71
+ 'season',
72
+ '2nd',
73
+ '3rd',
74
+ '4th',
75
+ '5th',
76
+ 'ii',
77
+ 'iii',
78
+ 'iv',
79
+ 'v',
80
+ 'vi',
81
+ 'vii',
82
+ 'viii',
83
+ 'ix',
84
+ 'x',
85
+ 'two',
86
+ 'three',
87
+ 'four',
88
+ 'five',
89
+ 'six',
90
+ 'seven',
91
+ 'eight',
92
+ 'nine',
93
+ 'ten',
94
+ ]);
95
+ function distinctiveTokensExcludingSeason(s) {
96
+ return [...new Set(tokenize(s).filter((w) => !STOPWORDS.has(w) && !SEASON_TOKENS.has(w)))];
97
+ }
98
+ const ROMAN_TO_NUM = {
99
+ ii: 2,
100
+ iii: 3,
101
+ iv: 4,
102
+ v: 5,
103
+ vi: 6,
104
+ vii: 7,
105
+ viii: 8,
106
+ ix: 9,
107
+ x: 10,
108
+ };
109
+ const WORD_TO_NUM = {
110
+ two: 2,
111
+ three: 3,
112
+ four: 4,
113
+ five: 5,
114
+ six: 6,
115
+ seven: 7,
116
+ eight: 8,
117
+ nine: 9,
118
+ ten: 10,
119
+ };
120
+ function detectSeasonMarker(s) {
121
+ const t = s.toLowerCase();
122
+ // "season 2", "season ii", "season 2nd", "season three"
123
+ const seasonNum = t.match(/\bseason\s*(\d+|ii|iii|iv|v|vi|vii|viii|ix|x|two|three|four|five|six|seven|eight|nine|ten)\b/);
124
+ if (seasonNum) {
125
+ const token = seasonNum[1];
126
+ const n = ROMAN_TO_NUM[token];
127
+ const w = WORD_TO_NUM[token];
128
+ return String(n ?? w ?? token);
129
+ }
130
+ const ord = t.match(/\b(\d+)(st|nd|rd|th)\b/);
131
+ if (ord)
132
+ return ord[1];
133
+ const trailing = t.match(/\b(ii|iii|iv|v|vi|vii|viii|ix|x)\b(?!\s*(st|nd|rd|th|season))/);
134
+ if (trailing)
135
+ return String(ROMAN_TO_NUM[trailing[1]]);
136
+ // bare trailing number, e.g. "...Another World 2"
137
+ const trailingNum = t.match(/\b(\d+)\s*(season)?\s*$/);
138
+ if (trailingNum)
139
+ return trailingNum[1];
140
+ return '';
141
+ }
142
+ function distinctiveTokens(s) {
143
+ return [...new Set(tokenize(s).filter((w) => !STOPWORDS.has(w)))];
144
+ }
145
+ function titleSimilarity(query, candidate) {
146
+ const q = normalizeCompact(query);
147
+ const c = normalizeCompact(candidate);
148
+ if (!q || !c)
149
+ return 0;
150
+ if (q === c)
151
+ return 1;
152
+ let score = bigramDice(q, c);
153
+ if (q.startsWith(c) || c.startsWith(q)) {
154
+ const prefixScore = 0.5 + 0.5 * (Math.min(q.length, c.length) / Math.max(q.length, c.length));
155
+ if (prefixScore > score)
156
+ score = prefixScore;
157
+ }
158
+ const qD = distinctiveTokensExcludingSeason(query);
159
+ const cD = distinctiveTokensExcludingSeason(candidate);
160
+ if (qD.length > 0 && cD.length >= 2) {
161
+ const cSet = new Set(cD);
162
+ let hits = 0;
163
+ for (const t of qD) {
164
+ if (cSet.has(t))
165
+ hits++;
166
+ }
167
+ const coverage = hits / Math.min(qD.length, cD.length);
168
+ if (coverage > score)
169
+ score = coverage;
170
+ }
171
+ const qMarker = detectSeasonMarker(query);
172
+ if (qMarker) {
173
+ const cMarker = detectSeasonMarker(candidate);
174
+ if (cMarker === qMarker) {
175
+ score += 0.25;
176
+ }
177
+ else if (!cMarker && score >= 0.5) {
178
+ score -= 0.25;
179
+ }
180
+ }
181
+ return score >= 1 ? 0.99 : score;
182
+ }
183
+ function pickBestMatch(candidates, targets, minScore = 0.6) {
184
+ if (!candidates.length)
185
+ return null;
186
+ const cleanTargets = targets.filter((t) => !!t && t.trim().length > 0);
187
+ if (!cleanTargets.length)
188
+ return null;
189
+ let best = null;
190
+ for (const item of candidates) {
191
+ if (!item.title)
192
+ continue;
193
+ let score = 0;
194
+ for (const target of cleanTargets) {
195
+ const s = titleSimilarity(target, item.title);
196
+ if (s > score)
197
+ score = s;
198
+ }
199
+ if (!best || score > best.score)
200
+ best = { item, score };
201
+ }
202
+ return best && best.score >= minScore ? best : null;
203
+ }
204
+ function buildQueryVariants(title, romaji) {
205
+ const seen = new Set();
206
+ const out = [];
207
+ const push = (q) => {
208
+ const v = q.replace(/\s+/g, ' ').trim();
209
+ if (v && !seen.has(v.toLowerCase())) {
210
+ seen.add(v.toLowerCase());
211
+ out.push(v);
212
+ }
213
+ };
214
+ if (romaji && romaji.trim())
215
+ push(romaji);
216
+ push(title);
217
+ const stripped = (s) => s.replace(/[^\p{L}\p{N}\s]/gu, ' ');
218
+ push(stripped(title));
219
+ if (romaji)
220
+ push(stripped(romaji));
221
+ const words = stripped(title).split(/\s+/).filter(Boolean);
222
+ if (words.length > 3)
223
+ push(words.slice(0, 3).join(' '));
224
+ if (words.length > 4)
225
+ push(words.slice(0, 4).join(' '));
226
+ const tokens = distinctiveTokens(title)
227
+ .concat(romaji ? distinctiveTokens(romaji) : [])
228
+ .filter((w) => w.length >= 4);
229
+ const unique = [...new Set(tokens)];
230
+ unique.sort((a, b) => b.length - a.length);
231
+ for (const t of unique.slice(0, 4))
232
+ push(t);
233
+ return out;
234
+ }