@vyla-entertainment/sdk 1.6.0 → 1.9.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.
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
- # vyla/sdk
2
- VylaSDK is a javascript base version of [Vyla](https://vyla.mintlify.app/)'s stream & scrape api, allowing developers to access content providers directly without using a REST api. This gives you the power & updates of Vyla without the headaches of api keys & server reliability.
1
+ # Vyla SDK
2
+
3
+ VylaSDK is a JavaScript/TypeScript SDK for accessing Vyla's stream providers, subtitles, and download sources directly from Node.js. Instead of using the Vyla REST API, developers can run the scraping layer directly in their own environment with full control over requests, execution, and provider management.
4
+
5
+ ### Installing
3
6
 
4
- ## Installing
5
7
  To start, install VylaSDK through npm or pnpm:
8
+
6
9
  ```bash
7
10
  # npm
8
11
  npm install @vyla-entertainment/sdk
@@ -11,23 +14,22 @@ npm install @vyla-entertainment/sdk
11
14
  pnpm install @vyla-entertainment/sdk
12
15
  ```
13
16
 
14
- View our full documentation at https://docs.vyla.cc/sdk
17
+ Please note we do not provide proxying, you have to do that on your end.
15
18
 
16
- ```
19
+ **View our full documentation at:** https://docs.vyla.cc/sdk
17
20
 
18
- # [DMCA](https://docs.vyla.cc/misc/dmca)
21
+ ### [DMCA](https://docs.vyla.cc/misc/dmca)
19
22
 
20
23
  If your source is listed here, I sincerely apologize. I understand that its inclusion may be costing you a lot of money, and that was never my intention. Please contact me as soon as possible, and I'll remove it ASAP.
21
24
 
22
25
  This is a small hobby project that I built for learning and personal interest, not for profit. I genuinely want to respect the wishes of source owners and will address any removal requests as quickly as I can.
23
26
 
24
- This project is developed and maintained by [Vyla Entertainment](https://github.com/vyla-entertainment) (@vyla-entertainment).
27
+ This project is developed and maintained by [Vyla Entertainment](https://github.com/vyla-entertainment).
25
28
 
26
29
  Original work and core authorship:
27
30
 
28
31
  [@endoverdosing](https://github.com/EndOverdosing)
29
-
30
32
  [@GavinGoGaming](https://github.com/GavinGoGaming)
31
33
 
32
- Full credits and acknowledgements:
33
- https://docs.vyla.cc/misc/credit
34
+ **Full credits and acknowledgements:**
35
+ https://docs.vyla.cc/misc/credit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vyla-entertainment/sdk",
3
- "version": "1.6.0",
3
+ "version": "1.9.0",
4
4
  "scripts": {
5
5
  "test": "node test/index.js"
6
6
  },
package/src/config.js CHANGED
@@ -1,4 +1,4 @@
1
- // Currently active sources reported on 7:47:50 PM PST 7/6/2026
1
+ // Currently active sources reported on 9:22:41 PM PST 7/30/2026
2
2
 
3
3
  // None Anime Tested: 936075
4
4
  // Anime Tested: 37854
@@ -60,6 +60,30 @@ export const SOURCES = [
60
60
  skipProxy: true
61
61
  },
62
62
 
63
+ {
64
+ key: 'anikoto-sub',
65
+ label: 'AniKoto (Sub)',
66
+ sourceFile: 'anikoto',
67
+ proxyParam: 'aksub',
68
+ timeout: 25000,
69
+ jitter: 500,
70
+ retries: 2,
71
+ multiUrl: true,
72
+ skipProxy: true
73
+ },
74
+
75
+ {
76
+ key: 'anikoto-dub',
77
+ label: 'AniKoto (Dub)',
78
+ sourceFile: 'anikoto',
79
+ proxyParam: 'akdub',
80
+ timeout: 25000,
81
+ jitter: 500,
82
+ retries: 2,
83
+ multiUrl: true,
84
+ skipProxy: true
85
+ },
86
+
63
87
  {
64
88
  key: 'anineko-sub',
65
89
  sourceFile: 'anineko',
@@ -114,6 +138,18 @@ export const SOURCES = [
114
138
  },
115
139
  },
116
140
 
141
+ {
142
+ key: 'anizone',
143
+ label: 'AniZone',
144
+ sourceFile: 'anizone',
145
+ proxyParam: 'az',
146
+ timeout: 25000,
147
+ jitter: 500,
148
+ retries: 2,
149
+ multiUrl: true,
150
+ skipProxy: true
151
+ },
152
+
117
153
  {
118
154
  key: 'cinejoy',
119
155
  label: 'Cinejoy',
@@ -126,6 +162,25 @@ export const SOURCES = [
126
162
  skipProxy: true,
127
163
  },
128
164
 
165
+ {
166
+ key: 'cinesu',
167
+ label: 'CineSu',
168
+ sourceFile: 'cinesu',
169
+ proxyParam: 'cs',
170
+ timeout: 20000,
171
+ jitter: 500,
172
+ retries: 2,
173
+ multiUrl: false,
174
+ skipProxy: true,
175
+ cdnHeaders: [{
176
+ pattern: /glendale-plumbing\.com/i,
177
+ headers: {
178
+ Referer: 'https://cine.su/',
179
+ Origin: 'https://cine.su',
180
+ },
181
+ }],
182
+ },
183
+
129
184
  {
130
185
  key: 'flaxmovies',
131
186
  label: 'FlaxMovies',
@@ -135,7 +190,7 @@ export const SOURCES = [
135
190
  jitter: 500,
136
191
  retries: 2,
137
192
  multiUrl: true,
138
- disabled: true, // Temporarily disabled because their site is down, all streams are returning 403's
193
+ skipProxy: true,
139
194
  cdnHeaders: [{
140
195
  pattern: /flix2watch\.pro/i,
141
196
  headers: {
@@ -161,18 +216,6 @@ export const SOURCES = [
161
216
  },
162
217
  },
163
218
 
164
- {
165
- key: 'goated',
166
- label: 'Goated',
167
- sourceFile: 'goated',
168
- proxyParam: 'gt',
169
- timeout: 25000,
170
- jitter: 500,
171
- retries: 2,
172
- multiUrl: true,
173
- skipProxy: true,
174
- },
175
-
176
219
  {
177
220
  key: 'fsonic',
178
221
  label: 'Fsonic',
@@ -206,6 +249,18 @@ export const SOURCES = [
206
249
  retries: 2,
207
250
  },
208
251
 
252
+ {
253
+ key: 'kickassanime',
254
+ label: 'KickAssAnime',
255
+ sourceFile: 'kickassanime',
256
+ proxyParam: 'kaa',
257
+ timeout: 25000,
258
+ jitter: 500,
259
+ retries: 2,
260
+ multiUrl: true,
261
+ skipProxy: true
262
+ },
263
+
209
264
  {
210
265
  key: 'kisskh',
211
266
  label: 'KissKH',
@@ -216,6 +271,17 @@ export const SOURCES = [
216
271
  retries: 1,
217
272
  },
218
273
 
274
+ {
275
+ key: 'lmscript',
276
+ label: 'LMScript',
277
+ sourceFile: 'lmscript',
278
+ proxyParam: 'lmsc',
279
+ timeout: 20000,
280
+ jitter: 500,
281
+ retries: 2,
282
+ multiUrl: true,
283
+ },
284
+
219
285
  {
220
286
  key: 'lookmovie',
221
287
  label: 'LookMovie',
@@ -249,39 +315,6 @@ export const SOURCES = [
249
315
  jitter: 500,
250
316
  retries: 2,
251
317
  multiUrl: true,
252
- skipProxy: true
253
- },
254
-
255
- {
256
- key: 'nebula',
257
- label: 'Nebula',
258
- sourceFile: 'nebula',
259
- proxyParam: 'nb',
260
- timeout: 25000,
261
- jitter: 500,
262
- retries: 1,
263
- multiUrl: true,
264
- skipProxy: true,
265
- },
266
-
267
- {
268
- key: 'nekowatch-sub',
269
- sourceFile: 'nekowatch',
270
- label: 'NekoWatch (Sub)',
271
- proxyParam: 'nwsub',
272
- timeout: 30000,
273
- jitter: 500,
274
- retries: 2
275
- },
276
-
277
- {
278
- key: 'nekowatch-dub',
279
- sourceFile: 'nekowatch',
280
- label: 'NekoWatch (Dub)',
281
- proxyParam: 'nwdub',
282
- timeout: 30000,
283
- jitter: 500,
284
- retries: 2
285
318
  },
286
319
 
287
320
  {
@@ -332,6 +365,31 @@ export const SOURCES = [
332
365
  timeout: 20000,
333
366
  jitter: 500,
334
367
  retries: 2,
368
+ skipProxy: true
369
+ },
370
+
371
+ {
372
+ key: 'reanime-sub',
373
+ label: 'Reanime (Sub)',
374
+ sourceFile: 'reanime',
375
+ proxyParam: 'resub',
376
+ timeout: 25000,
377
+ jitter: 500,
378
+ retries: 2,
379
+ multiUrl: true,
380
+ skipProxy: true
381
+ },
382
+
383
+ {
384
+ key: 'reanime-dub',
385
+ label: 'Reanime (Dub)',
386
+ sourceFile: 'reanime',
387
+ proxyParam: 'redub',
388
+ timeout: 25000,
389
+ jitter: 500,
390
+ retries: 2,
391
+ multiUrl: true,
392
+ skipProxy: true
335
393
  },
336
394
 
337
395
  {
@@ -485,6 +543,17 @@ export const SOURCES = [
485
543
  multiUrl: true
486
544
  },
487
545
 
546
+ {
547
+ key: 'vidvault',
548
+ label: 'VidVault',
549
+ sourceFile: 'vidvault',
550
+ proxyParam: 'vv',
551
+ timeout: 25000,
552
+ jitter: 500,
553
+ retries: 2,
554
+ multiUrl: true,
555
+ },
556
+
488
557
  {
489
558
  key: 'vidzee',
490
559
  label: 'VidZee',
@@ -494,7 +563,6 @@ export const SOURCES = [
494
563
  sourcesTimeout: 10000,
495
564
  jitter: 400,
496
565
  retries: 3,
497
- disabled: true, // Temporarily disabled because their site is down, all streams are returning 403's
498
566
  verifyHeaders: {
499
567
  Accept: '/',
500
568
  'Accept-Language': 'en-US,en;q=0.9',
package/src/sdk.d.ts CHANGED
@@ -95,7 +95,7 @@ export default class VylaSDK {
95
95
  s?: number | null,
96
96
  e?: number | null,
97
97
  clientIP?: string | null
98
- ): Promise<{allUrls: StreamResult[]}>;
98
+ ): Promise<StreamResult | { allUrls: StreamResult[] }>;
99
99
 
100
100
  /**
101
101
  * Get subtitles for a movie or TV episode.
@@ -3,13 +3,11 @@ import { USER_AGENT } from '../utils/helpers.js';
3
3
  const BASE_URL = "https://1embed.cc";
4
4
 
5
5
  const PROVIDERS = [
6
- { id: "sdev", name: "1Embed - SDev" },
7
- { id: "xpa", name: "1Embed - XPass" },
8
- { id: "vnes", name: "1Embed - VidNest" },
9
- { id: "pro", name: "1Embed - Pro" },
10
- { id: "pur", name: "1Embed - Purstream" },
11
- { id: "czo", name: "1Embed - Cinezo" },
12
- { id: "fas", name: "1Embed - VidFast" }
6
+ { id: "BUKE", name: "1Embed - BUKE" },
7
+ { id: "YOTA", name: "1Embed - YOTA" },
8
+ { id: "NEPT", name: "1Embed - NEPT" },
9
+ { id: "AZURE", name: "1Embed - AZURE" },
10
+ { id: "YORU", name: "1Embed - YORU" }
13
11
  ];
14
12
 
15
13
  let cachedToken = null;
@@ -28,6 +26,30 @@ async function getToken() {
28
26
  } catch { return null; }
29
27
  }
30
28
 
29
+ function decodeBsUrl(url) {
30
+ if (!url) return url;
31
+ let target = url;
32
+ if (url.includes('d=')) {
33
+ try {
34
+ const parts = url.split(/[?&]/);
35
+ for (const part of parts) {
36
+ if (part.startsWith('d=')) {
37
+ target = decodeURIComponent(part.substring(2));
38
+ break;
39
+ }
40
+ }
41
+ } catch { }
42
+ }
43
+ if (target.startsWith('bs_')) {
44
+ try {
45
+ const decoded = Buffer.from(target.slice(3), 'base64').toString('utf-8');
46
+ const reversed = decoded.split('').reverse().join('');
47
+ if (reversed.startsWith('http')) return reversed;
48
+ } catch { }
49
+ }
50
+ return url;
51
+ }
52
+
31
53
  async function fetchProviderStream(provider, type, id, s, e, token) {
32
54
  try {
33
55
  const referer = type === "movie" ? `${BASE_URL}/embed/movie/${id}` : `${BASE_URL}/embed/tv/${id}/${s}/${e}`;
@@ -51,11 +73,12 @@ async function fetchProviderStream(provider, type, id, s, e, token) {
51
73
  const results = [];
52
74
  for (const src of rawSources) {
53
75
  if (typeof src !== 'object' || !src) continue;
54
- let streamUrl = src.url || src.file || src.link;
55
- if (typeof streamUrl !== 'string') continue;
76
+ let rawUrl = src.url || src.file || src.link;
77
+ if (typeof rawUrl !== 'string') continue;
78
+ let streamUrl = decodeBsUrl(rawUrl);
56
79
  if (streamUrl.startsWith('/')) streamUrl = `${BASE_URL}${streamUrl}`;
57
80
  const isWorker = streamUrl.includes('omena-puu') || streamUrl.includes('nocach') || streamUrl.includes('?p=');
58
- const isHls = src.type === 'hls' || streamUrl.includes('m3u8') || isWorker;
81
+ const isHls = src.type === 'hls' || src.type === 'm3u8' || streamUrl.includes('m3u8') || isWorker;
59
82
  if (isHls && !streamUrl.includes('m3u8')) streamUrl += streamUrl.includes('?') ? '&format=.m3u8' : '?format=.m3u8';
60
83
  const srcHeaders = (src.headers && typeof src.headers === 'object') ? src.headers : {};
61
84
  results.push({
@@ -80,7 +103,7 @@ export async function getStream(args) {
80
103
  let targets = PROVIDERS;
81
104
  if (server && server !== 'all') {
82
105
  const cleanName = server.replace('1Embed - ', '').toLowerCase();
83
- targets = PROVIDERS.filter(p => p.id === cleanName || p.name.toLowerCase().includes(cleanName));
106
+ targets = PROVIDERS.filter(p => p.id.toLowerCase() === cleanName || p.name.toLowerCase().includes(cleanName));
84
107
  if (!targets.length) targets = PROVIDERS;
85
108
  }
86
109
  const settled = await Promise.allSettled(targets.map(p => fetchProviderStream(p, type, id, s, e, token)));
@@ -2,15 +2,27 @@ import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
2
2
 
3
3
  async function extractVoe(voeUrl) {
4
4
  try {
5
- const res = await fetch(voeUrl, {
5
+ let res = await fetch(voeUrl, {
6
6
  headers: {
7
7
  'User-Agent': USER_AGENT,
8
8
  'Referer': 'https://anihq.cc/'
9
9
  }
10
10
  });
11
11
 
12
+ let html = await res.text();
13
+
14
+ const redirectMatch = html.match(/window\.location\.href\s*=\s*['"](https?:\/\/[^'"]+)['"]/);
15
+ if (redirectMatch) {
16
+ res = await fetch(redirectMatch[1], {
17
+ headers: {
18
+ 'User-Agent': USER_AGENT,
19
+ 'Referer': 'https://anihq.cc/'
20
+ }
21
+ });
22
+ html = await res.text();
23
+ }
24
+
12
25
  const mirrorUrl = res.url;
13
- const html = await res.text();
14
26
 
15
27
  const scriptMatch = html.match(/<script type="application\/json">\["(.*?)"\]<\/script>/);
16
28
  if (!scriptMatch) return null;
@@ -0,0 +1,198 @@
1
+ import { fetchJson, fetchText, getTmdbInfo, tmdbToAnilist, USER_AGENT } from '../utils/helpers.js';
2
+
3
+ const ANIKOTO = "https://anikototv.to";
4
+ const MEGAPLAY = "https://megaplay.buzz";
5
+ const VIDWISH = "https://vidwish.live";
6
+ const SPOOF_REF = "https://hianimes.re/";
7
+ const HEADERS = { "User-Agent": USER_AGENT, Accept: "text/html,*/*" };
8
+
9
+ function extractEpisodes(html) {
10
+ const episodes = [];
11
+ const re = /<a\s[^>]*data-id="[^"]*"[^>]*>/g;
12
+ let m;
13
+ while ((m = re.exec(html)) !== null) {
14
+ const tag = m[0];
15
+ const getAttr = (a) => {
16
+ const x = tag.match(new RegExp(`data-${a}="([^"]*)"`));
17
+ return x ? x[1] : "";
18
+ };
19
+ const id = getAttr("id"), num = getAttr("num");
20
+ if (!id || !num) continue;
21
+ episodes.push({ id, num: parseInt(num), slug: getAttr("slug"), hasSub: getAttr("sub") === "1", hasDub: getAttr("dub") === "1", ids: getAttr("ids") });
22
+ }
23
+ return episodes;
24
+ }
25
+
26
+ function extractSearchCandidates(html) {
27
+ const results = [];
28
+ const re = /<a class="item" href="https:\/\/anikototv\.to\/watch\/([^"]+)"([\s\S]*?)<\/a>/g;
29
+ let m;
30
+ while ((m = re.exec(html)) !== null) {
31
+ const block = m[2];
32
+ const enM = block.match(/class="name d-title"[^>]*>([^<]*)</);
33
+ const jpM = block.match(/data-jp="([^"]*)"/);
34
+ results.push({ slug: m[1], titleEn: enM ? enM[1].trim() : "", titleJp: jpM ? jpM[1].trim() : "" });
35
+ }
36
+ return results;
37
+ }
38
+
39
+ async function searchAnikoto(keyword) {
40
+ const data = await fetchJson(`${ANIKOTO}/ajax/anime/search?keyword=${encodeURIComponent(keyword)}`, {
41
+ headers: { ...HEADERS, "X-Requested-With": "XMLHttpRequest", Referer: `${ANIKOTO}/` }
42
+ }).catch(() => null);
43
+
44
+ const results = extractSearchCandidates(data?.result?.html ?? "");
45
+ const html = await fetchText(`${ANIKOTO}/filter?keyword=${encodeURIComponent(keyword)}`, { headers: { ...HEADERS, Referer: `${ANIKOTO}/` } }).catch(() => "");
46
+
47
+ for (const m of html.matchAll(/<a class="name d-title" href="https:\/\/anikototv\.to\/watch\/([^"/]+)(?:\/ep-\d+)?" data-jp="([^"]*)">([\s\S]*?)<\/a>/g)) {
48
+ results.push({
49
+ slug: m[1],
50
+ titleEn: m[3].replace(/<[^>]*>/g, "").trim(),
51
+ titleJp: m[2].trim()
52
+ });
53
+ }
54
+
55
+ const seen = new Set();
56
+ return results.filter((r) => {
57
+ if (seen.has(r.slug)) return false;
58
+ seen.add(r.slug);
59
+ return true;
60
+ });
61
+ }
62
+
63
+ async function findAnikotoShow(title) {
64
+ const candidates = await searchAnikoto(title);
65
+ if (!candidates.length) return null;
66
+ const chosenSlug = candidates[0].slug;
67
+
68
+ const pageHtml = await fetchText(`${ANIKOTO}/watch/${chosenSlug}`, { headers: { ...HEADERS, Referer: `${ANIKOTO}/` } });
69
+ const idM = pageHtml.match(/data-id="(\d+)"/);
70
+ if (!idM) return null;
71
+ return { slug: chosenSlug, showId: idM[1] };
72
+ }
73
+
74
+ async function extractVidWish(realId, audio) {
75
+ try {
76
+ const page = await fetchText(`${VIDWISH}/stream/s-2/${realId}/${audio}`, { headers: { ...HEADERS, Referer: SPOOF_REF } });
77
+ const m = page.match(/data-id="([^"]*)"/);
78
+ if (!m?.[1]) return null;
79
+ const fileId = m[1];
80
+ const data = await fetchJson(`${VIDWISH}/stream/getSources?id=${fileId}&id=${fileId}`, { headers: { ...HEADERS, Referer: `${VIDWISH}/`, "X-Requested-With": "XMLHttpRequest" } });
81
+ return { fileId, data };
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ export async function getStream({ id, s, e, audio, tmdbApiKey }) {
88
+ try {
89
+ const isTv = !!s;
90
+ const mediaType = isTv ? 'tv' : 'movie';
91
+ const epNum = e ? Number(e) : 1;
92
+ const seasonNum = s ? Number(s) : 1;
93
+
94
+ let anilistId = null;
95
+ let titles = [];
96
+ const tmdbInfo = await getTmdbInfo(tmdbApiKey, id, mediaType, seasonNum);
97
+
98
+ if (tmdbInfo) {
99
+ titles = tmdbInfo.titles || [];
100
+ anilistId = await tmdbToAnilist(id, mediaType, seasonNum, titles, tmdbInfo.year);
101
+ }
102
+ if (!anilistId) {
103
+ anilistId = Number(id);
104
+ }
105
+
106
+ const audioPref = audio === 'dub' ? 'dub' : 'sub';
107
+ let embedUrl = `${MEGAPLAY}/stream/ani/${anilistId}/${epNum}/${audioPref}`;
108
+ let megaHtml = await fetchText(embedUrl, { headers: { ...HEADERS, Referer: SPOOF_REF } }).catch(() => '');
109
+
110
+ const frameSrc = megaHtml.match(/<iframe\b[^>]*src="([^"]+)"/i)?.[1];
111
+ if (!megaHtml.match(/data-id="([^"]*)"/) && frameSrc) {
112
+ embedUrl = frameSrc.startsWith("http") ? frameSrc : `${MEGAPLAY}${frameSrc}`;
113
+ megaHtml = await fetchText(embedUrl, { headers: { ...HEADERS, Referer: SPOOF_REF } }).catch(() => '');
114
+ }
115
+
116
+ const attrMatch = (name) => {
117
+ const m = megaHtml.match(new RegExp(`data-${name}="([^"]*)"`));
118
+ return m ? m[1] : null;
119
+ };
120
+
121
+ const fileId = attrMatch("id");
122
+ const realId = attrMatch("realid");
123
+
124
+ const urls = [];
125
+
126
+ if (fileId) {
127
+ const megaSources = await fetchJson(`${MEGAPLAY}/stream/getSources?id=${fileId}&id=${fileId}`, {
128
+ headers: { ...HEADERS, Referer: `${MEGAPLAY}/`, "X-Requested-With": "XMLHttpRequest" }
129
+ }).catch(() => null);
130
+
131
+ if (megaSources?.sources?.file) {
132
+ urls.push({
133
+ url: megaSources.sources.file,
134
+ server: 'AniKoto (Megaplay)',
135
+ });
136
+ }
137
+ }
138
+
139
+ if (realId) {
140
+ const vidwish = await extractVidWish(realId, audioPref);
141
+ if (vidwish?.data?.sources?.file) {
142
+ urls.push({
143
+ url: vidwish.data.sources.file,
144
+ server: 'AniKoto (VidWish)',
145
+ });
146
+ }
147
+ }
148
+
149
+ if (!urls.length && titles.length) {
150
+ const show = await findAnikotoShow(titles[0]);
151
+ if (show) {
152
+ const listData = await fetchJson(`${ANIKOTO}/ajax/episode/list/${show.showId}`, {
153
+ headers: { ...HEADERS, "X-Requested-With": "XMLHttpRequest", Referer: `${ANIKOTO}/watch/${show.slug}` }
154
+ }).catch(() => null);
155
+
156
+ const episodes = extractEpisodes(listData?.result ?? "");
157
+ const ep = episodes.find((item) => item.num === epNum);
158
+ if (ep && ep.ids) {
159
+ const serverData = await fetchJson(`${ANIKOTO}/ajax/server/list?servers=${encodeURIComponent(ep.ids)}`, {
160
+ headers: { ...HEADERS, "X-Requested-With": "XMLHttpRequest", Referer: `${ANIKOTO}/` }
161
+ }).catch(() => null);
162
+
163
+ const items = [];
164
+ const typeRe = /<div class="type" data-type="(sub|dub)">([\s\S]*?)<\/ul>\s*<\/div>/g;
165
+ let typeM;
166
+ while ((typeM = typeRe.exec(serverData?.result ?? "")) !== null) {
167
+ if (typeM[1] !== audioPref) continue;
168
+ for (const li of typeM[2].matchAll(/<li\s+([^>]*data-link-id[^>]*)>([\s\S]*?)<\/li>/g)) {
169
+ const linkId = li[1].match(/data-link-id="([^"]+)"/)?.[1];
170
+ const name = li[2].replace(/<[^>]+>/g, "").trim();
171
+ if (linkId) items.push({ linkId, name });
172
+ }
173
+ }
174
+
175
+ for (const item of items) {
176
+ const resolved = await fetchJson(`${ANIKOTO}/ajax/server?get=${encodeURIComponent(item.linkId)}`, {
177
+ headers: { ...HEADERS, "X-Requested-With": "XMLHttpRequest", Referer: `${ANIKOTO}/` }
178
+ }).catch(() => null);
179
+ if (resolved?.result?.url) {
180
+ urls.push({
181
+ url: resolved.result.url,
182
+ server: `AniKoto (${item.name})`,
183
+ });
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ return urls.length ? { allUrls: urls } : null;
191
+ } catch (err) {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ export async function getSources() {
197
+ return ['AniKoto'];
198
+ }