@serifpersia/dango 3.3.4

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 (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/client/dist/assets/AnimeCard-B5A20jAI.js +1 -0
  4. package/client/dist/assets/AnimeInfo-0huPAq0O.js +1 -0
  5. package/client/dist/assets/AnimeInfo-BFWtHlPC.css +1 -0
  6. package/client/dist/assets/AnimeInfoPage-Dqr5QH6R.js +2 -0
  7. package/client/dist/assets/AnimeMetaDetails-BPvv5xIl.js +1 -0
  8. package/client/dist/assets/AnimeMetaDetails-BwNQnRCd.css +1 -0
  9. package/client/dist/assets/Asmr-B6ZiqkmU.js +2 -0
  10. package/client/dist/assets/Asmr-Cq2ua6Zk.css +1 -0
  11. package/client/dist/assets/Asmr.module-ChUXDZKx.js +1 -0
  12. package/client/dist/assets/AsmrResetProgressModal-Cqsoo3XC.js +1 -0
  13. package/client/dist/assets/AvSyncCalibrator-C1_iBSyL.css +1 -0
  14. package/client/dist/assets/AvSyncCalibrator-C8ZoIwMY.js +10 -0
  15. package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
  16. package/client/dist/assets/ErrorMessage-z4f4WGgP.js +1 -0
  17. package/client/dist/assets/Home-DtRtTuI9.css +1 -0
  18. package/client/dist/assets/Home-thJhPfs1.js +1 -0
  19. package/client/dist/assets/Insights-DKtoVe6L.css +1 -0
  20. package/client/dist/assets/Insights-ZlgPrzGD.js +1 -0
  21. package/client/dist/assets/LibraryListPage-CCiee-Ot.js +1 -0
  22. package/client/dist/assets/ListeningList-ctAz_6Zi.js +1 -0
  23. package/client/dist/assets/Manga-BI9WMTx5.js +1 -0
  24. package/client/dist/assets/Manga-Dp_-l1Pw.css +1 -0
  25. package/client/dist/assets/Manga.module-BcfOdsNP.js +1 -0
  26. package/client/dist/assets/MangaInfoPage-DcrEr7Sk.js +1 -0
  27. package/client/dist/assets/MangaReadPage-SYg63m48.js +1 -0
  28. package/client/dist/assets/MangaResetProgressModal-DX-1PS4h.js +1 -0
  29. package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
  30. package/client/dist/assets/Map-Dj51g5zL.js +1 -0
  31. package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
  32. package/client/dist/assets/Mature-Dqh_9SA3.js +1 -0
  33. package/client/dist/assets/MediaCard-Be1ST9oj.js +1 -0
  34. package/client/dist/assets/MediaCard-DB6Yjgky.css +1 -0
  35. package/client/dist/assets/Music-BGH_0voi.css +1 -0
  36. package/client/dist/assets/Music-zsPwT8_5.js +1 -0
  37. package/client/dist/assets/OptionListMenu-Drc_vybW.js +1 -0
  38. package/client/dist/assets/Player-B036Ic3m.css +1 -0
  39. package/client/dist/assets/Player-BEtAYps2.js +56 -0
  40. package/client/dist/assets/PlayerRedirect-D70t8VUW.js +1 -0
  41. package/client/dist/assets/PlayerSettings-Bj6SsmQ2.css +1 -0
  42. package/client/dist/assets/PlayerSettings-Cwoji-P7.js +1 -0
  43. package/client/dist/assets/QueueOptionsButton-B6BMq66e.js +1 -0
  44. package/client/dist/assets/QueueOptionsButton-CM15uQn8.css +1 -0
  45. package/client/dist/assets/QueueRail-BUYEjdaq.css +1 -0
  46. package/client/dist/assets/QueueRail-VxpUDBSm.js +1 -0
  47. package/client/dist/assets/Radio-B2Q_Egj7.js +1 -0
  48. package/client/dist/assets/Radio-CCHNFN7R.css +1 -0
  49. package/client/dist/assets/Radio.module-DGSZLpKM.js +1 -0
  50. package/client/dist/assets/ReadingList-O0QPwXu7.js +1 -0
  51. package/client/dist/assets/ResetProgressModal-CR_ovLAu.js +1 -0
  52. package/client/dist/assets/Search-C8e2nlDD.js +1 -0
  53. package/client/dist/assets/Search-Cs45oAwI.css +1 -0
  54. package/client/dist/assets/Search.module-CMEai-B-.js +1 -0
  55. package/client/dist/assets/Settings-AuFxQcJF.css +1 -0
  56. package/client/dist/assets/Settings-BgPWBl2V.js +21 -0
  57. package/client/dist/assets/SkeletonGrid-CtHKKMLj.js +1 -0
  58. package/client/dist/assets/SkeletonGrid-DhPYefXr.css +1 -0
  59. package/client/dist/assets/SynopsisText-BcpwT5iw.js +1 -0
  60. package/client/dist/assets/SynopsisText-CYv2W3yR.css +1 -0
  61. package/client/dist/assets/Trackers-Bfoiyuzy.js +2 -0
  62. package/client/dist/assets/Trackers-D5XBOCDp.css +1 -0
  63. package/client/dist/assets/Tv-Ca0fCdko.js +3 -0
  64. package/client/dist/assets/Tv-CuEl3Iol.css +1 -0
  65. package/client/dist/assets/TvInfo-Bu36xJyO.css +1 -0
  66. package/client/dist/assets/TvInfo-D_scgw_h.js +1 -0
  67. package/client/dist/assets/TvPopup-YTI7dEbJ.js +1 -0
  68. package/client/dist/assets/TvSearch-CxU33UOf.js +1 -0
  69. package/client/dist/assets/TvWatchlist-B4X1fWEH.js +1 -0
  70. package/client/dist/assets/VirtualKeyboard-6ZNTym4R.js +1 -0
  71. package/client/dist/assets/VirtualKeyboard-KIPc5ZGH.css +1 -0
  72. package/client/dist/assets/Watchlist-BiDEU0iV.css +1 -0
  73. package/client/dist/assets/Watchlist-P6YMZUdF.js +1 -0
  74. package/client/dist/assets/Watchlist.module-XGtVQr1d.js +1 -0
  75. package/client/dist/assets/hls-BOLXTnjL.js +42 -0
  76. package/client/dist/assets/hls-C4cG9wQw.js +1 -0
  77. package/client/dist/assets/index-BOKlZEHq.js +30354 -0
  78. package/client/dist/assets/index-CUm1meqr.css +1 -0
  79. package/client/dist/assets/index-IZgh1beL.js +178 -0
  80. package/client/dist/assets/useAnimeInfoData-DeuwDSnI.js +1 -0
  81. package/client/dist/assets/useAsmr-CbMVRjmV.js +1 -0
  82. package/client/dist/assets/useAsmrPopup-D91Q3CBg.js +1 -0
  83. package/client/dist/assets/useIsMobile-Dm_b6Ob3.js +1 -0
  84. package/client/dist/assets/useMangaLibrary-BPvFb1lU.js +1 -0
  85. package/client/dist/assets/useMangaPopup-BOL2mQJs.js +1 -0
  86. package/client/dist/assets/useMatureConsent-CT7-KfzG.js +1 -0
  87. package/client/dist/assets/useProviders-BsRGONMA.js +1 -0
  88. package/client/dist/assets/useTv-D37MfTHA.js +1 -0
  89. package/client/dist/assets/useTvLibrary-Cwxgy_Bg.js +1 -0
  90. package/client/dist/assets/vendor-core-8R2fFbyN.js +1 -0
  91. package/client/dist/assets/vendor-icons-CDMuNDeL.js +1 -0
  92. package/client/dist/assets/vendor-query-BXRECSI6.js +1 -0
  93. package/client/dist/assets/vendor-ui-CUDCvRS_.js +1 -0
  94. package/client/dist/av-sync-test.mp4 +0 -0
  95. package/client/dist/favicon.ico +0 -0
  96. package/client/dist/index.html +46 -0
  97. package/client/dist/logo-mochi.png +0 -0
  98. package/client/dist/logo-sakura.png +0 -0
  99. package/client/dist/logo.png +0 -0
  100. package/client/dist/placeholder.svg +4 -0
  101. package/client/dist/robots.txt +3 -0
  102. package/orchestrator.js +534 -0
  103. package/package.json +85 -0
  104. package/server/dist/app-auth.js +177 -0
  105. package/server/dist/config.js +175 -0
  106. package/server/dist/controllers/asmr-library.controller.js +288 -0
  107. package/server/dist/controllers/auth.controller.js +225 -0
  108. package/server/dist/controllers/data.controller.js +766 -0
  109. package/server/dist/controllers/insights.controller.js +198 -0
  110. package/server/dist/controllers/manga-library.controller.js +296 -0
  111. package/server/dist/controllers/music.controller.js +698 -0
  112. package/server/dist/controllers/proxy.controller.js +820 -0
  113. package/server/dist/controllers/settings.controller.js +609 -0
  114. package/server/dist/controllers/tv-library.controller.js +310 -0
  115. package/server/dist/controllers/watchlist.controller.js +1015 -0
  116. package/server/dist/db.js +145 -0
  117. package/server/dist/discord-gateway.js +391 -0
  118. package/server/dist/discord-rpc.js +427 -0
  119. package/server/dist/github-sync.js +512 -0
  120. package/server/dist/google.js +748 -0
  121. package/server/dist/lib/anilist.js +1549 -0
  122. package/server/dist/lib/anischedule.js +194 -0
  123. package/server/dist/lib/discord-roles-sync.service.js +130 -0
  124. package/server/dist/lib/ipc.js +24 -0
  125. package/server/dist/lib/kitsu.js +544 -0
  126. package/server/dist/lib/mal.js +934 -0
  127. package/server/dist/lib/migration.js +329 -0
  128. package/server/dist/lib/offline-db.js +401 -0
  129. package/server/dist/lib/temp-ids.js +3 -0
  130. package/server/dist/lib/tmdb.js +72 -0
  131. package/server/dist/lib/tracker/anilist-tracker.js +245 -0
  132. package/server/dist/lib/tracker/sync.service.js +496 -0
  133. package/server/dist/lib/ytmusic.js +125 -0
  134. package/server/dist/logger.js +16 -0
  135. package/server/dist/providers/manga/manga.types.js +7 -0
  136. package/server/dist/providers/provider.interface.js +1 -0
  137. package/server/dist/providers/remote-loader.js +574 -0
  138. package/server/dist/providers/remote-types.js +117 -0
  139. package/server/dist/providers/title-matching.js +225 -0
  140. package/server/dist/providers/tv.types.js +1 -0
  141. package/server/dist/rclone.js +120 -0
  142. package/server/dist/repositories/asmr.repository.js +123 -0
  143. package/server/dist/repositories/insights.repository.js +85 -0
  144. package/server/dist/repositories/library.repository.js +108 -0
  145. package/server/dist/repositories/mal-cache.repository.js +55 -0
  146. package/server/dist/repositories/manga.repository.js +138 -0
  147. package/server/dist/repositories/notifications.repository.js +41 -0
  148. package/server/dist/repositories/queue.repository.js +61 -0
  149. package/server/dist/repositories/settings.repository.js +12 -0
  150. package/server/dist/repositories/shows-meta.repository.js +42 -0
  151. package/server/dist/repositories/temp-show-ids.repository.js +51 -0
  152. package/server/dist/repositories/tv.repository.js +151 -0
  153. package/server/dist/repositories/watched-episodes.repository.js +52 -0
  154. package/server/dist/repositories/watchlist.repository.js +71 -0
  155. package/server/dist/request-logger.js +16 -0
  156. package/server/dist/routes/asmr-library.routes.js +23 -0
  157. package/server/dist/routes/asmr.routes.js +81 -0
  158. package/server/dist/routes/auth.routes.js +25 -0
  159. package/server/dist/routes/data.routes.js +47 -0
  160. package/server/dist/routes/discord-gateway.routes.js +51 -0
  161. package/server/dist/routes/insights.routes.js +34 -0
  162. package/server/dist/routes/lan-auth.routes.js +46 -0
  163. package/server/dist/routes/manga-library.routes.js +23 -0
  164. package/server/dist/routes/manga.routes.js +141 -0
  165. package/server/dist/routes/music.routes.js +20 -0
  166. package/server/dist/routes/providers.routes.js +17 -0
  167. package/server/dist/routes/proxy.routes.js +12 -0
  168. package/server/dist/routes/radio.routes.js +138 -0
  169. package/server/dist/routes/settings.routes.js +24 -0
  170. package/server/dist/routes/tracker.routes.js +164 -0
  171. package/server/dist/routes/translate.routes.js +88 -0
  172. package/server/dist/routes/tv-library.routes.js +23 -0
  173. package/server/dist/routes/tv.routes.js +629 -0
  174. package/server/dist/routes/watchlist.routes.js +341 -0
  175. package/server/dist/scripts/backfill-episode-duration.js +42 -0
  176. package/server/dist/server.js +529 -0
  177. package/server/dist/shipped-defaults.js +7 -0
  178. package/server/dist/sync-config.js +23 -0
  179. package/server/dist/sync-payload.js +91 -0
  180. package/server/dist/sync.js +1128 -0
  181. package/server/dist/utils/cache.utils.js +25 -0
  182. package/server/dist/utils/cookie.utils.js +13 -0
  183. package/server/dist/utils/db-utils.js +9 -0
  184. package/server/dist/utils/env.utils.js +77 -0
  185. package/server/dist/utils/http.utils.js +70 -0
  186. package/server/dist/utils/machine-id.js +40 -0
  187. package/server/dist/utils/megaplay.utils.js +41 -0
  188. package/server/dist/utils/request-context.js +2 -0
  189. package/server/dist/utils/security.utils.js +94 -0
  190. package/server/package.json +44 -0
@@ -0,0 +1,25 @@
1
+ import { LRUCache } from 'lru-cache';
2
+ export class AppCache {
3
+ cache;
4
+ constructor(options = {}) {
5
+ this.cache = new LRUCache({
6
+ max: options.maxKeys ?? 5000,
7
+ ttl: (options.ttlSeconds ?? 3600) * 1000,
8
+ });
9
+ }
10
+ get(key) {
11
+ return this.cache.get(key);
12
+ }
13
+ set(key, value, ttlSeconds) {
14
+ const entry = value;
15
+ if (ttlSeconds === undefined) {
16
+ this.cache.set(key, entry);
17
+ }
18
+ else {
19
+ this.cache.set(key, entry, { ttl: ttlSeconds * 1000 });
20
+ }
21
+ }
22
+ delete(key) {
23
+ this.cache.delete(key);
24
+ }
25
+ }
@@ -0,0 +1,13 @@
1
+ export function sanitizeCfClearance(raw) {
2
+ if (!raw)
3
+ return '';
4
+ let s = String(raw).trim();
5
+ s = s.replace(/^cf_clearance/i, '');
6
+ s = s.replace(/^[:=]\s*/, '');
7
+ s = s.replace(/["']/g, '').trim();
8
+ return s;
9
+ }
10
+ export function buildCfClearanceCookie(raw) {
11
+ const c = sanitizeCfClearance(raw);
12
+ return c ? `cf_clearance=${c}` : '';
13
+ }
@@ -0,0 +1,9 @@
1
+ export const dbAll = (db, sql, params = []) => {
2
+ return db.all(sql, params);
3
+ };
4
+ export const dbGet = (db, sql, params = []) => {
5
+ return db.get(sql, params);
6
+ };
7
+ export const dbRun = (db, sql, params = []) => {
8
+ db.run(sql, params);
9
+ };
@@ -0,0 +1,77 @@
1
+ import fs from 'fs';
2
+ import { CONFIG } from '../config.js';
3
+ const lockPath = `${CONFIG.ENV_PATH}.lock`;
4
+ async function acquireLock() {
5
+ let handle;
6
+ let attempts = 0;
7
+ while (!handle) {
8
+ try {
9
+ handle = await fs.promises.open(lockPath, 'wx');
10
+ }
11
+ catch {
12
+ if (++attempts >= 100) {
13
+ fs.promises.unlink(lockPath).catch(() => { });
14
+ throw new Error('Failed to acquire lock on .env file - another process may be holding it');
15
+ }
16
+ await new Promise((r) => setTimeout(r, 100));
17
+ }
18
+ }
19
+ return handle;
20
+ }
21
+ function releaseLock(handle) {
22
+ handle.close().catch(() => { });
23
+ fs.promises.unlink(lockPath).catch(() => { });
24
+ }
25
+ export async function updateEnvFile(updates) {
26
+ const lockHandle = await acquireLock();
27
+ try {
28
+ const envPath = CONFIG.ENV_PATH;
29
+ let envContent = '';
30
+ if (fs.existsSync(envPath)) {
31
+ envContent = fs.readFileSync(envPath, 'utf8');
32
+ }
33
+ const lines = envContent.split('\n');
34
+ const newLines = [...lines];
35
+ Object.entries(updates).forEach(([rawKey, rawValue]) => {
36
+ const key = rawKey.trim();
37
+ if (!/^[A-Z0-9_]+$/i.test(key)) {
38
+ throw new Error(`Invalid environment variable key: ${key}`);
39
+ }
40
+ const value = String(rawValue ?? '').replace(/[\r\n]/g, '');
41
+ let found = false;
42
+ for (let i = 0; i < newLines.length; i++) {
43
+ const line = newLines[i];
44
+ if (line && line.startsWith(`${key}=`)) {
45
+ if (value === '') {
46
+ newLines.splice(i, 1);
47
+ continue;
48
+ }
49
+ else {
50
+ newLines[i] = `${key}=${value}`;
51
+ }
52
+ found = true;
53
+ break;
54
+ }
55
+ }
56
+ if (!found && value !== '') {
57
+ newLines.push(`${key}=${value}`);
58
+ }
59
+ });
60
+ const finalContent = newLines
61
+ .join('\n')
62
+ .replace(/\n{2,}/g, '\n')
63
+ .trim() + '\n';
64
+ fs.writeFileSync(envPath, finalContent);
65
+ Object.entries(updates).forEach(([key, value]) => {
66
+ if (value === '') {
67
+ delete process.env[key];
68
+ }
69
+ else {
70
+ process.env[key] = value;
71
+ }
72
+ });
73
+ }
74
+ finally {
75
+ releaseLock(lockHandle);
76
+ }
77
+ }
@@ -0,0 +1,70 @@
1
+ import { brotliDecompressSync, gunzipSync, inflateSync } from 'node:zlib';
2
+ export class HttpError extends Error {
3
+ status;
4
+ constructor(status, message) {
5
+ super(message || `Upstream request failed with status ${status}`);
6
+ this.name = 'HttpError';
7
+ this.status = status;
8
+ }
9
+ }
10
+ export function decodeMaybeGzip(bytes) {
11
+ if (bytes.length > 2 && bytes[0] === 0x1f && bytes[1] === 0x8b)
12
+ return gunzipSync(bytes);
13
+ return bytes;
14
+ }
15
+ export async function parseJsonBody(res) {
16
+ const buf = Buffer.from(await res.arrayBuffer());
17
+ const text = decodeMaybeGzip(buf).toString('utf8');
18
+ try {
19
+ return JSON.parse(text);
20
+ }
21
+ catch (err) {
22
+ const attempts = [() => brotliDecompressSync(buf), () => inflateSync(buf)];
23
+ for (const attempt of attempts) {
24
+ try {
25
+ return JSON.parse(attempt().toString('utf8'));
26
+ }
27
+ catch {
28
+ // ignore
29
+ }
30
+ }
31
+ throw err;
32
+ }
33
+ }
34
+ export function isAbortError(err) {
35
+ if (!err || typeof err !== 'object')
36
+ return false;
37
+ const name = err.name;
38
+ return name === 'AbortError' || name === 'TimeoutError';
39
+ }
40
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
41
+ function isRetryableStatus(status) {
42
+ return status === 408 || status === 429 || status >= 500;
43
+ }
44
+ export async function fetchWithRetry(input, init = {}, options = {}) {
45
+ const { retries = 0, baseDelayMs = 100, timeoutMs = 30000, signal } = options;
46
+ let attempt = 0;
47
+ for (;;) {
48
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
49
+ const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
50
+ try {
51
+ const response = await fetch(input, { ...init, signal: combined });
52
+ if (!isRetryableStatus(response.status) || attempt >= retries)
53
+ return response;
54
+ try {
55
+ await response.body?.cancel();
56
+ }
57
+ catch {
58
+ // ignore
59
+ }
60
+ }
61
+ catch (err) {
62
+ if (signal?.aborted || attempt >= retries)
63
+ throw err;
64
+ if (!isAbortError(err) && !(err instanceof TypeError))
65
+ throw err;
66
+ }
67
+ attempt += 1;
68
+ await sleep(Math.min(baseDelayMs * 2 ** (attempt - 1), 3000));
69
+ }
70
+ }
@@ -0,0 +1,40 @@
1
+ import { execSync } from 'child_process';
2
+ import os from 'os';
3
+ import crypto from 'crypto';
4
+ import fs from 'fs';
5
+ export function getMachineId() {
6
+ let hardwareId = '';
7
+ try {
8
+ if (process.platform === 'win32') {
9
+ hardwareId = execSync('reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid')
10
+ .toString()
11
+ .split('REG_SZ')[1]
12
+ .trim();
13
+ }
14
+ else if (process.platform === 'linux') {
15
+ if (fs.existsSync('/etc/machine-id')) {
16
+ hardwareId = fs.readFileSync('/etc/machine-id', 'utf8').trim();
17
+ }
18
+ else if (fs.existsSync('/var/lib/dbus/machine-id')) {
19
+ hardwareId = fs.readFileSync('/var/lib/dbus/machine-id', 'utf8').trim();
20
+ }
21
+ }
22
+ else if (process.platform === 'darwin') {
23
+ hardwareId = execSync("ioreg -rd1 -c IOPlatformExpertDevice | grep -E 'IOPlatformUUID' | awk -F'\"' '{print $4}'")
24
+ .toString()
25
+ .trim();
26
+ }
27
+ }
28
+ catch (err) {
29
+ const interfaces = os.networkInterfaces();
30
+ const macs = Object.values(interfaces)
31
+ .flat()
32
+ .filter((iface) => iface && !iface.internal && iface.mac !== '00:00:00:00:00:00')
33
+ .map((iface) => iface.mac)
34
+ .sort();
35
+ hardwareId = macs.length > 0 ? macs.join('-') : os.hostname();
36
+ }
37
+ const username = os.userInfo().username;
38
+ const seed = `${hardwareId}:${username}:${process.platform}`;
39
+ return crypto.createHash('sha256').update(seed).digest('hex');
40
+ }
@@ -0,0 +1,41 @@
1
+ import { createHmac } from 'node:crypto';
2
+ const MEGAPLAY_CDN_TOKEN_KEY = 'MpCdnT0k3n!9f2K#xQ7vL5mR8wN1pY4s';
3
+ export const MEGAPLAY_CDN_TOKEN_TTL_SECONDS = 120;
4
+ export const MEGAPLAY_ORIGIN = 'https://megaplay.buzz';
5
+ export const MEGAPLAY_REFERER = 'https://megaplay.buzz/';
6
+ function base64UrlEncode(input) {
7
+ return Buffer.from(input)
8
+ .toString('base64')
9
+ .replace(/\+/g, '-')
10
+ .replace(/\//g, '_')
11
+ .replace(/=+$/, '');
12
+ }
13
+ export function isNexabloomMasterUrl(url) {
14
+ try {
15
+ const parsed = new URL(url);
16
+ return (parsed.hostname.toLowerCase().endsWith('nexabloom.top') &&
17
+ parsed.pathname.includes('master.m3u8'));
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ export function signNexabloomMasterUrl(masterUrl, ttlSeconds = MEGAPLAY_CDN_TOKEN_TTL_SECONDS) {
24
+ const match = masterUrl.match(/\/([a-f0-9]{32})\/([a-f0-9]{32})\//i);
25
+ if (!match)
26
+ return masterUrl;
27
+ const path = `${match[1].toLowerCase()}/${match[2].toLowerCase()}`;
28
+ const expires = Math.floor(Date.now() / 1000) + ttlSeconds;
29
+ const message = `${expires}|${path}`;
30
+ const signature = base64UrlEncode(createHmac('sha256', MEGAPLAY_CDN_TOKEN_KEY).update(message).digest());
31
+ const token = `${base64UrlEncode(message)}.${signature}`;
32
+ try {
33
+ const parsed = new URL(masterUrl);
34
+ parsed.searchParams.set('token', token);
35
+ return parsed.href;
36
+ }
37
+ catch {
38
+ const separator = masterUrl.includes('?') ? '&' : '?';
39
+ return `${masterUrl}${separator}token=${encodeURIComponent(token)}`;
40
+ }
41
+ }
@@ -0,0 +1,2 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ export const requestContext = new AsyncLocalStorage();
@@ -0,0 +1,94 @@
1
+ import { CONFIG } from '../config.js';
2
+ export function isSafeExternalUrl(rawUrl) {
3
+ if (typeof rawUrl !== 'string' || !rawUrl.trim()) {
4
+ return { safe: false, error: 'URL required' };
5
+ }
6
+ let parsed;
7
+ try {
8
+ parsed = new URL(rawUrl.trim());
9
+ }
10
+ catch {
11
+ return { safe: false, error: 'Invalid URL format' };
12
+ }
13
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
14
+ return { safe: false, error: 'Only http and https protocols are supported' };
15
+ }
16
+ const hostname = parsed.hostname.toLowerCase();
17
+ if (hostname === 'localhost' ||
18
+ hostname === '127.0.0.1' ||
19
+ hostname === '::1' ||
20
+ hostname === '0.0.0.0' ||
21
+ hostname.endsWith('.localhost') ||
22
+ hostname.endsWith('.local') ||
23
+ hostname.endsWith('.internal')) {
24
+ return { safe: false, error: 'Access to loopback/local addresses is forbidden' };
25
+ }
26
+ const ipv4Match = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
27
+ if (ipv4Match) {
28
+ const o1 = Number(ipv4Match[1]);
29
+ const o2 = Number(ipv4Match[2]);
30
+ const o3 = Number(ipv4Match[3]);
31
+ const o4 = Number(ipv4Match[4]);
32
+ if ([o1, o2, o3, o4].some((o) => o < 0 || o > 255)) {
33
+ return { safe: false, error: 'Invalid IP address format' };
34
+ }
35
+ if (o1 === 0 ||
36
+ o1 === 10 ||
37
+ o1 === 127 ||
38
+ (o1 === 172 && o2 >= 16 && o2 <= 31) ||
39
+ (o1 === 192 && o2 === 168) ||
40
+ (o1 === 169 && o2 === 254)) {
41
+ return { safe: false, error: 'Access to private or link-local IP addresses is forbidden' };
42
+ }
43
+ }
44
+ if (hostname.startsWith('[') || hostname.includes(':')) {
45
+ const cleanIpv6 = hostname.replace(/^\[|\]$/g, '').toLowerCase();
46
+ if (cleanIpv6 === '::1' ||
47
+ cleanIpv6 === '::' ||
48
+ cleanIpv6.startsWith('fe80:') ||
49
+ cleanIpv6.startsWith('fc00:') ||
50
+ cleanIpv6.startsWith('fd00:') ||
51
+ cleanIpv6.startsWith('::ffff:127.') ||
52
+ cleanIpv6.startsWith('::ffff:192.168.') ||
53
+ cleanIpv6.startsWith('::ffff:10.') ||
54
+ cleanIpv6.startsWith('::ffff:172.') ||
55
+ cleanIpv6.startsWith('::ffff:169.254.')) {
56
+ return { safe: false, error: 'Access to private or link-local IPv6 addresses is forbidden' };
57
+ }
58
+ }
59
+ return { safe: true, url: parsed };
60
+ }
61
+ export function isAllowedOrigin(origin) {
62
+ if (!origin)
63
+ return true; // Non-browser clients, same-origin, or mobile webview
64
+ try {
65
+ const parsed = new URL(origin);
66
+ const host = parsed.hostname.toLowerCase();
67
+ const isLocal = host === 'localhost' || host === '127.0.0.1' || host === '::1';
68
+ if (CONFIG.HOST === '0.0.0.0') {
69
+ return true;
70
+ }
71
+ return isLocal;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ export function crossSiteProtectionMiddleware(req, res, next) {
78
+ const method = req.method.toUpperCase();
79
+ const isMutating = ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method);
80
+ if (!isMutating)
81
+ return next();
82
+ const secFetchSite = req.headers['sec-fetch-site'];
83
+ if (secFetchSite === 'cross-site') {
84
+ const origin = req.headers.origin;
85
+ if (!isAllowedOrigin(origin)) {
86
+ return res.status(403).json({ error: 'Forbidden: cross-site request rejected' });
87
+ }
88
+ }
89
+ const origin = req.headers.origin;
90
+ if (origin && !isAllowedOrigin(origin)) {
91
+ return res.status(403).json({ error: 'Forbidden: cross-origin request rejected' });
92
+ }
93
+ next();
94
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "dango-server",
3
+ "private": true,
4
+ "version": "3.3.4",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.12.0"
8
+ },
9
+ "scripts": {
10
+ "start": "node --max-old-space-size=256 dist/server.js",
11
+ "dev": "tsx watch src/server.ts --dev",
12
+ "build": "tsc",
13
+ "format": "prettier --write .",
14
+ "lint": "eslint ."
15
+ },
16
+ "dependencies": {
17
+ "@octokit/auth-oauth-device": "^8.0.3",
18
+ "@octokit/rest": "^22.0.1",
19
+ "@xhayper/discord-rpc": "^1.3.4",
20
+ "cheerio": "^1.2.0",
21
+ "compression": "^1.8.1",
22
+ "cors": "^2.8.5",
23
+ "dotenv": "^17.4.2",
24
+ "express": "^5.1.0",
25
+ "fast-xml-parser": "^5.11.1",
26
+ "got-scraping": "^3.2.9",
27
+ "lru-cache": "^11.5.2",
28
+ "multer": "^2.0.2",
29
+ "pino": "^10.3.1",
30
+ "ws": "^8.18.0",
31
+ "youtubei.js": "^18.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/compression": "^1.8.1",
35
+ "@types/cors": "^2.8.19",
36
+ "@types/express": "^5.0.0",
37
+ "@types/express-serve-static-core": "^5.1.3",
38
+ "@types/multer": "^2.1.0",
39
+ "@types/node": "^22.5.0",
40
+ "@types/ws": "^8.5.10",
41
+ "pino-pretty": "^13.1.1",
42
+ "tsx": "^4.23.13"
43
+ }
44
+ }