@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.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/client/dist/assets/AnimeCard-B5A20jAI.js +1 -0
- package/client/dist/assets/AnimeInfo-0huPAq0O.js +1 -0
- package/client/dist/assets/AnimeInfo-BFWtHlPC.css +1 -0
- package/client/dist/assets/AnimeInfoPage-Dqr5QH6R.js +2 -0
- package/client/dist/assets/AnimeMetaDetails-BPvv5xIl.js +1 -0
- package/client/dist/assets/AnimeMetaDetails-BwNQnRCd.css +1 -0
- package/client/dist/assets/Asmr-B6ZiqkmU.js +2 -0
- package/client/dist/assets/Asmr-Cq2ua6Zk.css +1 -0
- package/client/dist/assets/Asmr.module-ChUXDZKx.js +1 -0
- package/client/dist/assets/AsmrResetProgressModal-Cqsoo3XC.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-C1_iBSyL.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-C8ZoIwMY.js +10 -0
- package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
- package/client/dist/assets/ErrorMessage-z4f4WGgP.js +1 -0
- package/client/dist/assets/Home-DtRtTuI9.css +1 -0
- package/client/dist/assets/Home-thJhPfs1.js +1 -0
- package/client/dist/assets/Insights-DKtoVe6L.css +1 -0
- package/client/dist/assets/Insights-ZlgPrzGD.js +1 -0
- package/client/dist/assets/LibraryListPage-CCiee-Ot.js +1 -0
- package/client/dist/assets/ListeningList-ctAz_6Zi.js +1 -0
- package/client/dist/assets/Manga-BI9WMTx5.js +1 -0
- package/client/dist/assets/Manga-Dp_-l1Pw.css +1 -0
- package/client/dist/assets/Manga.module-BcfOdsNP.js +1 -0
- package/client/dist/assets/MangaInfoPage-DcrEr7Sk.js +1 -0
- package/client/dist/assets/MangaReadPage-SYg63m48.js +1 -0
- package/client/dist/assets/MangaResetProgressModal-DX-1PS4h.js +1 -0
- package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
- package/client/dist/assets/Map-Dj51g5zL.js +1 -0
- package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
- package/client/dist/assets/Mature-Dqh_9SA3.js +1 -0
- package/client/dist/assets/MediaCard-Be1ST9oj.js +1 -0
- package/client/dist/assets/MediaCard-DB6Yjgky.css +1 -0
- package/client/dist/assets/Music-BGH_0voi.css +1 -0
- package/client/dist/assets/Music-zsPwT8_5.js +1 -0
- package/client/dist/assets/OptionListMenu-Drc_vybW.js +1 -0
- package/client/dist/assets/Player-B036Ic3m.css +1 -0
- package/client/dist/assets/Player-BEtAYps2.js +56 -0
- package/client/dist/assets/PlayerRedirect-D70t8VUW.js +1 -0
- package/client/dist/assets/PlayerSettings-Bj6SsmQ2.css +1 -0
- package/client/dist/assets/PlayerSettings-Cwoji-P7.js +1 -0
- package/client/dist/assets/QueueOptionsButton-B6BMq66e.js +1 -0
- package/client/dist/assets/QueueOptionsButton-CM15uQn8.css +1 -0
- package/client/dist/assets/QueueRail-BUYEjdaq.css +1 -0
- package/client/dist/assets/QueueRail-VxpUDBSm.js +1 -0
- package/client/dist/assets/Radio-B2Q_Egj7.js +1 -0
- package/client/dist/assets/Radio-CCHNFN7R.css +1 -0
- package/client/dist/assets/Radio.module-DGSZLpKM.js +1 -0
- package/client/dist/assets/ReadingList-O0QPwXu7.js +1 -0
- package/client/dist/assets/ResetProgressModal-CR_ovLAu.js +1 -0
- package/client/dist/assets/Search-C8e2nlDD.js +1 -0
- package/client/dist/assets/Search-Cs45oAwI.css +1 -0
- package/client/dist/assets/Search.module-CMEai-B-.js +1 -0
- package/client/dist/assets/Settings-AuFxQcJF.css +1 -0
- package/client/dist/assets/Settings-BgPWBl2V.js +21 -0
- package/client/dist/assets/SkeletonGrid-CtHKKMLj.js +1 -0
- package/client/dist/assets/SkeletonGrid-DhPYefXr.css +1 -0
- package/client/dist/assets/SynopsisText-BcpwT5iw.js +1 -0
- package/client/dist/assets/SynopsisText-CYv2W3yR.css +1 -0
- package/client/dist/assets/Trackers-Bfoiyuzy.js +2 -0
- package/client/dist/assets/Trackers-D5XBOCDp.css +1 -0
- package/client/dist/assets/Tv-Ca0fCdko.js +3 -0
- package/client/dist/assets/Tv-CuEl3Iol.css +1 -0
- package/client/dist/assets/TvInfo-Bu36xJyO.css +1 -0
- package/client/dist/assets/TvInfo-D_scgw_h.js +1 -0
- package/client/dist/assets/TvPopup-YTI7dEbJ.js +1 -0
- package/client/dist/assets/TvSearch-CxU33UOf.js +1 -0
- package/client/dist/assets/TvWatchlist-B4X1fWEH.js +1 -0
- package/client/dist/assets/VirtualKeyboard-6ZNTym4R.js +1 -0
- package/client/dist/assets/VirtualKeyboard-KIPc5ZGH.css +1 -0
- package/client/dist/assets/Watchlist-BiDEU0iV.css +1 -0
- package/client/dist/assets/Watchlist-P6YMZUdF.js +1 -0
- package/client/dist/assets/Watchlist.module-XGtVQr1d.js +1 -0
- package/client/dist/assets/hls-BOLXTnjL.js +42 -0
- package/client/dist/assets/hls-C4cG9wQw.js +1 -0
- package/client/dist/assets/index-BOKlZEHq.js +30354 -0
- package/client/dist/assets/index-CUm1meqr.css +1 -0
- package/client/dist/assets/index-IZgh1beL.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DeuwDSnI.js +1 -0
- package/client/dist/assets/useAsmr-CbMVRjmV.js +1 -0
- package/client/dist/assets/useAsmrPopup-D91Q3CBg.js +1 -0
- package/client/dist/assets/useIsMobile-Dm_b6Ob3.js +1 -0
- package/client/dist/assets/useMangaLibrary-BPvFb1lU.js +1 -0
- package/client/dist/assets/useMangaPopup-BOL2mQJs.js +1 -0
- package/client/dist/assets/useMatureConsent-CT7-KfzG.js +1 -0
- package/client/dist/assets/useProviders-BsRGONMA.js +1 -0
- package/client/dist/assets/useTv-D37MfTHA.js +1 -0
- package/client/dist/assets/useTvLibrary-Cwxgy_Bg.js +1 -0
- package/client/dist/assets/vendor-core-8R2fFbyN.js +1 -0
- package/client/dist/assets/vendor-icons-CDMuNDeL.js +1 -0
- package/client/dist/assets/vendor-query-BXRECSI6.js +1 -0
- package/client/dist/assets/vendor-ui-CUDCvRS_.js +1 -0
- package/client/dist/av-sync-test.mp4 +0 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +46 -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 +534 -0
- package/package.json +85 -0
- package/server/dist/app-auth.js +177 -0
- package/server/dist/config.js +175 -0
- package/server/dist/controllers/asmr-library.controller.js +288 -0
- package/server/dist/controllers/auth.controller.js +225 -0
- package/server/dist/controllers/data.controller.js +766 -0
- package/server/dist/controllers/insights.controller.js +198 -0
- package/server/dist/controllers/manga-library.controller.js +296 -0
- package/server/dist/controllers/music.controller.js +698 -0
- package/server/dist/controllers/proxy.controller.js +820 -0
- package/server/dist/controllers/settings.controller.js +609 -0
- package/server/dist/controllers/tv-library.controller.js +310 -0
- package/server/dist/controllers/watchlist.controller.js +1015 -0
- package/server/dist/db.js +145 -0
- package/server/dist/discord-gateway.js +391 -0
- package/server/dist/discord-rpc.js +427 -0
- package/server/dist/github-sync.js +512 -0
- package/server/dist/google.js +748 -0
- package/server/dist/lib/anilist.js +1549 -0
- package/server/dist/lib/anischedule.js +194 -0
- package/server/dist/lib/discord-roles-sync.service.js +130 -0
- package/server/dist/lib/ipc.js +24 -0
- package/server/dist/lib/kitsu.js +544 -0
- package/server/dist/lib/mal.js +934 -0
- package/server/dist/lib/migration.js +329 -0
- package/server/dist/lib/offline-db.js +401 -0
- package/server/dist/lib/temp-ids.js +3 -0
- package/server/dist/lib/tmdb.js +72 -0
- package/server/dist/lib/tracker/anilist-tracker.js +245 -0
- package/server/dist/lib/tracker/sync.service.js +496 -0
- package/server/dist/lib/ytmusic.js +125 -0
- package/server/dist/logger.js +16 -0
- package/server/dist/providers/manga/manga.types.js +7 -0
- package/server/dist/providers/provider.interface.js +1 -0
- package/server/dist/providers/remote-loader.js +574 -0
- package/server/dist/providers/remote-types.js +117 -0
- package/server/dist/providers/title-matching.js +225 -0
- package/server/dist/providers/tv.types.js +1 -0
- package/server/dist/rclone.js +120 -0
- package/server/dist/repositories/asmr.repository.js +123 -0
- package/server/dist/repositories/insights.repository.js +85 -0
- package/server/dist/repositories/library.repository.js +108 -0
- package/server/dist/repositories/mal-cache.repository.js +55 -0
- package/server/dist/repositories/manga.repository.js +138 -0
- package/server/dist/repositories/notifications.repository.js +41 -0
- package/server/dist/repositories/queue.repository.js +61 -0
- package/server/dist/repositories/settings.repository.js +12 -0
- package/server/dist/repositories/shows-meta.repository.js +42 -0
- package/server/dist/repositories/temp-show-ids.repository.js +51 -0
- package/server/dist/repositories/tv.repository.js +151 -0
- package/server/dist/repositories/watched-episodes.repository.js +52 -0
- package/server/dist/repositories/watchlist.repository.js +71 -0
- package/server/dist/request-logger.js +16 -0
- package/server/dist/routes/asmr-library.routes.js +23 -0
- package/server/dist/routes/asmr.routes.js +81 -0
- package/server/dist/routes/auth.routes.js +25 -0
- package/server/dist/routes/data.routes.js +47 -0
- package/server/dist/routes/discord-gateway.routes.js +51 -0
- package/server/dist/routes/insights.routes.js +34 -0
- package/server/dist/routes/lan-auth.routes.js +46 -0
- package/server/dist/routes/manga-library.routes.js +23 -0
- package/server/dist/routes/manga.routes.js +141 -0
- package/server/dist/routes/music.routes.js +20 -0
- package/server/dist/routes/providers.routes.js +17 -0
- package/server/dist/routes/proxy.routes.js +12 -0
- package/server/dist/routes/radio.routes.js +138 -0
- package/server/dist/routes/settings.routes.js +24 -0
- package/server/dist/routes/tracker.routes.js +164 -0
- package/server/dist/routes/translate.routes.js +88 -0
- package/server/dist/routes/tv-library.routes.js +23 -0
- package/server/dist/routes/tv.routes.js +629 -0
- package/server/dist/routes/watchlist.routes.js +341 -0
- package/server/dist/scripts/backfill-episode-duration.js +42 -0
- package/server/dist/server.js +529 -0
- package/server/dist/shipped-defaults.js +7 -0
- package/server/dist/sync-config.js +23 -0
- package/server/dist/sync-payload.js +91 -0
- package/server/dist/sync.js +1128 -0
- package/server/dist/utils/cache.utils.js +25 -0
- package/server/dist/utils/cookie.utils.js +13 -0
- package/server/dist/utils/db-utils.js +9 -0
- package/server/dist/utils/env.utils.js +77 -0
- package/server/dist/utils/http.utils.js +70 -0
- package/server/dist/utils/machine-id.js +40 -0
- package/server/dist/utils/megaplay.utils.js +41 -0
- package/server/dist/utils/request-context.js +2 -0
- package/server/dist/utils/security.utils.js +94 -0
- package/server/package.json +44 -0
package/orchestrator.js
ADDED
|
@@ -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
|
+
}
|