@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,534 @@
1
+ #!/usr/bin/env node
2
+ const { spawn, execSync } = require('child_process')
3
+ const readline = require('readline')
4
+ const http = require('http')
5
+ const os = require('os')
6
+ const path = require('path')
7
+ const crypto = require('crypto')
8
+
9
+ async function fetchJson(url, timeoutMs) {
10
+ const controller = new AbortController()
11
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
12
+ try {
13
+ const res = await fetch(url, {
14
+ signal: controller.signal,
15
+ headers: { 'User-Agent': 'dango-cli' },
16
+ })
17
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
18
+ return await res.json()
19
+ } finally {
20
+ clearTimeout(timer)
21
+ }
22
+ }
23
+
24
+ function detectGlobalInstall() {
25
+ const normalized = __dirname.split(path.sep).join('/')
26
+ if (normalized.includes('node_modules/@serifpersia/dango')) return true
27
+ try {
28
+ const npmGlobalPrefix = execSync(`${npmCmd} config get prefix`, {
29
+ encoding: 'utf8',
30
+ timeout: 10000,
31
+ }).trim()
32
+ return path.resolve(__dirname).includes(path.resolve(npmGlobalPrefix))
33
+ } catch {
34
+ return false
35
+ }
36
+ }
37
+
38
+ const shutdownToken = crypto.randomBytes(32).toString('hex')
39
+ const mode = process.argv[2] || 'prod'
40
+ const isWin = os.platform() === 'win32'
41
+ const npmCmd = isWin ? 'npm.cmd' : 'npm'
42
+
43
+ const colors = {
44
+ reset: '\x1b[0m',
45
+ server: '\x1b[36m',
46
+ client: '\x1b[32m',
47
+ system: '\x1b[33m',
48
+ }
49
+
50
+ if (mode === '--version' || mode === '-v') {
51
+ const pkg = require('./package.json')
52
+ console.log(`dango version ${pkg.version}`)
53
+ process.exit(0)
54
+ }
55
+
56
+ if (mode === '--help' || mode === '-h') {
57
+ console.log('Usage: dango [dev|prod] [--no-update] [--version] [--help]')
58
+ console.log(' dev Run with hot-reload (server + client)')
59
+ console.log(' prod Run pre-built version (default)')
60
+ console.log(' --no-update Skip update checks')
61
+ console.log(' --version, -v Print version and exit')
62
+ console.log(' --help, -h Show this help and exit')
63
+ process.exit(0)
64
+ }
65
+
66
+ function satisfiesEngines(current, engines) {
67
+ const range = String((engines && engines.node) || '').trim()
68
+ const match = range.match(/^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
69
+ if (!match) return true
70
+ const wanted = [match[1], match[2], match[3]].map((n) => parseInt(n || '0', 10))
71
+ const currentParts = parseVersionParts(current)
72
+ for (let i = 0; i < 3; i++) {
73
+ const have = currentParts[i] || 0
74
+ if (have !== wanted[i]) return have > wanted[i]
75
+ }
76
+ return true
77
+ }
78
+
79
+ if (!satisfiesEngines(process.versions.node, require('./package.json').engines)) {
80
+ const wanted = require('./package.json').engines.node
81
+ console.error(
82
+ `${colors.system}[System]${colors.reset} Node.js ${wanted} or newer is required (found v${process.versions.node}).`
83
+ )
84
+ console.error(
85
+ `${colors.system}[System]${colors.reset} Download it from ${colors.client}https://nodejs.org/${colors.reset} then restart dango.`
86
+ )
87
+ process.exit(1)
88
+ }
89
+
90
+ function parseVersionParts(v) {
91
+ return String(v || '')
92
+ .replace(/^v/, '')
93
+ .split('.')
94
+ .map((n) => parseInt(n, 10) || 0)
95
+ }
96
+
97
+ const isNewerVersion = (remote, current) => {
98
+ const r = parseVersionParts(remote)
99
+ const c = parseVersionParts(current)
100
+ for (let i = 0; i < 3; i++) {
101
+ if ((r[i] || 0) !== (c[i] || 0)) return (r[i] || 0) > (c[i] || 0)
102
+ }
103
+ return false
104
+ }
105
+
106
+ async function checkForUpdates() {
107
+ if (process.argv.includes('--no-update') || mode === 'dev') return
108
+
109
+ try {
110
+ const isGlobalInstall = detectGlobalInstall()
111
+
112
+ const pkg = require('./package.json')
113
+ const current = pkg.version
114
+
115
+ if (isGlobalInstall) {
116
+ const data = await fetchJson('https://registry.npmjs.org/@serifpersia/dango/latest', 3000)
117
+ const latest = data.version
118
+
119
+ if (isNewerVersion(latest, current)) {
120
+ console.log(
121
+ `\n${colors.system}[Update]${colors.reset} ` +
122
+ `New version ${colors.client}${latest}${colors.reset} available (current: ${current})`
123
+ )
124
+
125
+ if (process.stdin.isTTY) {
126
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
127
+ const answer = await new Promise((resolve) => {
128
+ rl.question(
129
+ `${colors.system}[Update]${colors.reset} Would you like to perform a clean install now? (y/N) `,
130
+ (ans) => {
131
+ rl.close()
132
+ resolve(ans.toLowerCase())
133
+ }
134
+ )
135
+ })
136
+
137
+ if (answer === 'y' || answer === 'yes') {
138
+ console.log(`${colors.system}[Update]${colors.reset} Updating dango...`)
139
+ try {
140
+ terminateProcess(serverProcess)
141
+ terminateProcess(clientProcess)
142
+ execSync(`${npmCmd} install -g @serifpersia/dango@latest`, {
143
+ stdio: 'inherit',
144
+ })
145
+ console.log(
146
+ `\n${colors.system}[Update]${colors.reset} Update successful! Please restart dango to apply changes.`
147
+ )
148
+ process.exit(0)
149
+ } catch (err) {
150
+ console.error(
151
+ `\n${colors.system}[Update]${colors.reset} Update failed: ${err.message}`
152
+ )
153
+ if (!isWin) {
154
+ console.log(
155
+ `${colors.system}[Update]${colors.reset} Hint: If you encounter EACCES permission errors, configure npm global directory without root:`
156
+ )
157
+ console.log(
158
+ `${colors.system}[Update]${colors.reset} ${colors.client}npm config set prefix ~/.npm-global${colors.reset}\n`
159
+ )
160
+ }
161
+ console.log(
162
+ `${colors.system}[Update]${colors.reset} Continuing with current version...\n`
163
+ )
164
+ }
165
+ } else {
166
+ console.log(
167
+ `${colors.system}[Update]${colors.reset} Continuing with version ${current}...\n`
168
+ )
169
+ }
170
+ if (process.stdin.isTTY) process.stdin.resume()
171
+ } else {
172
+ console.log(
173
+ `${colors.system}[Update]${colors.reset} Run: npm install -g @serifpersia/dango to update.\n`
174
+ )
175
+ }
176
+ }
177
+ } else {
178
+ const data = await fetchJson(
179
+ 'https://api.github.com/repos/serifpersia/dango/releases/latest',
180
+ 3000
181
+ )
182
+ const remoteVersion = (data.name && data.name.match(/v(\d+\.\d+\.\d+)/)?.[1]) || null
183
+ if (remoteVersion) {
184
+ if (isNewerVersion(remoteVersion, current)) {
185
+ console.log(`\n${colors.system}====================================================`)
186
+ console.log(
187
+ `${colors.system}[Update Available]${colors.reset} New version ${remoteVersion} found (current: ${current})!`
188
+ )
189
+ console.log(
190
+ `Please download the latest release: ${colors.client}${data.html_url}${colors.reset}`
191
+ )
192
+ console.log(`Replace your current files with the new ones from the zip.`)
193
+ console.log(
194
+ `${colors.system}====================================================\n${colors.reset}`
195
+ )
196
+ }
197
+ return
198
+ }
199
+ if (!pkg.versionDate) return
200
+
201
+ const latestDate = new Date(data.published_at)
202
+ const pkgDate = new Date(pkg.versionDate)
203
+
204
+ if (latestDate > pkgDate) {
205
+ console.log(`\n${colors.system}====================================================`)
206
+ console.log(`${colors.system}[Update Available]${colors.reset} New version found!`)
207
+ console.log(
208
+ `Please download the latest release: ${colors.client}${data.html_url}${colors.reset}`
209
+ )
210
+ console.log(`Replace your current files with the new ones from the zip.`)
211
+ console.log(
212
+ `${colors.system}====================================================\n${colors.reset}`
213
+ )
214
+ }
215
+ }
216
+ } catch (error) {
217
+ // Silently ignore network/registry errors
218
+ }
219
+ }
220
+
221
+ const SERVER_DIR = path.join(__dirname, 'server')
222
+ const CLIENT_DIR = path.join(__dirname, 'client')
223
+
224
+ let syncSpinner = null
225
+ let syncMessage = ''
226
+ let syncDots = 0
227
+
228
+ const startSpinner = (msg) => {
229
+ stopSpinner()
230
+ syncMessage = msg
231
+ syncDots = 0
232
+ process.stdout.write(`${colors.system}[System]${colors.reset} ${msg}`)
233
+ syncSpinner = setInterval(() => {
234
+ syncDots = (syncDots + 1) % 4
235
+ process.stdout.write(
236
+ `\r${colors.system}[System]${colors.reset} ${msg}${'.'.repeat(syncDots)}${' '.repeat(3 - syncDots)}`
237
+ )
238
+ }, 400)
239
+ }
240
+
241
+ const stopSpinner = () => {
242
+ if (syncSpinner) {
243
+ clearInterval(syncSpinner)
244
+ syncSpinner = null
245
+ process.stdout.write('\n')
246
+ }
247
+ }
248
+
249
+ const levelNames = { 10: 'TRACE', 20: 'DEBUG', 30: 'INFO', 40: 'WARN', 50: 'ERROR', 60: 'FATAL' }
250
+ const levelColors = {
251
+ 10: '\x1b[90m',
252
+ 20: '\x1b[34m',
253
+ 30: '\x1b[32m',
254
+ 40: '\x1b[33m',
255
+ 50: '\x1b[31m',
256
+ 60: '\x1b[41m',
257
+ }
258
+ const messageColor = '\x1b[36m'
259
+ const propertyColor = '\x1b[35m'
260
+
261
+ const formatChildLine = (line) => {
262
+ let obj
263
+ try {
264
+ obj = JSON.parse(line)
265
+ } catch {
266
+ return { text: line }
267
+ }
268
+ if (!obj || typeof obj !== 'object' || typeof obj.msg !== 'string') {
269
+ return { text: line }
270
+ }
271
+ const time = obj.time ? new Date(obj.time).toLocaleTimeString('en-GB', { hour12: false }) : ''
272
+ const levelName = levelNames[obj.level] || 'INFO'
273
+ const levelColor = levelColors[obj.level] || colors.reset
274
+ const extras = []
275
+ for (const [key, value] of Object.entries(obj)) {
276
+ if (key === 'level' || key === 'time' || key === 'pid' || key === 'hostname' || key === 'msg') {
277
+ continue
278
+ }
279
+ extras.push(`${key}=${typeof value === 'object' ? JSON.stringify(value) : value}`)
280
+ }
281
+ const text =
282
+ `${time ? `[${time}] ` : ''}${levelColor}${levelName}${colors.reset} ${messageColor}${obj.msg}${colors.reset}` +
283
+ (extras.length ? ` (${propertyColor}${extras.join(' ')}${colors.reset})` : '')
284
+ return { text, msg: obj.msg }
285
+ }
286
+
287
+ const log = (prefix, color, data) => {
288
+ const lines = data
289
+ .toString()
290
+ .split('\n')
291
+ .filter((line) => line.trim() !== '')
292
+ if (lines.length === 0) return
293
+
294
+ const display = []
295
+ for (const line of lines) {
296
+ const { text, msg } = formatChildLine(line)
297
+ const content = msg !== undefined ? msg : text
298
+
299
+ if (content.includes('[SERVER_EXIT]')) {
300
+ isShuttingDown = true
301
+ stopSpinner()
302
+ console.log(
303
+ `${colors.system}[System]${colors.reset} Server sync complete. Shutting down cleanly.`
304
+ )
305
+ terminateProcess(serverProcess)
306
+ terminateProcess(clientProcess)
307
+ setTimeout(() => process.exit(0), 2000)
308
+ return
309
+ }
310
+
311
+ if (content.includes('[SYNC_START]')) {
312
+ startSpinner(content.split('[SYNC_START]')[1].trim())
313
+ continue
314
+ }
315
+ if (content.includes('[SYNC_END]')) {
316
+ stopSpinner()
317
+ }
318
+
319
+ const withoutTags = content.replace('[SYNC_END]', '').trim()
320
+ if (withoutTags) display.push(text)
321
+ }
322
+
323
+ if (display.length === 0) return
324
+
325
+ if (syncSpinner) {
326
+ process.stdout.write('\r\x1b[K')
327
+ for (const line of display) {
328
+ console.log(`${color}[${prefix}]${colors.reset} ${line}`)
329
+ }
330
+
331
+ process.stdout.write(
332
+ `${colors.system}[System]${colors.reset} ${syncMessage}${'.'.repeat(syncDots)}${' '.repeat(3 - syncDots)}`
333
+ )
334
+ } else {
335
+ for (const line of display) {
336
+ console.log(`${color}[${prefix}]${colors.reset} ${line}`)
337
+ }
338
+ }
339
+ }
340
+
341
+ const spawnOpts = (cwd, extraEnv, withIpc = false) => ({
342
+ // 'ipc' gives the child a structured message channel (see server/src/lib/ipc.ts).
343
+ // It only works for directly spawned node processes (prod); under a
344
+ // package-manager runner (dev) there is no IPC forwarding, so stdout tags remain the fallback signal.
345
+ stdio: withIpc ? ['pipe', 'pipe', 'pipe', 'ipc'] : 'pipe',
346
+ shell: false,
347
+ cwd,
348
+ detached: !isWin,
349
+ windowsHide: true,
350
+ env: { ...process.env, ...(extraEnv || {}) },
351
+ })
352
+
353
+ const rootPkg = require('./package.json')
354
+ const usePnpm =
355
+ typeof rootPkg.packageManager === 'string' && rootPkg.packageManager.startsWith('pnpm')
356
+ const pmShellCmd = isWin ? `${usePnpm ? 'pnpm' : 'npm'}.cmd` : usePnpm ? 'pnpm' : 'npm'
357
+
358
+ const pmRun = (script, filter) =>
359
+ usePnpm ? ['--filter', filter, 'run', script] : ['run', script, `--workspace=${filter}`]
360
+
361
+ const spawnPm = (script, filter, cwd, env) => {
362
+ const args = pmRun(script, filter)
363
+ if (isWin) {
364
+ return spawn('cmd.exe', ['/c', pmShellCmd, ...args], spawnOpts(cwd, env))
365
+ }
366
+ return spawn(pmShellCmd, args, spawnOpts(cwd, env))
367
+ }
368
+
369
+ const terminateProcess = (proc, signal = 'SIGTERM') => {
370
+ if (!proc || !proc.pid) return
371
+ if (isWin) {
372
+ try {
373
+ spawn('taskkill', ['/pid', String(proc.pid), '/f', '/t'], {
374
+ stdio: 'ignore',
375
+ windowsHide: true,
376
+ })
377
+ } catch (err) {
378
+ console.error(
379
+ `${colors.system}[System]${colors.reset} Failed to kill pid ${proc.pid}: ${err.message}`
380
+ )
381
+ }
382
+ return
383
+ }
384
+ try {
385
+ process.kill(-proc.pid, signal)
386
+ } catch (err) {
387
+ // ESRCH just means the process group is already gone — not an error.
388
+ if (err.code !== 'ESRCH') {
389
+ console.error(
390
+ `${colors.system}[System]${colors.reset} Failed to signal ${proc.pid} (${signal}): ${err.message}`
391
+ )
392
+ }
393
+ }
394
+ }
395
+ let serverProcess, clientProcess
396
+ let isShuttingDown = false
397
+
398
+ async function main() {
399
+ console.log(
400
+ `${colors.system}[System]${colors.reset} Starting dango in ${mode.toUpperCase()} mode...`
401
+ )
402
+ console.log(
403
+ `${colors.system}[System]${colors.reset} Press 'q' or 'Ctrl+C' to cleanly exit and sync data.\n`
404
+ )
405
+
406
+ if (mode === 'dev') {
407
+ serverProcess = spawnPm('dev', 'dango-server', __dirname, {
408
+ NODE_ENV: 'development',
409
+ INTERNAL_SHUTDOWN_TOKEN: shutdownToken,
410
+ })
411
+ clientProcess = spawnPm('dev', 'dango-client', __dirname, {
412
+ NODE_ENV: 'development',
413
+ })
414
+ } else {
415
+ const serverPath = path.join(SERVER_DIR, 'dist', 'server.js')
416
+ serverProcess = spawn(
417
+ 'node',
418
+ ['--max-old-space-size=512', serverPath],
419
+ spawnOpts(SERVER_DIR, {
420
+ NODE_ENV: 'production',
421
+ INTERNAL_SHUTDOWN_TOKEN: shutdownToken,
422
+ })
423
+ )
424
+ }
425
+
426
+ if (serverProcess) {
427
+ serverProcess.stdout.on('data', (data) => log('Server', colors.server, data))
428
+ serverProcess.stderr.on('data', (data) => log('Server', colors.server, data))
429
+ serverProcess.on('exit', (code) => {
430
+ if (!isShuttingDown) {
431
+ log('System', colors.system, `Server crashed or exited prematurely.`)
432
+ terminateProcess(clientProcess)
433
+ setTimeout(() => process.exit(code || 0), 500)
434
+ }
435
+ })
436
+ }
437
+
438
+ if (clientProcess) {
439
+ clientProcess.stdout.on('data', (data) => log('Client', colors.client, data))
440
+ clientProcess.stderr.on('data', (data) => log('Client', colors.client, data))
441
+ }
442
+
443
+ if (process.stdin.isTTY) {
444
+ process.stdin.resume()
445
+ readline.emitKeypressEvents(process.stdin)
446
+ process.stdin.setRawMode(true)
447
+ }
448
+
449
+ process.stdin.on('keypress', (str, key) => {
450
+ if (key && (key.name === 'q' || (key.ctrl && key.name === 'c'))) {
451
+ shutdown()
452
+ }
453
+ })
454
+ }
455
+
456
+ const shutdown = () => {
457
+ if (isShuttingDown) return
458
+ isShuttingDown = true
459
+ console.log(`\n${colors.system}[System]${colors.reset} Initiating clean shutdown...`)
460
+
461
+ if (clientProcess) {
462
+ terminateProcess(clientProcess, 'SIGTERM')
463
+ setTimeout(() => {
464
+ if (clientProcess.exitCode === null && !clientProcess.killed) {
465
+ terminateProcess(clientProcess, 'SIGKILL')
466
+ }
467
+ }, 5000)
468
+ }
469
+
470
+ const serverPort = Number(process.env.PORT) || 3000
471
+ const req = http.request(
472
+ {
473
+ hostname: '127.0.0.1',
474
+ port: serverPort,
475
+ path: '/api/internal/shutdown',
476
+ method: 'POST',
477
+ headers: {
478
+ 'x-internal-token': shutdownToken,
479
+ },
480
+ },
481
+ (res) => {
482
+ if (res.statusCode !== 200) {
483
+ console.log(
484
+ `${colors.system}[System]${colors.reset} Server rejected shutdown request (${res.statusCode}), forcing exit.`
485
+ )
486
+ terminateProcess(serverProcess, 'SIGKILL')
487
+ terminateProcess(clientProcess, 'SIGKILL')
488
+ setTimeout(() => process.exit(0), 1000)
489
+ }
490
+ }
491
+ )
492
+
493
+ req.on('error', () => {
494
+ console.log(`${colors.system}[System]${colors.reset} Server unreachable, forcing exit.`)
495
+ terminateProcess(serverProcess, 'SIGKILL')
496
+ terminateProcess(clientProcess, 'SIGKILL')
497
+ setTimeout(() => process.exit(0), 1000)
498
+ })
499
+
500
+ req.end()
501
+
502
+ setTimeout(() => {
503
+ console.log(`${colors.system}[System]${colors.reset} Force exiting after timeout.`)
504
+ terminateProcess(serverProcess, 'SIGKILL')
505
+ terminateProcess(clientProcess, 'SIGKILL')
506
+ setTimeout(() => process.exit(1), 1000)
507
+ }, 15000)
508
+ }
509
+
510
+ const restoreTerminal = () => {
511
+ try {
512
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
513
+ process.stdin.setRawMode(false)
514
+ process.stdin.pause()
515
+ }
516
+ } catch {
517
+ // ignore terminal restore errors
518
+ }
519
+ }
520
+
521
+ process.on('SIGINT', () => {
522
+ shutdown()
523
+ })
524
+ process.on('SIGTERM', () => {
525
+ shutdown()
526
+ })
527
+ process.on('SIGHUP', () => {
528
+ shutdown()
529
+ })
530
+ process.on('exit', restoreTerminal)
531
+ ;(async () => {
532
+ await checkForUpdates().catch(() => {})
533
+ await main()
534
+ })()
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@serifpersia/dango",
3
+ "version": "3.3.4",
4
+ "description": "A web application for watching anime.",
5
+ "main": "orchestrator.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/serifpersia/dango.git"
9
+ },
10
+ "bin": {
11
+ "dango": "orchestrator.js"
12
+ },
13
+ "engines": {
14
+ "node": ">=22.12.0"
15
+ },
16
+ "scripts": {
17
+ "prebuild": "node -e \"if(!String(process.env.npm_config_user_agent||'').startsWith('pnpm/')){console.error('[dango] Use pnpm, not npm. Run: pnpm run build');process.exit(1)}\"",
18
+ "prelint": "node -e \"if(!String(process.env.npm_config_user_agent||'').startsWith('pnpm/')){console.error('[dango] Use pnpm, not npm. Run: pnpm run lint');process.exit(1)}\"",
19
+ "preformat": "node -e \"if(!String(process.env.npm_config_user_agent||'').startsWith('pnpm/')){console.error('[dango] Use pnpm, not npm. Run: pnpm run format');process.exit(1)}\"",
20
+ "build": "pnpm -r run build",
21
+ "start": "node orchestrator.js prod",
22
+ "dev": "node orchestrator.js dev",
23
+ "client": "pnpm --filter dango-client run start",
24
+ "server": "pnpm --filter dango-server run start",
25
+ "preview": "node orchestrator.js prod",
26
+ "test": "pnpm --filter dango-client run test",
27
+ "lint": "pnpm -r --if-present run lint",
28
+ "format": "prettier --write .",
29
+ "prepublishOnly": "node sync-deps.js && pnpm run build",
30
+ "update-version": "node update-version.js"
31
+ },
32
+ "files": [
33
+ "orchestrator.js",
34
+ "client/dist",
35
+ "server/dist",
36
+ "server/package.json",
37
+ "LICENSE",
38
+ "README.md",
39
+ "package.json"
40
+ ],
41
+ "keywords": [
42
+ "anime",
43
+ "react",
44
+ "express",
45
+ "typescript",
46
+ "streaming",
47
+ "cli"
48
+ ],
49
+ "author": "",
50
+ "license": "MIT",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "type": "commonjs",
55
+ "packageManager": "pnpm@11.27.0",
56
+ "dependencies": {
57
+ "@octokit/auth-oauth-device": "^8.0.3",
58
+ "@octokit/rest": "^22.0.1",
59
+ "@xhayper/discord-rpc": "^1.3.4",
60
+ "cheerio": "^1.2.0",
61
+ "compression": "^1.8.1",
62
+ "cors": "^2.8.5",
63
+ "dotenv": "^17.4.2",
64
+ "express": "^5.1.0",
65
+ "fast-xml-parser": "^5.11.1",
66
+ "got-scraping": "^3.2.9",
67
+ "lru-cache": "^11.5.2",
68
+ "multer": "^2.0.2",
69
+ "pino": "^10.3.1",
70
+ "ws": "^8.18.0",
71
+ "youtubei.js": "^18.1.0"
72
+ },
73
+ "devDependencies": {
74
+ "@eslint/js": "^10.0.1",
75
+ "eslint": "^10.2.1",
76
+ "globals": "^17.5.0",
77
+ "prettier": "^3.8.3",
78
+ "typescript": "^6.0.3",
79
+ "typescript-eslint": "^8.62.0"
80
+ },
81
+ "allowScripts": {
82
+ "esbuild@0.28.2": true
83
+ },
84
+ "versionDate": "2026-09-23T19:38:08.380Z"
85
+ }