ani-web 1.5.8

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.

Potentially problematic release.


This version of ani-web might be problematic. Click here for more details.

Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/client/dist/assets/AnimeInfo-C7DQp7Oo.js +1 -0
  4. package/client/dist/assets/AnimeInfo-Sb3YiXHJ.css +1 -0
  5. package/client/dist/assets/AnimeInfoPage-DJA7AJQ8.js +2 -0
  6. package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
  7. package/client/dist/assets/Button-o0l9V_NG.js +1 -0
  8. package/client/dist/assets/ErrorMessage-Ddf2zmRx.js +1 -0
  9. package/client/dist/assets/ErrorMessage-FOxXyZC9.css +1 -0
  10. package/client/dist/assets/Home-CKHJA97j.css +1 -0
  11. package/client/dist/assets/Home-Dey0azy1.js +1 -0
  12. package/client/dist/assets/Insights-BSRcCkDs.css +1 -0
  13. package/client/dist/assets/Insights-CogjPOd_.js +1 -0
  14. package/client/dist/assets/MAL-CYArH4yf.js +1 -0
  15. package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
  16. package/client/dist/assets/Player-BWFN9gud.js +9 -0
  17. package/client/dist/assets/Player-CBCYW7uG.css +1 -0
  18. package/client/dist/assets/PlayerSettings-BgStUrrP.css +1 -0
  19. package/client/dist/assets/PlayerSettings-rWZuATQf.js +1 -0
  20. package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
  21. package/client/dist/assets/RemoveConfirmationModal-CLYqyGOv.js +1 -0
  22. package/client/dist/assets/Search-DZAWgKwq.js +1 -0
  23. package/client/dist/assets/Search-lWsVQ0Ke.css +1 -0
  24. package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
  25. package/client/dist/assets/Settings-DyisJGeD.js +1 -0
  26. package/client/dist/assets/ToggleSwitch-CLnWnAuY.js +1 -0
  27. package/client/dist/assets/ToggleSwitch-DInRb7iM.css +1 -0
  28. package/client/dist/assets/Watchlist-2dVYksxq.css +1 -0
  29. package/client/dist/assets/Watchlist-CuqJISI3.js +1 -0
  30. package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
  31. package/client/dist/assets/index-BK_Zaqaw.css +1 -0
  32. package/client/dist/assets/index-CHVF4D4L.js +178 -0
  33. package/client/dist/assets/useAnimeInfoData-Cr58brCY.js +1 -0
  34. package/client/dist/assets/useIsMobile-gHo4t6g6.js +1 -0
  35. package/client/dist/assets/vendor-DdbgYKo4.js +3 -0
  36. package/client/dist/favicon.ico +0 -0
  37. package/client/dist/index.html +35 -0
  38. package/client/dist/logo.png +0 -0
  39. package/client/dist/placeholder.svg +4 -0
  40. package/client/dist/robots.txt +3 -0
  41. package/client/package.json +54 -0
  42. package/orchestrator.js +302 -0
  43. package/package.json +69 -0
  44. package/server/dist/config.js +86 -0
  45. package/server/dist/constants.json +1359 -0
  46. package/server/dist/controllers/auth.controller.js +213 -0
  47. package/server/dist/controllers/data.controller.js +126 -0
  48. package/server/dist/controllers/insights.controller.js +125 -0
  49. package/server/dist/controllers/proxy.controller.js +235 -0
  50. package/server/dist/controllers/settings.controller.js +147 -0
  51. package/server/dist/controllers/watchlist.controller.js +499 -0
  52. package/server/dist/db.js +231 -0
  53. package/server/dist/github-sync.js +279 -0
  54. package/server/dist/google.js +274 -0
  55. package/server/dist/logger.js +21 -0
  56. package/server/dist/providers/123anime.provider.js +229 -0
  57. package/server/dist/providers/allanime.provider.js +773 -0
  58. package/server/dist/providers/animepahe.provider.js +313 -0
  59. package/server/dist/providers/animeya.provider.js +799 -0
  60. package/server/dist/providers/provider.interface.js +2 -0
  61. package/server/dist/rclone.js +126 -0
  62. package/server/dist/repositories/insights.repository.js +30 -0
  63. package/server/dist/repositories/notifications.repository.js +22 -0
  64. package/server/dist/repositories/settings.repository.js +13 -0
  65. package/server/dist/repositories/shows-meta.repository.js +39 -0
  66. package/server/dist/repositories/watched-episodes.repository.js +60 -0
  67. package/server/dist/repositories/watchlist.repository.js +49 -0
  68. package/server/dist/routes/auth.routes.js +23 -0
  69. package/server/dist/routes/data.routes.js +43 -0
  70. package/server/dist/routes/insights.routes.js +11 -0
  71. package/server/dist/routes/proxy.routes.js +13 -0
  72. package/server/dist/routes/settings.routes.js +26 -0
  73. package/server/dist/routes/watchlist.routes.js +26 -0
  74. package/server/dist/server.js +179 -0
  75. package/server/dist/sync-config.js +28 -0
  76. package/server/dist/sync.js +383 -0
  77. package/server/dist/utils/db-utils.js +36 -0
  78. package/server/dist/utils/env.utils.js +70 -0
  79. package/server/package.json +54 -0
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = 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 axios = require('axios')
8
+
9
+ const mode = process.argv[2] || 'prod'
10
+ const isWin = os.platform() === 'win32'
11
+ const npmCmd = isWin ? 'npm.cmd' : 'npm'
12
+
13
+ const colors = {
14
+ reset: '\x1b[0m',
15
+ server: '\x1b[36m',
16
+ client: '\x1b[32m',
17
+ system: '\x1b[33m',
18
+ }
19
+
20
+ if (mode === '--version' || mode === '-v') {
21
+ const pkg = require('./package.json')
22
+ console.log(`ani-web version ${pkg.version}`)
23
+ process.exit(0)
24
+ }
25
+
26
+ async function checkForUpdates() {
27
+ if (process.argv.includes('--no-update') || mode === 'dev') return
28
+
29
+ try {
30
+ const npmGlobalPrefix = require('child_process')
31
+ .execSync('npm config get prefix', { encoding: 'utf8' })
32
+ .trim()
33
+ const scriptPath = path.resolve(__dirname)
34
+ const isGlobalInstall = scriptPath.includes(npmGlobalPrefix)
35
+
36
+ if (!isGlobalInstall) return
37
+
38
+ const pkg = require('./package.json')
39
+ const current = pkg.version
40
+
41
+ const { data } = await axios.get('https://registry.npmjs.org/ani-web/latest', {
42
+ timeout: 3000,
43
+ headers: { 'User-Agent': 'ani-web-cli' },
44
+ })
45
+ const latest = data.version
46
+
47
+ if (current !== latest) {
48
+ console.log(
49
+ `\n${colors.system}[Update]${colors.reset} ` +
50
+ `New version ${colors.client}${latest}${colors.reset} available (current: ${current})`
51
+ )
52
+
53
+ if (process.stdin.isTTY) {
54
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
55
+ const answer = await new Promise((resolve) => {
56
+ rl.question(
57
+ `${colors.system}[Update]${colors.reset} Would you like to perform a clean install now? (y/N) `,
58
+ (ans) => {
59
+ rl.close()
60
+ resolve(ans.toLowerCase())
61
+ }
62
+ )
63
+ })
64
+
65
+ if (answer === 'y' || answer === 'yes') {
66
+ console.log(`${colors.system}[Update]${colors.reset} Updating ani-web...`)
67
+ try {
68
+ require('child_process').execSync(`${npmCmd} install -g ani-web@latest`, {
69
+ stdio: 'inherit',
70
+ })
71
+ console.log(
72
+ `\n${colors.system}[Update]${colors.reset} Update successful! Please restart ani-web to apply changes.`
73
+ )
74
+ process.exit(0)
75
+ } catch (err) {
76
+ console.error(`\n${colors.system}[Update]${colors.reset} Update failed: ${err.message}`)
77
+ if (!isWin) {
78
+ console.log(
79
+ `${colors.system}[Update]${colors.reset} Hint: You might need to run with sudo:`
80
+ )
81
+ console.log(
82
+ `${colors.system}[Update]${colors.reset} ${colors.client}sudo ani-web${colors.reset}\n`
83
+ )
84
+ }
85
+ console.log(
86
+ `${colors.system}[Update]${colors.reset} Continuing with current version...\n`
87
+ )
88
+ }
89
+ } else {
90
+ console.log(
91
+ `${colors.system}[Update]${colors.reset} Continuing with version ${current}...\n`
92
+ )
93
+ }
94
+ if (process.stdin.isTTY) process.stdin.resume()
95
+ } else {
96
+ console.log(
97
+ `${colors.system}[Update]${colors.reset} Run: npm install -g ani-web to update.\n`
98
+ )
99
+ }
100
+ }
101
+ } catch (error) {
102
+ // Silently ignore network/registry errors
103
+ }
104
+ }
105
+
106
+ const SERVER_DIR = path.join(__dirname, 'server')
107
+ const CLIENT_DIR = path.join(__dirname, 'client')
108
+
109
+ let syncSpinner = null
110
+ let syncMessage = ''
111
+ let syncDots = 0
112
+
113
+ const startSpinner = (msg) => {
114
+ syncMessage = msg
115
+ syncDots = 0
116
+ process.stdout.write(`${colors.system}[System]${colors.reset} ${msg}`)
117
+ syncSpinner = setInterval(() => {
118
+ syncDots = (syncDots + 1) % 4
119
+ process.stdout.write(
120
+ `\r${colors.system}[System]${colors.reset} ${msg}${'.'.repeat(syncDots)}${' '.repeat(3 - syncDots)}`
121
+ )
122
+ }, 400)
123
+ }
124
+
125
+ const stopSpinner = () => {
126
+ if (syncSpinner) {
127
+ clearInterval(syncSpinner)
128
+ syncSpinner = null
129
+ process.stdout.write('\n')
130
+ }
131
+ }
132
+
133
+ const log = (prefix, color, data) => {
134
+ const str = data.toString()
135
+
136
+ if (str.includes('[SYNC_START]')) {
137
+ const parts = str.split('[SYNC_START]')
138
+ if (parts[1]) startSpinner(parts[1].split('\n')[0].trim())
139
+ return
140
+ }
141
+ if (str.includes('[SYNC_END]')) {
142
+ stopSpinner()
143
+ return
144
+ }
145
+
146
+ if (str.includes('[SERVER_EXIT]')) {
147
+ stopSpinner()
148
+ console.log(
149
+ `${colors.system}[System]${colors.reset} Server sync complete. Shutting down cleanly.`
150
+ )
151
+
152
+ if (isWin) {
153
+ if (serverProcess) spawn('taskkill', ['/pid', serverProcess.pid, '/f', '/t'], { shell: true })
154
+ if (clientProcess) spawn('taskkill', ['/pid', clientProcess.pid, '/f', '/t'], { shell: true })
155
+ } else {
156
+ if (serverProcess) {
157
+ serverProcess.kill('SIGTERM')
158
+ setTimeout(() => {
159
+ if (serverProcess.connected || !serverProcess.killed) serverProcess.kill('SIGKILL')
160
+ }, 5000)
161
+ }
162
+ if (clientProcess) {
163
+ clientProcess.kill('SIGTERM')
164
+ setTimeout(() => {
165
+ if (clientProcess.connected || !clientProcess.killed) clientProcess.kill('SIGKILL')
166
+ }, 5000)
167
+ }
168
+ }
169
+ setTimeout(() => process.exit(0), 5500)
170
+ return
171
+ }
172
+
173
+ const lines = str.split('\n').filter((line) => line.trim() !== '')
174
+ if (lines.length === 0) return
175
+
176
+ if (syncSpinner) {
177
+ process.stdout.write('\r\x1b[K')
178
+ for (const line of lines) {
179
+ console.log(`${color}[${prefix}]${colors.reset} ${line}`)
180
+ }
181
+
182
+ process.stdout.write(
183
+ `${colors.system}[System]${colors.reset} ${syncMessage}${'.'.repeat(syncDots)}${' '.repeat(3 - syncDots)}`
184
+ )
185
+ } else {
186
+ for (const line of lines) {
187
+ console.log(`${color}[${prefix}]${colors.reset} ${line}`)
188
+ }
189
+ }
190
+ }
191
+
192
+ const spawnOpts = (cwd) => ({ stdio: 'pipe', shell: isWin, cwd })
193
+ let serverProcess, clientProcess
194
+ let isShuttingDown = false
195
+
196
+ async function main() {
197
+ console.log(
198
+ `${colors.system}[System]${colors.reset} Starting ani-web in ${mode.toUpperCase()} mode...`
199
+ )
200
+ console.log(
201
+ `${colors.system}[System]${colors.reset} Press 'q' or 'Ctrl+C' to cleanly exit and sync data.\n`
202
+ )
203
+
204
+ if (mode === 'dev') {
205
+ serverProcess = spawn(npmCmd, ['run', 'dev'], spawnOpts(SERVER_DIR))
206
+ clientProcess = spawn(npmCmd, ['run', 'dev'], spawnOpts(CLIENT_DIR))
207
+ } else {
208
+ const serverPath = path.join(SERVER_DIR, 'dist', 'server.js')
209
+ serverProcess = spawn('node', ['--max-old-space-size=256', serverPath], spawnOpts(SERVER_DIR))
210
+ }
211
+
212
+ if (serverProcess) {
213
+ serverProcess.stdout.on('data', (data) => log('Server', colors.server, data))
214
+ serverProcess.stderr.on('data', (data) => log('Server', colors.server, data))
215
+ serverProcess.on('exit', (code) => {
216
+ if (!isShuttingDown) {
217
+ log('System', colors.system, `Server crashed or exited prematurely.`)
218
+ process.exit(code || 0)
219
+ }
220
+ })
221
+ }
222
+
223
+ if (clientProcess) {
224
+ clientProcess.stdout.on('data', (data) => log('Client', colors.client, data))
225
+ clientProcess.stderr.on('data', (data) => log('Client', colors.client, data))
226
+ }
227
+
228
+ if (process.stdin.isTTY) {
229
+ process.stdin.resume()
230
+ readline.emitKeypressEvents(process.stdin)
231
+ process.stdin.setRawMode(true)
232
+ }
233
+
234
+ process.stdin.on('keypress', (str, key) => {
235
+ if (key && (key.name === 'q' || (key.ctrl && key.name === 'c'))) {
236
+ shutdown()
237
+ }
238
+ })
239
+ }
240
+
241
+ const shutdown = () => {
242
+ if (isShuttingDown) return
243
+ isShuttingDown = true
244
+ console.log(`\n${colors.system}[System]${colors.reset} Initiating clean shutdown...`)
245
+
246
+ if (clientProcess) {
247
+ if (isWin) spawn('taskkill', ['/pid', clientProcess.pid, '/f', '/t'], { shell: true })
248
+ else {
249
+ clientProcess.kill('SIGTERM')
250
+ setTimeout(() => {
251
+ if (clientProcess.connected || !clientProcess.killed) clientProcess.kill('SIGKILL')
252
+ }, 5000)
253
+ }
254
+ }
255
+
256
+ const req = http.request({
257
+ hostname: '127.0.0.1',
258
+ port: 3000,
259
+ path: '/api/internal/shutdown',
260
+ method: 'POST',
261
+ })
262
+
263
+ req.on('error', () => {
264
+ console.log(`${colors.system}[System]${colors.reset} Server unreachable, forcing exit.`)
265
+ if (isWin && serverProcess)
266
+ spawn('taskkill', ['/pid', serverProcess.pid, '/f', '/t'], { shell: true })
267
+ else if (serverProcess) {
268
+ serverProcess.kill('SIGTERM')
269
+ setTimeout(() => {
270
+ if (serverProcess.connected || !serverProcess.killed) serverProcess.kill('SIGKILL')
271
+ process.exit(0)
272
+ }, 5000)
273
+ return
274
+ }
275
+ process.exit(0)
276
+ })
277
+
278
+ req.end()
279
+
280
+ setTimeout(() => {
281
+ console.log(`${colors.system}[System]${colors.reset} Force exiting after timeout.`)
282
+ if (isWin && serverProcess)
283
+ spawn('taskkill', ['/pid', serverProcess.pid, '/f', '/t'], { shell: true })
284
+ else if (serverProcess) {
285
+ serverProcess.kill('SIGTERM')
286
+ setTimeout(() => {
287
+ if (serverProcess.connected || !serverProcess.killed) serverProcess.kill('SIGKILL')
288
+ process.exit(1)
289
+ }, 5000)
290
+ return
291
+ }
292
+ process.exit(1)
293
+ }, 15000)
294
+ }
295
+
296
+ process.on('SIGINT', () => {
297
+ shutdown()
298
+ })
299
+ ;(async () => {
300
+ await checkForUpdates()
301
+ main()
302
+ })()
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "ani-web",
3
+ "version": "1.5.8",
4
+ "description": "A web application for watching anime.",
5
+ "main": "orchestrator.js",
6
+ "bin": {
7
+ "ani-web": "orchestrator.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=22.5.0"
11
+ },
12
+ "scripts": {
13
+ "setup": "npm install --prefix client && npm install --prefix server",
14
+ "build": "npm run build --prefix client && npm run build --prefix server",
15
+ "start": "node orchestrator.js prod",
16
+ "dev": "node orchestrator.js dev",
17
+ "client": "npm start --prefix client",
18
+ "server": "npm start --prefix server",
19
+ "preview": "node orchestrator.js prod",
20
+ "lighthouse": "npm run lighthouse --prefix client",
21
+ "test": "echo \"Error: no test specified\" && exit 1",
22
+ "lint": "npm run lint --prefix client && npm run lint --prefix server",
23
+ "format": "prettier --write .",
24
+ "prepublishOnly": "npm run build",
25
+ "update-version": "node update-version.js"
26
+ },
27
+ "files": [
28
+ "orchestrator.js",
29
+ "client/dist",
30
+ "server/dist",
31
+ "LICENSE",
32
+ "README.md",
33
+ "package.json"
34
+ ],
35
+ "keywords": [
36
+ "anime",
37
+ "react",
38
+ "express",
39
+ "typescript",
40
+ "streaming",
41
+ "cli"
42
+ ],
43
+ "author": "",
44
+ "license": "ISC",
45
+ "type": "commonjs",
46
+ "dependencies": {
47
+ "axios": "^1.15.1",
48
+ "axios-retry": "^4.5.0",
49
+ "cheerio": "^1.2.0",
50
+ "chokidar": "^5.0.0",
51
+ "compression": "^1.8.1",
52
+ "cors": "^2.8.5",
53
+ "crypto-js": "^4.2.0",
54
+ "dotenv": "^17.4.2",
55
+ "express": "^5.1.0",
56
+ "multer": "^2.0.2",
57
+ "node-cache": "^5.1.2",
58
+ "pino": "^10.3.1",
59
+ "pino-pretty": "^13.1.1",
60
+ "xml2js": "^0.6.2"
61
+ },
62
+ "overrides": {
63
+ "encoding-sniffer": "^1.0.2"
64
+ },
65
+ "devDependencies": {
66
+ "eslint": "^10.2.1",
67
+ "prettier": "^3.8.3"
68
+ }
69
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CONFIG = exports.SERVER_ROOT = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const dotenv_1 = __importDefault(require("dotenv"));
11
+ exports.SERVER_ROOT = path_1.default.resolve(__dirname, '..');
12
+ const PACKAGE_ROOT = path_1.default.resolve(exports.SERVER_ROOT, '..');
13
+ function resolveDataRoot() {
14
+ if (process.platform === 'win32' && process.env.APPDATA) {
15
+ return path_1.default.join(process.env.APPDATA, 'ani-web');
16
+ }
17
+ if (process.platform === 'darwin') {
18
+ return path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support', 'ani-web');
19
+ }
20
+ if (process.env.XDG_DATA_HOME) {
21
+ return path_1.default.join(process.env.XDG_DATA_HOME, 'ani-web');
22
+ }
23
+ return path_1.default.join(os_1.default.homedir(), '.local', 'share', 'ani-web');
24
+ }
25
+ function moveFileIfNeeded(sourcePath, destinationPath) {
26
+ if (!fs_1.default.existsSync(sourcePath) || fs_1.default.existsSync(destinationPath)) {
27
+ return;
28
+ }
29
+ try {
30
+ fs_1.default.renameSync(sourcePath, destinationPath);
31
+ }
32
+ catch {
33
+ fs_1.default.copyFileSync(sourcePath, destinationPath);
34
+ fs_1.default.unlinkSync(sourcePath);
35
+ }
36
+ }
37
+ function migrateLegacyData(packageServerRoot, dataRoot) {
38
+ const legacyFiles = [
39
+ '.env',
40
+ 'google_tokens.json',
41
+ 'sync_manifest.json',
42
+ 'sync_manifest.dev.json',
43
+ 'anime.db',
44
+ 'anime.db-shm',
45
+ 'anime.db-wal',
46
+ 'anime.dev.db',
47
+ 'anime.dev.db-shm',
48
+ 'anime.dev.db-wal',
49
+ ];
50
+ fs_1.default.mkdirSync(dataRoot, { recursive: true });
51
+ for (const filename of legacyFiles) {
52
+ moveFileIfNeeded(path_1.default.join(packageServerRoot, filename), path_1.default.join(dataRoot, filename));
53
+ }
54
+ }
55
+ const DATA_ROOT = resolveDataRoot();
56
+ const ENV_PATH = path_1.default.join(DATA_ROOT, '.env');
57
+ migrateLegacyData(exports.SERVER_ROOT, DATA_ROOT);
58
+ dotenv_1.default.config({ path: ENV_PATH });
59
+ const IS_DEV = process.argv.includes('--dev');
60
+ const PORT = 3000;
61
+ const GOOGLE_REDIRECT_URI = IS_DEV
62
+ ? 'http://localhost:5173/api/auth/google/callback'
63
+ : `http://localhost:${PORT}/api/auth/google/callback`;
64
+ exports.CONFIG = {
65
+ ROOT: DATA_ROOT,
66
+ SERVER_ROOT: exports.SERVER_ROOT,
67
+ PACKAGE_ROOT,
68
+ ENV_PATH,
69
+ TOKEN_PATH: path_1.default.join(DATA_ROOT, 'google_tokens.json'),
70
+ LOCAL_MANIFEST_PATH: path_1.default.join(DATA_ROOT, IS_DEV ? 'sync_manifest.dev.json' : 'sync_manifest.json'),
71
+ DB_NAME_PROD: 'anime.db',
72
+ DB_NAME_DEV: 'anime.dev.db',
73
+ REMOTE_FOLDER_PROD: 'aniweb_db',
74
+ REMOTE_FOLDER_DEV: 'aniweb_dev_db',
75
+ MANIFEST_FILENAME: IS_DEV ? 'sync_manifest.dev.json' : 'sync_manifest.json',
76
+ GOOGLE_SCOPES: [
77
+ 'https://www.googleapis.com/auth/drive',
78
+ 'https://www.googleapis.com/auth/userinfo.profile',
79
+ ],
80
+ IS_DEV,
81
+ PORT,
82
+ GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
83
+ GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
84
+ GOOGLE_REDIRECT_URI: GOOGLE_REDIRECT_URI,
85
+ RCLONE_REMOTE: process.env.RCLONE_REMOTE,
86
+ };