@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.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/client/dist/assets/AnimeInfo-CMxs0nRH.css +1 -0
- package/client/dist/assets/AnimeInfo-DVdaocCl.js +1 -0
- package/client/dist/assets/AnimeInfoPage-Bx5EvGt-.js +2 -0
- package/client/dist/assets/Asmr-C2i9GKD4.css +1 -0
- package/client/dist/assets/Asmr-CIjmFtRH.js +2 -0
- package/client/dist/assets/Asmr.module-DPG2JhDJ.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-CIYcyon5.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-vqErH7Cm.js +10 -0
- package/client/dist/assets/ErrorMessage-BwSjzblw.js +1 -0
- package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
- package/client/dist/assets/Home-D7YfGBUk.js +1 -0
- package/client/dist/assets/Home-DKCfEiwl.css +1 -0
- package/client/dist/assets/Insights-BljI64Ua.css +1 -0
- package/client/dist/assets/Insights-CLQGrlpX.js +1 -0
- package/client/dist/assets/Manga-DQ9HYnpk.js +1 -0
- package/client/dist/assets/Manga-H8Wb04w3.css +1 -0
- package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
- package/client/dist/assets/Map-L_ryVBTq.js +1 -0
- package/client/dist/assets/Mature-BeWzAJ7B.js +1 -0
- package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
- package/client/dist/assets/Player-CYpd__FP.js +30 -0
- package/client/dist/assets/Player-DreAhybc.css +1 -0
- package/client/dist/assets/PlayerRedirect-fxUAp9JV.js +1 -0
- package/client/dist/assets/PlayerSettings-BcsotxP9.js +1 -0
- package/client/dist/assets/PlayerSettings-Cqua1CDD.css +1 -0
- package/client/dist/assets/QueueRail-B9Pgbqc3.js +1 -0
- package/client/dist/assets/QueueRail-BnaltOP8.css +1 -0
- package/client/dist/assets/Radio-BvdY5twD.js +1 -0
- package/client/dist/assets/Radio-CUuYy3DY.css +1 -0
- package/client/dist/assets/Search-BzO-aRP7.css +1 -0
- package/client/dist/assets/Search-R9YLAXS1.js +1 -0
- package/client/dist/assets/Search.module-D8ZbQAiK.js +1 -0
- package/client/dist/assets/Settings-9kYcXdTR.js +21 -0
- package/client/dist/assets/Settings-DF8JThUL.css +1 -0
- package/client/dist/assets/SkeletonGrid-D5vRvX5q.js +1 -0
- package/client/dist/assets/SkeletonGrid-DEbYtO4H.css +1 -0
- package/client/dist/assets/SynopsisText-BlFw0R-0.js +1 -0
- package/client/dist/assets/SynopsisText-CUgOjOxt.css +1 -0
- package/client/dist/assets/Trackers-DfZlnnVq.js +2 -0
- package/client/dist/assets/Trackers-Dt9lR_uP.css +1 -0
- package/client/dist/assets/Tv-CVKC2YBs.css +1 -0
- package/client/dist/assets/Tv-Db1Wx_b7.js +15 -0
- package/client/dist/assets/Watchlist-B2nkNZqM.css +1 -0
- package/client/dist/assets/Watchlist-a77CYtzu.js +1 -0
- package/client/dist/assets/hls-6Owgw2Aj.js +1 -0
- package/client/dist/assets/hls-BOLXTnjL.js +42 -0
- package/client/dist/assets/index-B6qdXFtl.css +1 -0
- package/client/dist/assets/index-B8TPVaIS.js +30354 -0
- package/client/dist/assets/index-CpjgU8ou.js +178 -0
- package/client/dist/assets/useAnimeInfoData-BOfyi9UJ.js +1 -0
- package/client/dist/assets/useIsMobile-CM15uQn8.css +1 -0
- package/client/dist/assets/useIsMobile-Cb8HcKRj.js +1 -0
- package/client/dist/assets/useMatureConsent-CZMYBVXd.js +1 -0
- package/client/dist/assets/vendor-rLNfZm7Y.js +1 -0
- package/client/dist/av-sync-test.mp4 +0 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +31 -0
- package/client/dist/logo-mochi.png +0 -0
- package/client/dist/logo-sakura.png +0 -0
- package/client/dist/logo.png +0 -0
- package/client/dist/placeholder.svg +4 -0
- package/client/dist/robots.txt +3 -0
- package/orchestrator.js +416 -0
- package/package.json +89 -0
- package/server/dist/app-auth.js +231 -0
- package/server/dist/config.js +133 -0
- package/server/dist/controllers/auth.controller.js +247 -0
- package/server/dist/controllers/data.controller.js +785 -0
- package/server/dist/controllers/insights.controller.js +197 -0
- package/server/dist/controllers/proxy.controller.js +665 -0
- package/server/dist/controllers/settings.controller.js +267 -0
- package/server/dist/controllers/watchlist.controller.js +967 -0
- package/server/dist/db.js +152 -0
- package/server/dist/discord-gateway.js +381 -0
- package/server/dist/discord-rpc.js +424 -0
- package/server/dist/github-sync.js +403 -0
- package/server/dist/google.js +549 -0
- package/server/dist/lib/anilist.js +1519 -0
- package/server/dist/lib/anischedule.js +202 -0
- package/server/dist/lib/ipc.js +32 -0
- package/server/dist/lib/kitsu.js +552 -0
- package/server/dist/lib/mal.js +886 -0
- package/server/dist/lib/migration.js +335 -0
- package/server/dist/lib/temp-ids.js +11 -0
- package/server/dist/lib/tmdb.js +71 -0
- package/server/dist/lib/tracker/anilist-tracker.js +250 -0
- package/server/dist/lib/tracker/sync.service.js +312 -0
- package/server/dist/logger.js +19 -0
- package/server/dist/providers/123anime.provider.js +240 -0
- package/server/dist/providers/anilight.provider.js +366 -0
- package/server/dist/providers/animepahe.provider.js +531 -0
- package/server/dist/providers/animeya.provider.js +746 -0
- package/server/dist/providers/base-provider.js +45 -0
- package/server/dist/providers/hn.provider.js +473 -0
- package/server/dist/providers/ht.provider.js +439 -0
- package/server/dist/providers/jasmr.provider.js +504 -0
- package/server/dist/providers/kaa.provider.js +388 -0
- package/server/dist/providers/manga/manga.types.js +10 -0
- package/server/dist/providers/manga/mangadex.provider.js +227 -0
- package/server/dist/providers/manga/mangapill.provider.js +232 -0
- package/server/dist/providers/megaplay.provider.js +583 -0
- package/server/dist/providers/op.provider.js +440 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/providers/title-matching.js +234 -0
- package/server/dist/providers/wh.provider.js +600 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +43 -0
- package/server/dist/repositories/mal-cache.repository.js +66 -0
- package/server/dist/repositories/notifications.repository.js +40 -0
- package/server/dist/repositories/queue.repository.js +64 -0
- package/server/dist/repositories/settings.repository.js +15 -0
- package/server/dist/repositories/shows-meta.repository.js +43 -0
- package/server/dist/repositories/temp-show-ids.repository.js +54 -0
- package/server/dist/repositories/watched-episodes.repository.js +54 -0
- package/server/dist/repositories/watchlist.repository.js +74 -0
- package/server/dist/routes/asmr.routes.js +72 -0
- package/server/dist/routes/auth.routes.js +28 -0
- package/server/dist/routes/data.routes.js +50 -0
- package/server/dist/routes/discord-gateway.routes.js +57 -0
- package/server/dist/routes/insights.routes.js +12 -0
- package/server/dist/routes/lan-auth.routes.js +49 -0
- package/server/dist/routes/manga.routes.js +115 -0
- package/server/dist/routes/proxy.routes.js +15 -0
- package/server/dist/routes/radio.routes.js +143 -0
- package/server/dist/routes/settings.routes.js +27 -0
- package/server/dist/routes/tracker.routes.js +143 -0
- package/server/dist/routes/translate.routes.js +90 -0
- package/server/dist/routes/tv.routes.js +843 -0
- package/server/dist/routes/watchlist.routes.js +312 -0
- package/server/dist/server.js +310 -0
- package/server/dist/shipped-defaults.js +8 -0
- package/server/dist/sync-config.js +32 -0
- package/server/dist/sync.js +506 -0
- package/server/dist/utils/cookie.utils.js +17 -0
- package/server/dist/utils/db-utils.js +15 -0
- package/server/dist/utils/env.utils.js +83 -0
- package/server/dist/utils/machine-id.js +46 -0
- package/server/dist/utils/request-context.js +5 -0
- package/server/dist/utils/security.utils.js +99 -0
|
@@ -0,0 +1,600 @@
|
|
|
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.WhProvider = exports.WH_GENRES = 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://watchhentai.net';
|
|
11
|
+
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';
|
|
12
|
+
function cleanText(text) {
|
|
13
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
14
|
+
}
|
|
15
|
+
function resolveUrl(href) {
|
|
16
|
+
if (!href)
|
|
17
|
+
return '';
|
|
18
|
+
if (href.startsWith('http'))
|
|
19
|
+
return href;
|
|
20
|
+
if (href.startsWith('//'))
|
|
21
|
+
return `https:${href}`;
|
|
22
|
+
if (href.startsWith('/'))
|
|
23
|
+
return `${BASE_URL}${href}`;
|
|
24
|
+
return `${BASE_URL}/${href}`;
|
|
25
|
+
}
|
|
26
|
+
function unwrapTimthumb(raw) {
|
|
27
|
+
if (!raw)
|
|
28
|
+
return '';
|
|
29
|
+
const m = raw.match(/[?&]src=([^&]+)/i);
|
|
30
|
+
if (!m)
|
|
31
|
+
return raw;
|
|
32
|
+
try {
|
|
33
|
+
return decodeURIComponent(m[1]);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return m[1];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function extractImgUrl(html) {
|
|
40
|
+
const dataSrcM = html.match(/\bdata-src=["']([^"']+)["']/i);
|
|
41
|
+
if (dataSrcM)
|
|
42
|
+
return unwrapTimthumb(dataSrcM[1]);
|
|
43
|
+
const srcM = html.match(/\bsrc=["']([^"']+)["']/i);
|
|
44
|
+
if (srcM && !srcM[1].startsWith('data:'))
|
|
45
|
+
return unwrapTimthumb(srcM[1]);
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
function cleanUrl(raw) {
|
|
49
|
+
try {
|
|
50
|
+
return decodeURIComponent(raw.replace(/&/g, '&'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return raw.replace(/&/g, '&');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function whDecodeMediaUrl(encoded) {
|
|
57
|
+
const normalized = encoded.replace(/-/g, '+').replace(/_/g, '/');
|
|
58
|
+
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=');
|
|
59
|
+
const bytes = Buffer.from(padded, 'base64');
|
|
60
|
+
let xored = '';
|
|
61
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
62
|
+
xored += String.fromCharCode(bytes[i] ^ ((13 + (i % 17)) & 255));
|
|
63
|
+
}
|
|
64
|
+
const reversed = xored.split('').reverse().join('');
|
|
65
|
+
return Buffer.from(reversed, 'base64').toString('utf8');
|
|
66
|
+
}
|
|
67
|
+
function extractDirectSrc(playerUrl) {
|
|
68
|
+
const clean = playerUrl.replace(/&/g, '&');
|
|
69
|
+
try {
|
|
70
|
+
const source = new URL(clean).searchParams.get('source');
|
|
71
|
+
if (source)
|
|
72
|
+
return decodeURIComponent(source);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
const m = clean.match(/[?&]source=([^&]+)/i);
|
|
76
|
+
if (m) {
|
|
77
|
+
try {
|
|
78
|
+
return decodeURIComponent(m[1]);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return m[1];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
async function fetchText(url) {
|
|
88
|
+
const res = await fetch(url, {
|
|
89
|
+
headers: {
|
|
90
|
+
'User-Agent': UA,
|
|
91
|
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
92
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
93
|
+
'Cache-Control': 'no-cache',
|
|
94
|
+
},
|
|
95
|
+
signal: AbortSignal.timeout(30000),
|
|
96
|
+
});
|
|
97
|
+
if (!res.ok)
|
|
98
|
+
throw new Error(`HTTP ${res.status}: ${url}`);
|
|
99
|
+
return res.text();
|
|
100
|
+
}
|
|
101
|
+
function extractArticles(html) {
|
|
102
|
+
const results = [];
|
|
103
|
+
const openTag = '<article';
|
|
104
|
+
const closeTag = '</article>';
|
|
105
|
+
let pos = 0;
|
|
106
|
+
while (pos < html.length) {
|
|
107
|
+
const start = html.toLowerCase().indexOf(openTag.toLowerCase(), pos);
|
|
108
|
+
if (start === -1)
|
|
109
|
+
break;
|
|
110
|
+
const end = html.toLowerCase().indexOf(closeTag.toLowerCase(), start);
|
|
111
|
+
if (end === -1)
|
|
112
|
+
break;
|
|
113
|
+
results.push(html.slice(start, end + closeTag.length));
|
|
114
|
+
pos = end + closeTag.length;
|
|
115
|
+
}
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
function browseUrl(query, page, genre) {
|
|
119
|
+
if (genre) {
|
|
120
|
+
return page > 1 ? `${BASE_URL}/genre/${genre}/page/${page}/` : `${BASE_URL}/genre/${genre}/`;
|
|
121
|
+
}
|
|
122
|
+
if (query) {
|
|
123
|
+
const base = `${BASE_URL}/?s=${encodeURIComponent(query)}`;
|
|
124
|
+
return page > 1 ? `${BASE_URL}/page/${page}/?s=${encodeURIComponent(query)}` : base;
|
|
125
|
+
}
|
|
126
|
+
return page > 1 ? `${BASE_URL}/series/page/${page}/` : `${BASE_URL}/series/`;
|
|
127
|
+
}
|
|
128
|
+
exports.WH_GENRES = [
|
|
129
|
+
'3d',
|
|
130
|
+
'ahegao',
|
|
131
|
+
'anal',
|
|
132
|
+
'blackmail',
|
|
133
|
+
'blowjob',
|
|
134
|
+
'bondage',
|
|
135
|
+
'censored',
|
|
136
|
+
'cosplay',
|
|
137
|
+
'creampie',
|
|
138
|
+
'dark-skin',
|
|
139
|
+
'deepthroat',
|
|
140
|
+
'dubbed',
|
|
141
|
+
'femdom',
|
|
142
|
+
'harem',
|
|
143
|
+
'horny-slut',
|
|
144
|
+
'incest',
|
|
145
|
+
'large-breasts',
|
|
146
|
+
'ntr',
|
|
147
|
+
'public-sex',
|
|
148
|
+
'rape',
|
|
149
|
+
'school-girls',
|
|
150
|
+
'tits-fuck',
|
|
151
|
+
'uncensored',
|
|
152
|
+
'vanilla-id-1',
|
|
153
|
+
'virgins',
|
|
154
|
+
'x-ray',
|
|
155
|
+
'yuri',
|
|
156
|
+
];
|
|
157
|
+
function parseSearchArticles(html) {
|
|
158
|
+
const results = [];
|
|
159
|
+
const articles = extractArticles(html);
|
|
160
|
+
for (const art of articles) {
|
|
161
|
+
const hrefM = art.match(/<a\s[^>]*\bhref=["']([^"']+)["']/i);
|
|
162
|
+
const href = hrefM ? hrefM[1] : '';
|
|
163
|
+
const h3M = art.match(/<h3(?:\s[^>]*)?>([^<]+)<\/h3>/i);
|
|
164
|
+
const title = h3M ? cleanText(h3M[1]) : '';
|
|
165
|
+
const poster = extractImgUrl(art);
|
|
166
|
+
const yearM = art.match(/buttonyear[^>]*>.*?(\d{4})/s);
|
|
167
|
+
const year = yearM ? yearM[1] : '';
|
|
168
|
+
if (!title) {
|
|
169
|
+
const altM = art.match(/\balt=["']([^"']+)["']/i);
|
|
170
|
+
if (altM) {
|
|
171
|
+
const altTitle = cleanText(altM[1]);
|
|
172
|
+
if (altTitle && href) {
|
|
173
|
+
results.push({ title: altTitle, url: resolveUrl(href), poster, year });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (href && title) {
|
|
178
|
+
results.push({ title, url: resolveUrl(href), poster, year });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
function extractPlayerData(html) {
|
|
184
|
+
let defaultSrc = '';
|
|
185
|
+
let thumbnail = '';
|
|
186
|
+
const jwMatch = html.match(/var\s+jw\s*=\s*(\{[\s\S]*?\})\s*(?:<\/script>|;)/);
|
|
187
|
+
if (jwMatch) {
|
|
188
|
+
const fileM = jwMatch[1].match(/"file"\s*:\s*"([^"]+)"/);
|
|
189
|
+
const imageM = jwMatch[1].match(/"image"\s*:\s*"([^"]+)"/);
|
|
190
|
+
if (fileM)
|
|
191
|
+
defaultSrc = cleanUrl(fileM[1].replace(/\\\//g, '/'));
|
|
192
|
+
if (imageM)
|
|
193
|
+
thumbnail = cleanUrl(imageM[1].replace(/\\\//g, '/'));
|
|
194
|
+
}
|
|
195
|
+
let duration = '';
|
|
196
|
+
const schemaMatch = html.match(/<script[^>]+type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/i);
|
|
197
|
+
if (schemaMatch) {
|
|
198
|
+
try {
|
|
199
|
+
const schema = JSON.parse(schemaMatch[1].trim());
|
|
200
|
+
if (!defaultSrc && schema.contentUrl)
|
|
201
|
+
defaultSrc = schema.contentUrl;
|
|
202
|
+
if (!thumbnail && schema.thumbnailUrl)
|
|
203
|
+
thumbnail = schema.thumbnailUrl;
|
|
204
|
+
if (schema.duration)
|
|
205
|
+
duration = schema.duration;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
const durM = schemaMatch[1].match(/"duration"\s*:\s*"([^"]+)"/);
|
|
209
|
+
const cuM = schemaMatch[1].match(/"contentUrl"\s*:\s*"([^"]+)"/);
|
|
210
|
+
const thM = schemaMatch[1].match(/"thumbnailUrl"\s*:\s*"([^"]+)"/);
|
|
211
|
+
if (durM)
|
|
212
|
+
duration = durM[1];
|
|
213
|
+
if (cuM && !defaultSrc)
|
|
214
|
+
defaultSrc = cuM[1];
|
|
215
|
+
if (thM && !thumbnail)
|
|
216
|
+
thumbnail = thM[1];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const sources = [];
|
|
220
|
+
const sourcesBlockM = html.match(/sources\s*:\s*\[([\s\S]*?)\]/);
|
|
221
|
+
if (sourcesBlockM) {
|
|
222
|
+
const entryRe = /\{([\s\S]*?)\}/g;
|
|
223
|
+
let em;
|
|
224
|
+
while ((em = entryRe.exec(sourcesBlockM[1])) !== null) {
|
|
225
|
+
const entry = em[1];
|
|
226
|
+
const fileM = entry.match(/["']?file["']?\s*:\s*["']([^"']+)["']/);
|
|
227
|
+
const typeM = entry.match(/["']?type["']?\s*:\s*["']([^"']+)["']/);
|
|
228
|
+
const labelM = entry.match(/["']?label["']?\s*:\s*["']([^"']+)["']/);
|
|
229
|
+
if (fileM) {
|
|
230
|
+
sources.push({
|
|
231
|
+
src: cleanUrl(fileM[1].replace(/\\\//g, '/')),
|
|
232
|
+
type: typeM ? typeM[1] : 'video/mp4',
|
|
233
|
+
label: labelM ? labelM[1] : 'default',
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (sources.length === 0 && defaultSrc) {
|
|
239
|
+
const labelGuess = defaultSrc.match(/_(\d+p)\./)?.[1] ?? 'default';
|
|
240
|
+
sources.push({ src: defaultSrc, type: 'video/mp4', label: labelGuess });
|
|
241
|
+
}
|
|
242
|
+
if (sources.length === 0) {
|
|
243
|
+
const videoUrlRe = /https?:\/\/[^"' ]+\.(mp4|m3u8)[^"' ]*/gi;
|
|
244
|
+
let vm;
|
|
245
|
+
const seen = new Set();
|
|
246
|
+
while ((vm = videoUrlRe.exec(html)) !== null) {
|
|
247
|
+
const url = cleanUrl(vm[0]);
|
|
248
|
+
if (seen.has(url))
|
|
249
|
+
continue;
|
|
250
|
+
seen.add(url);
|
|
251
|
+
const label = url.match(/_(\d{3,4}p)\./)?.[1] ?? 'default';
|
|
252
|
+
sources.push({
|
|
253
|
+
src: url,
|
|
254
|
+
type: url.includes('.m3u8') ? 'application/x-mpegURL' : 'video/mp4',
|
|
255
|
+
label,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { sources, defaultSrc, thumbnail, duration };
|
|
260
|
+
}
|
|
261
|
+
class WhProvider extends base_provider_1.BaseProvider {
|
|
262
|
+
name = 'Wh';
|
|
263
|
+
bestMatch(results, query) {
|
|
264
|
+
if (!results.length)
|
|
265
|
+
return null;
|
|
266
|
+
if (results.length === 1)
|
|
267
|
+
return { ...results[0], score: 0 };
|
|
268
|
+
const q = query.toLowerCase().trim();
|
|
269
|
+
let best = results[0];
|
|
270
|
+
let bestScore = -1;
|
|
271
|
+
for (const item of results) {
|
|
272
|
+
const title = item.title.toLowerCase();
|
|
273
|
+
let score = -1;
|
|
274
|
+
if (title === q)
|
|
275
|
+
score = 3;
|
|
276
|
+
else if (title.startsWith(q))
|
|
277
|
+
score = 2;
|
|
278
|
+
else if (title.includes(q))
|
|
279
|
+
score = 1;
|
|
280
|
+
if (score > bestScore) {
|
|
281
|
+
bestScore = score;
|
|
282
|
+
best = item;
|
|
283
|
+
if (score === 3)
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { ...best, score: bestScore };
|
|
288
|
+
}
|
|
289
|
+
async browse(options) {
|
|
290
|
+
try {
|
|
291
|
+
const query = (options.query || '').trim();
|
|
292
|
+
const page = Math.max(1, options.page || 1);
|
|
293
|
+
const genre = (options.genre || '').trim().toLowerCase();
|
|
294
|
+
const cacheKey = `wh_browse_${genre || query}_${page}`;
|
|
295
|
+
const cached = this.cache.get(cacheKey);
|
|
296
|
+
if (cached)
|
|
297
|
+
return cached;
|
|
298
|
+
const html = await fetchText(browseUrl(query, page, genre || undefined));
|
|
299
|
+
const results = parseSearchArticles(html);
|
|
300
|
+
const shows = results.map((r) => {
|
|
301
|
+
const slug = r.url.split('/').filter(Boolean).pop() || '';
|
|
302
|
+
return {
|
|
303
|
+
_id: slug,
|
|
304
|
+
id: slug,
|
|
305
|
+
name: r.title,
|
|
306
|
+
englishName: r.title,
|
|
307
|
+
thumbnail: r.poster,
|
|
308
|
+
type: 'TV',
|
|
309
|
+
year: r.year ? Number(r.year) : null,
|
|
310
|
+
isAdult: true,
|
|
311
|
+
availableEpisodesDetail: { sub: [], dub: [] },
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
const output = { shows, hasMore: results.length >= 50 };
|
|
315
|
+
this.cache.set(cacheKey, output, 300);
|
|
316
|
+
return output;
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
logger_1.default.error({ error }, '[WH] Browse failed');
|
|
320
|
+
return { shows: [], hasMore: false };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async search(options) {
|
|
324
|
+
try {
|
|
325
|
+
const query = (options.query || '').trim();
|
|
326
|
+
if (!query)
|
|
327
|
+
return [];
|
|
328
|
+
const html = await fetchText(`${BASE_URL}/?s=${encodeURIComponent(query)}`);
|
|
329
|
+
const results = parseSearchArticles(html);
|
|
330
|
+
if (results.length === 0)
|
|
331
|
+
return [];
|
|
332
|
+
const matchResult = (0, title_matching_1.pickBestMatch)(results, [query]);
|
|
333
|
+
if (!matchResult)
|
|
334
|
+
return [];
|
|
335
|
+
const matched = matchResult.item;
|
|
336
|
+
const slug = matched.url.split('/').filter(Boolean).pop() || '';
|
|
337
|
+
return [
|
|
338
|
+
{
|
|
339
|
+
_id: slug,
|
|
340
|
+
id: slug,
|
|
341
|
+
name: matched.title,
|
|
342
|
+
englishName: matched.title,
|
|
343
|
+
thumbnail: matched.poster,
|
|
344
|
+
type: 'TV',
|
|
345
|
+
year: matched.year ? Number(matched.year) : null,
|
|
346
|
+
availableEpisodesDetail: { sub: [], dub: [] },
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
logger_1.default.error({ error }, '[WH] Search failed');
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async resolveShowId(title, romaji) {
|
|
356
|
+
const query = (romaji || title).trim();
|
|
357
|
+
if (!query)
|
|
358
|
+
return null;
|
|
359
|
+
const targets = [title, romaji].filter((t) => !!t);
|
|
360
|
+
for (const variant of (0, title_matching_1.buildQueryVariants)(title, romaji)) {
|
|
361
|
+
const html = await fetchText(`${BASE_URL}/?s=${encodeURIComponent(variant)}`);
|
|
362
|
+
const results = parseSearchArticles(html);
|
|
363
|
+
if (results.length === 0)
|
|
364
|
+
continue;
|
|
365
|
+
const matchResult = (0, title_matching_1.pickBestMatch)(results, targets);
|
|
366
|
+
if (matchResult) {
|
|
367
|
+
return matchResult.item.url.split('/').filter(Boolean).pop() || null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
async getEpisodes(showId) {
|
|
373
|
+
try {
|
|
374
|
+
if (!showId)
|
|
375
|
+
return null;
|
|
376
|
+
const cacheKey = `wh_eps_${showId}`;
|
|
377
|
+
const cached = this.cache.get(cacheKey);
|
|
378
|
+
if (cached) {
|
|
379
|
+
return { episodes: cached.episodes, description: cached.description };
|
|
380
|
+
}
|
|
381
|
+
const html = await fetchText(`${BASE_URL}/series/${showId}/`);
|
|
382
|
+
const videosLinks = html.match(/\/videos\/[^"'\s]+/gi) || [];
|
|
383
|
+
const episodeMap = {};
|
|
384
|
+
const episodeNumbers = [];
|
|
385
|
+
videosLinks.forEach((link) => {
|
|
386
|
+
const slug = link.replace(/^\/videos\//, '').replace(/\/$/, '');
|
|
387
|
+
const numM = slug.match(/episode[-\s]?(\d+)/i);
|
|
388
|
+
const num = numM ? numM[1] : '';
|
|
389
|
+
if (num && slug && !episodeMap[num]) {
|
|
390
|
+
episodeMap[num] = slug;
|
|
391
|
+
episodeNumbers.push(num);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
const description = '';
|
|
395
|
+
const result = {
|
|
396
|
+
episodes: episodeNumbers,
|
|
397
|
+
description,
|
|
398
|
+
};
|
|
399
|
+
this.cache.set(cacheKey, {
|
|
400
|
+
episodes: episodeNumbers,
|
|
401
|
+
description,
|
|
402
|
+
episodeMap,
|
|
403
|
+
}, 120);
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
logger_1.default.error({ error, showId }, '[WH] getEpisodes failed');
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async getEpisodeSlug(seriesSlug, episodeNumber) {
|
|
412
|
+
try {
|
|
413
|
+
const cacheKey = `wh_epmap_${seriesSlug}`;
|
|
414
|
+
let cached = this.cache.get(cacheKey) || {};
|
|
415
|
+
const buildEpisodeMap = async () => {
|
|
416
|
+
const html = await fetchText(`${BASE_URL}/series/${seriesSlug}/`);
|
|
417
|
+
const videosLinks = html.match(/\/videos\/[^"'\s]+/gi) || [];
|
|
418
|
+
const map = {};
|
|
419
|
+
videosLinks.forEach((link) => {
|
|
420
|
+
const slug = link.replace(/^\/videos\//, '').replace(/\/$/, '');
|
|
421
|
+
const numM = slug.match(/episode[-\s]?(\d+)/i);
|
|
422
|
+
const num = numM ? numM[1] : '';
|
|
423
|
+
if (num && slug) {
|
|
424
|
+
map[num] = slug;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
this.cache.set(cacheKey, map, 3600);
|
|
428
|
+
return map;
|
|
429
|
+
};
|
|
430
|
+
if (Object.keys(cached).length === 0) {
|
|
431
|
+
cached = await buildEpisodeMap();
|
|
432
|
+
}
|
|
433
|
+
if (cached[episodeNumber])
|
|
434
|
+
return cached[episodeNumber];
|
|
435
|
+
const target = parseFloat(episodeNumber);
|
|
436
|
+
const keys = Object.keys(cached);
|
|
437
|
+
for (const key of keys) {
|
|
438
|
+
if (parseFloat(key) === target)
|
|
439
|
+
return cached[key];
|
|
440
|
+
}
|
|
441
|
+
const sorted = keys.sort((a, b) => Number(a) - Number(b));
|
|
442
|
+
const first = Number(sorted[0]);
|
|
443
|
+
if (target < first && sorted.length > 0)
|
|
444
|
+
return cached[sorted[0]];
|
|
445
|
+
cached = await buildEpisodeMap();
|
|
446
|
+
if (cached[episodeNumber])
|
|
447
|
+
return cached[episodeNumber];
|
|
448
|
+
for (const key of Object.keys(cached)) {
|
|
449
|
+
if (parseFloat(key) === target)
|
|
450
|
+
return cached[key];
|
|
451
|
+
}
|
|
452
|
+
const sorted2 = Object.keys(cached).sort((a, b) => Number(a) - Number(b));
|
|
453
|
+
const first2 = Number(sorted2[0]);
|
|
454
|
+
if (target < first2 && sorted2.length > 0)
|
|
455
|
+
return cached[sorted2[0]];
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
logger_1.default.error({ error, seriesSlug, episodeNumber }, '[WH] getEpisodeSlug failed');
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async getStreamUrls(showId, episodeNumber, _mode) {
|
|
464
|
+
try {
|
|
465
|
+
let targetEpisode = episodeNumber;
|
|
466
|
+
if (episodeNumber === '0')
|
|
467
|
+
targetEpisode = '1';
|
|
468
|
+
const episodeSlug = await this.getEpisodeSlug(showId, targetEpisode);
|
|
469
|
+
if (!episodeSlug) {
|
|
470
|
+
logger_1.default.warn({ showId, episodeNumber }, '[WH] Could not resolve episode slug');
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
const cacheKey = `wh_stream_${showId}_${targetEpisode}`;
|
|
474
|
+
const cached = this.cache.get(cacheKey);
|
|
475
|
+
if (cached)
|
|
476
|
+
return cached;
|
|
477
|
+
const watchUrl = `${BASE_URL}/videos/${episodeSlug}/`;
|
|
478
|
+
const watchHtml = await fetchText(watchUrl);
|
|
479
|
+
const plyrUrlMatch = watchHtml.match(/https:\/\/watchhentai\.net\/player\/\d+\/\d+\/(?:mp4|gdrive)\/?/);
|
|
480
|
+
const jwUrlMatch = watchHtml.match(/https:\/\/watchhentai\.net\/jwplayer\/\?[^'")\s]+/);
|
|
481
|
+
let playerHtml = watchHtml;
|
|
482
|
+
let directFallback = '';
|
|
483
|
+
let playerUrl = '';
|
|
484
|
+
if (plyrUrlMatch) {
|
|
485
|
+
playerUrl = cleanUrl(plyrUrlMatch[0]);
|
|
486
|
+
}
|
|
487
|
+
else if (jwUrlMatch) {
|
|
488
|
+
playerUrl = cleanUrl(jwUrlMatch[0]);
|
|
489
|
+
directFallback = extractDirectSrc(playerUrl);
|
|
490
|
+
}
|
|
491
|
+
if (playerUrl) {
|
|
492
|
+
try {
|
|
493
|
+
const res = await fetch(playerUrl, {
|
|
494
|
+
headers: {
|
|
495
|
+
'User-Agent': UA,
|
|
496
|
+
Referer: watchUrl,
|
|
497
|
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
498
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
499
|
+
},
|
|
500
|
+
signal: AbortSignal.timeout(30000),
|
|
501
|
+
});
|
|
502
|
+
if (res.ok)
|
|
503
|
+
playerHtml = await res.text();
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// ignore
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const links = [];
|
|
510
|
+
if (plyrUrlMatch) {
|
|
511
|
+
const jwSourcesMatch = playerHtml.match(/var\s+whJwSources\s*=\s*(\[[\s\S]*?\])\s*;/);
|
|
512
|
+
if (jwSourcesMatch) {
|
|
513
|
+
try {
|
|
514
|
+
const entries = JSON.parse(jwSourcesMatch[1]);
|
|
515
|
+
for (const entry of entries) {
|
|
516
|
+
if (!entry.file)
|
|
517
|
+
continue;
|
|
518
|
+
const url = whDecodeMediaUrl(entry.file);
|
|
519
|
+
if (!/^https?:\/\//.test(url))
|
|
520
|
+
continue;
|
|
521
|
+
links.push({
|
|
522
|
+
resolutionStr: entry.label || 'Auto',
|
|
523
|
+
link: url,
|
|
524
|
+
hls: url.includes('.m3u8'),
|
|
525
|
+
headers: { Referer: BASE_URL + '/' },
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
// ignore
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (links.length === 0) {
|
|
534
|
+
const seen = new Set();
|
|
535
|
+
const urlRe = /https?:\/\/[^"' ]+\.(?:mp4|m3u8)[^"' ]*/gi;
|
|
536
|
+
let vm;
|
|
537
|
+
while ((vm = urlRe.exec(playerHtml)) !== null) {
|
|
538
|
+
const url = cleanUrl(vm[0]);
|
|
539
|
+
if (seen.has(url))
|
|
540
|
+
continue;
|
|
541
|
+
seen.add(url);
|
|
542
|
+
links.push({
|
|
543
|
+
resolutionStr: url.match(/[_-](\d{3,4}p)\./)?.[1] ?? 'Auto',
|
|
544
|
+
link: url,
|
|
545
|
+
hls: url.includes('.m3u8'),
|
|
546
|
+
headers: { Referer: BASE_URL + '/' },
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (links.length === 0) {
|
|
552
|
+
const playerData = extractPlayerData(playerHtml);
|
|
553
|
+
for (const src of playerData.sources) {
|
|
554
|
+
links.push({
|
|
555
|
+
resolutionStr: src.label || 'Auto',
|
|
556
|
+
link: src.src,
|
|
557
|
+
hls: src.type === 'application/x-mpegURL' ||
|
|
558
|
+
src.type === 'm3u8' ||
|
|
559
|
+
src.src.includes('.m3u8'),
|
|
560
|
+
headers: { Referer: BASE_URL + '/' },
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (links.length === 0 && directFallback) {
|
|
565
|
+
links.push({
|
|
566
|
+
resolutionStr: directFallback.match(/_(\d+p)\./)?.[1] ?? 'Auto',
|
|
567
|
+
link: directFallback,
|
|
568
|
+
hls: directFallback.includes('.m3u8'),
|
|
569
|
+
headers: { Referer: BASE_URL + '/' },
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
if (links.length === 0) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
const result = [
|
|
576
|
+
{
|
|
577
|
+
sourceName: 'WH (Direct)',
|
|
578
|
+
links,
|
|
579
|
+
type: 'player',
|
|
580
|
+
actualEpisodeNumber: targetEpisode,
|
|
581
|
+
},
|
|
582
|
+
];
|
|
583
|
+
if (playerUrl) {
|
|
584
|
+
result.push({
|
|
585
|
+
sourceName: 'WH (Iframe)',
|
|
586
|
+
links: [{ resolutionStr: 'Auto', link: playerUrl, hls: false }],
|
|
587
|
+
type: 'iframe',
|
|
588
|
+
actualEpisodeNumber: targetEpisode,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
this.cache.set(cacheKey, result, 3600);
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
logger_1.default.error({ error, showId, episodeNumber }, '[WH] getStreamUrls failed');
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
exports.WhProvider = WhProvider;
|