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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/client/dist/assets/AnimeInfo-C7DQp7Oo.js +1 -0
- package/client/dist/assets/AnimeInfo-Sb3YiXHJ.css +1 -0
- package/client/dist/assets/AnimeInfoPage-DJA7AJQ8.js +2 -0
- package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
- package/client/dist/assets/Button-o0l9V_NG.js +1 -0
- package/client/dist/assets/ErrorMessage-Ddf2zmRx.js +1 -0
- package/client/dist/assets/ErrorMessage-FOxXyZC9.css +1 -0
- package/client/dist/assets/Home-CKHJA97j.css +1 -0
- package/client/dist/assets/Home-Dey0azy1.js +1 -0
- package/client/dist/assets/Insights-BSRcCkDs.css +1 -0
- package/client/dist/assets/Insights-CogjPOd_.js +1 -0
- package/client/dist/assets/MAL-CYArH4yf.js +1 -0
- package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
- package/client/dist/assets/Player-BWFN9gud.js +9 -0
- package/client/dist/assets/Player-CBCYW7uG.css +1 -0
- package/client/dist/assets/PlayerSettings-BgStUrrP.css +1 -0
- package/client/dist/assets/PlayerSettings-rWZuATQf.js +1 -0
- package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
- package/client/dist/assets/RemoveConfirmationModal-CLYqyGOv.js +1 -0
- package/client/dist/assets/Search-DZAWgKwq.js +1 -0
- package/client/dist/assets/Search-lWsVQ0Ke.css +1 -0
- package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
- package/client/dist/assets/Settings-DyisJGeD.js +1 -0
- package/client/dist/assets/ToggleSwitch-CLnWnAuY.js +1 -0
- package/client/dist/assets/ToggleSwitch-DInRb7iM.css +1 -0
- package/client/dist/assets/Watchlist-2dVYksxq.css +1 -0
- package/client/dist/assets/Watchlist-CuqJISI3.js +1 -0
- package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
- package/client/dist/assets/index-BK_Zaqaw.css +1 -0
- package/client/dist/assets/index-CHVF4D4L.js +178 -0
- package/client/dist/assets/useAnimeInfoData-Cr58brCY.js +1 -0
- package/client/dist/assets/useIsMobile-gHo4t6g6.js +1 -0
- package/client/dist/assets/vendor-DdbgYKo4.js +3 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +35 -0
- package/client/dist/logo.png +0 -0
- package/client/dist/placeholder.svg +4 -0
- package/client/dist/robots.txt +3 -0
- package/client/package.json +54 -0
- package/orchestrator.js +302 -0
- package/package.json +69 -0
- package/server/dist/config.js +86 -0
- package/server/dist/constants.json +1359 -0
- package/server/dist/controllers/auth.controller.js +213 -0
- package/server/dist/controllers/data.controller.js +126 -0
- package/server/dist/controllers/insights.controller.js +125 -0
- package/server/dist/controllers/proxy.controller.js +235 -0
- package/server/dist/controllers/settings.controller.js +147 -0
- package/server/dist/controllers/watchlist.controller.js +499 -0
- package/server/dist/db.js +231 -0
- package/server/dist/github-sync.js +279 -0
- package/server/dist/google.js +274 -0
- package/server/dist/logger.js +21 -0
- package/server/dist/providers/123anime.provider.js +229 -0
- package/server/dist/providers/allanime.provider.js +773 -0
- package/server/dist/providers/animepahe.provider.js +313 -0
- package/server/dist/providers/animeya.provider.js +799 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +30 -0
- package/server/dist/repositories/notifications.repository.js +22 -0
- package/server/dist/repositories/settings.repository.js +13 -0
- package/server/dist/repositories/shows-meta.repository.js +39 -0
- package/server/dist/repositories/watched-episodes.repository.js +60 -0
- package/server/dist/repositories/watchlist.repository.js +49 -0
- package/server/dist/routes/auth.routes.js +23 -0
- package/server/dist/routes/data.routes.js +43 -0
- package/server/dist/routes/insights.routes.js +11 -0
- package/server/dist/routes/proxy.routes.js +13 -0
- package/server/dist/routes/settings.routes.js +26 -0
- package/server/dist/routes/watchlist.routes.js +26 -0
- package/server/dist/server.js +179 -0
- package/server/dist/sync-config.js +28 -0
- package/server/dist/sync.js +383 -0
- package/server/dist/utils/db-utils.js +36 -0
- package/server/dist/utils/env.utils.js +70 -0
- package/server/package.json +54 -0
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
process.setMaxListeners(100);
|
|
7
|
+
const events_1 = require("events");
|
|
8
|
+
events_1.EventEmitter.defaultMaxListeners = 100;
|
|
9
|
+
const express_1 = __importDefault(require("express"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const cors_1 = __importDefault(require("cors"));
|
|
12
|
+
const compression_1 = __importDefault(require("compression"));
|
|
13
|
+
const node_cache_1 = __importDefault(require("node-cache"));
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
16
|
+
const logger_1 = __importDefault(require("./logger"));
|
|
17
|
+
const allanime_provider_1 = require("./providers/allanime.provider");
|
|
18
|
+
const animepahe_provider_1 = require("./providers/animepahe.provider");
|
|
19
|
+
const _123anime_provider_1 = require("./providers/123anime.provider");
|
|
20
|
+
const animeya_provider_1 = require("./providers/animeya.provider");
|
|
21
|
+
const config_1 = require("./config");
|
|
22
|
+
const sync_1 = require("./sync");
|
|
23
|
+
const auth_routes_1 = require("./routes/auth.routes");
|
|
24
|
+
const watchlist_routes_1 = require("./routes/watchlist.routes");
|
|
25
|
+
const data_routes_1 = require("./routes/data.routes");
|
|
26
|
+
const proxy_routes_1 = require("./routes/proxy.routes");
|
|
27
|
+
const settings_routes_1 = require("./routes/settings.routes");
|
|
28
|
+
const insights_routes_1 = require("./routes/insights.routes");
|
|
29
|
+
const app = (0, express_1.default)();
|
|
30
|
+
const apiCache = new node_cache_1.default({ stdTTL: 3600 });
|
|
31
|
+
const allAnimeProvider = new allanime_provider_1.AllAnimeProvider(apiCache);
|
|
32
|
+
const animePaheProvider = new animepahe_provider_1.AnimePaheProvider(apiCache);
|
|
33
|
+
const _123AnimeProvider = new _123anime_provider_1._123AnimeProvider(apiCache);
|
|
34
|
+
const animeyaProvider = new animeya_provider_1.AnimeyaProvider(apiCache);
|
|
35
|
+
const providers = {
|
|
36
|
+
allanime: allAnimeProvider,
|
|
37
|
+
animepahe: animePaheProvider,
|
|
38
|
+
'123anime': _123AnimeProvider,
|
|
39
|
+
animeya: animeyaProvider,
|
|
40
|
+
};
|
|
41
|
+
let db;
|
|
42
|
+
let isShuttingDown = false;
|
|
43
|
+
async function runSyncSequence(database) {
|
|
44
|
+
const dbName = config_1.CONFIG.IS_DEV ? config_1.CONFIG.DB_NAME_DEV : config_1.CONFIG.DB_NAME_PROD;
|
|
45
|
+
const dbPath = path_1.default.join(config_1.CONFIG.ROOT, dbName);
|
|
46
|
+
const remoteFolder = config_1.CONFIG.IS_DEV ? config_1.CONFIG.REMOTE_FOLDER_DEV : config_1.CONFIG.REMOTE_FOLDER_PROD;
|
|
47
|
+
await (0, sync_1.initSyncProvider)();
|
|
48
|
+
const didDownload = await (0, sync_1.syncDownOnBoot)(database, dbPath, remoteFolder, () => {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
if (database && !database.isClosedCheck()) {
|
|
51
|
+
database.checkpoint();
|
|
52
|
+
database.close(() => resolve());
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
resolve();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
if (didDownload) {
|
|
60
|
+
db = await (0, sync_1.initializeDatabase)(dbPath);
|
|
61
|
+
logger_1.default.info('Database re-initialized after sync.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
app.use((req, res, next) => {
|
|
65
|
+
if (isShuttingDown) {
|
|
66
|
+
return res.status(503).send('Server is shutting down...');
|
|
67
|
+
}
|
|
68
|
+
if (!db) {
|
|
69
|
+
return res.status(503).send('Database initializing...');
|
|
70
|
+
}
|
|
71
|
+
req.db = db;
|
|
72
|
+
next();
|
|
73
|
+
});
|
|
74
|
+
// axiosRetry is applied only on the dedicated proxy axiosInstance (proxy.controller.ts)
|
|
75
|
+
// to avoid amplifying retries on providers that already have their own fallback logic.
|
|
76
|
+
app.use((0, compression_1.default)({
|
|
77
|
+
level: 2,
|
|
78
|
+
threshold: 1024,
|
|
79
|
+
filter: (req, res) => {
|
|
80
|
+
if (req.headers['x-no-compression']) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return compression_1.default.filter(req, res);
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
app.use((0, cors_1.default)());
|
|
87
|
+
app.use(express_1.default.json({ limit: '10mb' }));
|
|
88
|
+
app.use('/api/auth', (0, auth_routes_1.createAuthRouter)((database) => runSyncSequence(database)));
|
|
89
|
+
app.use('/api', (0, watchlist_routes_1.createWatchlistRouter)(allAnimeProvider));
|
|
90
|
+
app.use('/api', (0, data_routes_1.createDataRouter)(apiCache, providers));
|
|
91
|
+
app.use('/api', (0, proxy_routes_1.createProxyRouter)());
|
|
92
|
+
app.use('/api', (0, insights_routes_1.createInsightsRouter)(allAnimeProvider));
|
|
93
|
+
app.use('/api', (0, settings_routes_1.createSettingsRouter)(allAnimeProvider, () => db, sync_1.initializeDatabase, (newDb) => {
|
|
94
|
+
db = newDb;
|
|
95
|
+
}));
|
|
96
|
+
if (!config_1.CONFIG.IS_DEV) {
|
|
97
|
+
const frontendPath = path_1.default.join(config_1.CONFIG.PACKAGE_ROOT, 'client', 'dist');
|
|
98
|
+
logger_1.default.info(`Serving frontend from: ${frontendPath}`);
|
|
99
|
+
app.use(express_1.default.static(frontendPath));
|
|
100
|
+
app.get(/^(?!\/api).+/, (req, res) => {
|
|
101
|
+
res.sendFile('index.html', { root: frontendPath }, (err) => {
|
|
102
|
+
if (err) {
|
|
103
|
+
logger_1.default.error({ err }, `Failed to serve index.html from ${frontendPath}`);
|
|
104
|
+
if (!res.headersSent) {
|
|
105
|
+
res.status(500).send('Server Error: Frontend build not found.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function main() {
|
|
112
|
+
logger_1.default.info('DEBUG: main() started');
|
|
113
|
+
const dbName = config_1.CONFIG.IS_DEV ? config_1.CONFIG.DB_NAME_DEV : config_1.CONFIG.DB_NAME_PROD;
|
|
114
|
+
const dbPath = path_1.default.join(config_1.CONFIG.ROOT, dbName);
|
|
115
|
+
const remoteFolder = config_1.CONFIG.IS_DEV ? config_1.CONFIG.REMOTE_FOLDER_DEV : config_1.CONFIG.REMOTE_FOLDER_PROD;
|
|
116
|
+
db = await (0, sync_1.initializeDatabase)(dbPath);
|
|
117
|
+
logger_1.default.info(`Database initialized at ${dbPath}`);
|
|
118
|
+
await runSyncSequence(db);
|
|
119
|
+
if (!fs_1.default.existsSync(config_1.CONFIG.LOCAL_MANIFEST_PATH)) {
|
|
120
|
+
fs_1.default.writeFileSync(config_1.CONFIG.LOCAL_MANIFEST_PATH, JSON.stringify({ version: 0 }));
|
|
121
|
+
}
|
|
122
|
+
const watcher = chokidar_1.default.watch(config_1.CONFIG.LOCAL_MANIFEST_PATH, {
|
|
123
|
+
persistent: true,
|
|
124
|
+
ignoreInitial: true,
|
|
125
|
+
});
|
|
126
|
+
let debounceTimer;
|
|
127
|
+
const expressServer = app.listen(config_1.CONFIG.PORT, () => {
|
|
128
|
+
logger_1.default.info(`Server running on http://localhost:${config_1.CONFIG.PORT}`);
|
|
129
|
+
});
|
|
130
|
+
watcher.on('change', () => {
|
|
131
|
+
clearTimeout(debounceTimer);
|
|
132
|
+
debounceTimer = setTimeout(() => (0, sync_1.syncUp)(db, dbPath, remoteFolder), 30000);
|
|
133
|
+
});
|
|
134
|
+
const shutdown = async (signal) => {
|
|
135
|
+
if (isShuttingDown)
|
|
136
|
+
return;
|
|
137
|
+
isShuttingDown = true;
|
|
138
|
+
clearTimeout(debounceTimer);
|
|
139
|
+
// Close the watcher first to prevent a stale 'change' event from arming
|
|
140
|
+
// a new debounce timer that would call syncUp on an already-closed database.
|
|
141
|
+
await watcher.close();
|
|
142
|
+
if (expressServer) {
|
|
143
|
+
expressServer.close();
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
await (0, sync_1.syncUp)(db, dbPath, remoteFolder);
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
console.error('Sync failed:', e);
|
|
150
|
+
}
|
|
151
|
+
db.close(() => {
|
|
152
|
+
console.log('[SERVER_EXIT]');
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
if (signal === 'SIGUSR2') {
|
|
155
|
+
process.kill(process.pid, 'SIGUSR2');
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
}, 600);
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
164
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
165
|
+
process.once('SIGUSR2', () => shutdown('SIGUSR2'));
|
|
166
|
+
app.post('/api/internal/shutdown', (req, res) => {
|
|
167
|
+
if (req.ip === '::1' || req.ip === '127.0.0.1' || req.ip === '::ffff:127.0.0.1') {
|
|
168
|
+
res.status(200).json({ message: 'Shutting down' });
|
|
169
|
+
setTimeout(() => shutdown(), 500);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
res.status(403).send('Forbidden');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
main().catch((err) => {
|
|
177
|
+
console.error('Server failed to start:', err);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setActiveRemote = setActiveRemote;
|
|
4
|
+
exports.getActiveRemote = getActiveRemote;
|
|
5
|
+
exports.initialize = initialize;
|
|
6
|
+
exports.getRemoteString = getRemoteString;
|
|
7
|
+
const log = (message) => console.log(`[Sync Config] ${new Date().toISOString()} - ${message}`);
|
|
8
|
+
let activeRemote;
|
|
9
|
+
function setActiveRemote(remote) {
|
|
10
|
+
log(`Setting active sync remote to: ${remote}`);
|
|
11
|
+
activeRemote = remote;
|
|
12
|
+
}
|
|
13
|
+
function getActiveRemote() {
|
|
14
|
+
return activeRemote;
|
|
15
|
+
}
|
|
16
|
+
async function initialize() {
|
|
17
|
+
if (activeRemote !== 'gdrive') {
|
|
18
|
+
log('Active remote is not gdrive, skipping gdrive-specific initialization.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
log('gdrive is the active remote.');
|
|
22
|
+
}
|
|
23
|
+
function getRemoteString(remoteDir) {
|
|
24
|
+
if (!activeRemote) {
|
|
25
|
+
throw new Error('Cannot get remote string: active remote is not set.');
|
|
26
|
+
}
|
|
27
|
+
return `${activeRemote}:${remoteDir}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.initSyncProvider = initSyncProvider;
|
|
40
|
+
exports.getLocalManifestVersion = getLocalManifestVersion;
|
|
41
|
+
exports.setLocalManifestVersion = setLocalManifestVersion;
|
|
42
|
+
exports.syncDownOnBoot = syncDownOnBoot;
|
|
43
|
+
exports.syncUp = syncUp;
|
|
44
|
+
exports.performWriteTransaction = performWriteTransaction;
|
|
45
|
+
exports.initializeDatabase = initializeDatabase;
|
|
46
|
+
const fs = __importStar(require("fs/promises"));
|
|
47
|
+
const fs_1 = require("fs");
|
|
48
|
+
const path_1 = __importDefault(require("path"));
|
|
49
|
+
const logger_1 = __importDefault(require("./logger"));
|
|
50
|
+
const google_1 = require("./google");
|
|
51
|
+
const rclone_1 = require("./rclone");
|
|
52
|
+
const github_sync_1 = require("./github-sync");
|
|
53
|
+
const config_1 = require("./config");
|
|
54
|
+
const db_1 = require("./db");
|
|
55
|
+
const db_utils_1 = require("./utils/db-utils");
|
|
56
|
+
const log = logger_1.default.child({ module: 'Sync' });
|
|
57
|
+
class Mutex {
|
|
58
|
+
_locked = false;
|
|
59
|
+
_waiting = [];
|
|
60
|
+
async lock() {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
if (!this._locked) {
|
|
63
|
+
this._locked = true;
|
|
64
|
+
resolve();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this._waiting.push(resolve);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
unlock() {
|
|
72
|
+
if (this._waiting.length > 0) {
|
|
73
|
+
const resolve = this._waiting.shift();
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
this._locked = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const syncMutex = new Mutex();
|
|
82
|
+
let isSyncing = false;
|
|
83
|
+
let activeProvider = 'none';
|
|
84
|
+
async function initSyncProvider() {
|
|
85
|
+
if (github_sync_1.githubSyncService.isAuthenticated()) {
|
|
86
|
+
activeProvider = 'github';
|
|
87
|
+
log.info('Sync Provider: GitHub');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (google_1.googleDriveService.isAuthenticated()) {
|
|
91
|
+
activeProvider = 'google';
|
|
92
|
+
log.info('Sync Provider: Google Drive API');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const rcloneAvailable = await rclone_1.rcloneService.init();
|
|
96
|
+
if (rcloneAvailable) {
|
|
97
|
+
activeProvider = 'rclone';
|
|
98
|
+
log.info(`Sync Provider: Rclone (${rclone_1.rcloneService.getRemoteName()})`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
activeProvider = 'none';
|
|
102
|
+
log.info('No sync provider available.');
|
|
103
|
+
}
|
|
104
|
+
async function getLocalManifestVersion() {
|
|
105
|
+
if ((0, fs_1.existsSync)(config_1.CONFIG.LOCAL_MANIFEST_PATH)) {
|
|
106
|
+
try {
|
|
107
|
+
const content = await fs.readFile(config_1.CONFIG.LOCAL_MANIFEST_PATH, 'utf-8');
|
|
108
|
+
return JSON.parse(content).version || 0;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
async function setLocalManifestVersion(version) {
|
|
117
|
+
await fs.writeFile(config_1.CONFIG.LOCAL_MANIFEST_PATH, JSON.stringify({ version }));
|
|
118
|
+
}
|
|
119
|
+
async function getRemoteManifestVersion(remoteFolder) {
|
|
120
|
+
try {
|
|
121
|
+
if (activeProvider === 'github') {
|
|
122
|
+
return { version: await github_sync_1.githubSyncService.getRemoteVersion() };
|
|
123
|
+
}
|
|
124
|
+
else if (activeProvider === 'google') {
|
|
125
|
+
const folderId = await google_1.googleDriveService.ensureFolder(remoteFolder);
|
|
126
|
+
const file = await google_1.googleDriveService.findFile(config_1.CONFIG.MANIFEST_FILENAME, folderId);
|
|
127
|
+
if (!file)
|
|
128
|
+
return { version: 0 };
|
|
129
|
+
const tempPath = path_1.default.join(config_1.CONFIG.ROOT, `temp_${Date.now()}_manifest.json`);
|
|
130
|
+
try {
|
|
131
|
+
await google_1.googleDriveService.downloadFile(file.id, tempPath);
|
|
132
|
+
const content = await fs.readFile(tempPath, 'utf-8');
|
|
133
|
+
return { version: JSON.parse(content).version || 0, fileId: file.id };
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
if ((0, fs_1.existsSync)(tempPath))
|
|
137
|
+
await fs.unlink(tempPath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (activeProvider === 'rclone') {
|
|
141
|
+
const exists = await rclone_1.rcloneService.fileExists(remoteFolder, config_1.CONFIG.MANIFEST_FILENAME);
|
|
142
|
+
if (!exists)
|
|
143
|
+
return { version: 0 };
|
|
144
|
+
const tempPath = path_1.default.join(config_1.CONFIG.ROOT, `temp_${Date.now()}_manifest.json`);
|
|
145
|
+
try {
|
|
146
|
+
await rclone_1.rcloneService.downloadFile(remoteFolder, config_1.CONFIG.MANIFEST_FILENAME, tempPath);
|
|
147
|
+
const content = await fs.readFile(tempPath, 'utf-8');
|
|
148
|
+
return { version: JSON.parse(content).version || 0 };
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if ((0, fs_1.existsSync)(tempPath))
|
|
152
|
+
await fs.unlink(tempPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
log.warn({ err }, 'Could not read remote manifest.');
|
|
158
|
+
}
|
|
159
|
+
return { version: 0 };
|
|
160
|
+
}
|
|
161
|
+
async function syncDownOnBoot(db, dbPath, remoteFolderName, closeMainDb) {
|
|
162
|
+
let localVersion = await getLocalManifestVersion();
|
|
163
|
+
if (localVersion === 0 && db) {
|
|
164
|
+
const row = await (0, db_utils_1.dbGet)(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
|
|
165
|
+
localVersion = row?.value ?? 0;
|
|
166
|
+
if (localVersion > 0) {
|
|
167
|
+
await setLocalManifestVersion(localVersion);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (activeProvider === 'none')
|
|
171
|
+
return false;
|
|
172
|
+
await syncMutex.lock();
|
|
173
|
+
if (isSyncing) {
|
|
174
|
+
syncMutex.unlock();
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
isSyncing = true;
|
|
178
|
+
try {
|
|
179
|
+
console.log(`[SYNC_START] Initial sync check (${activeProvider})`);
|
|
180
|
+
const { version: remoteVersion } = await getRemoteManifestVersion(remoteFolderName);
|
|
181
|
+
console.log('[SYNC_END]');
|
|
182
|
+
log.info(`Sync Check: Local v${localVersion} vs Remote v${remoteVersion}`);
|
|
183
|
+
if (remoteVersion > localVersion) {
|
|
184
|
+
if (activeProvider === 'github') {
|
|
185
|
+
console.log(`[SYNC_START] Importing GitHub sync data (Remote v${remoteVersion})`);
|
|
186
|
+
const importedVersion = await github_sync_1.githubSyncService.syncDown(db);
|
|
187
|
+
await setLocalManifestVersion(importedVersion || remoteVersion);
|
|
188
|
+
console.log('[SYNC_END]');
|
|
189
|
+
log.info('GitHub sync down complete.');
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
console.log(`[SYNC_START] Downloading remote database (Remote v${remoteVersion})`);
|
|
193
|
+
await closeMainDb();
|
|
194
|
+
const backupPath = `${dbPath}.bak`;
|
|
195
|
+
const dbName = path_1.default.basename(dbPath);
|
|
196
|
+
try {
|
|
197
|
+
if ((0, fs_1.existsSync)(dbPath)) {
|
|
198
|
+
await fs.copyFile(dbPath, backupPath);
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await fs.unlink(`${dbPath}-wal`);
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
void e;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await fs.unlink(`${dbPath}-shm`);
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
void e;
|
|
211
|
+
}
|
|
212
|
+
if (activeProvider === 'google') {
|
|
213
|
+
const folderId = await google_1.googleDriveService.ensureFolder(remoteFolderName);
|
|
214
|
+
const remoteDb = await google_1.googleDriveService.findFile(dbName, folderId);
|
|
215
|
+
const remoteManifest = await google_1.googleDriveService.findFile(config_1.CONFIG.MANIFEST_FILENAME, folderId);
|
|
216
|
+
if (remoteDb)
|
|
217
|
+
await google_1.googleDriveService.downloadFile(remoteDb.id, dbPath);
|
|
218
|
+
if (remoteManifest)
|
|
219
|
+
await google_1.googleDriveService.downloadFile(remoteManifest.id, config_1.CONFIG.LOCAL_MANIFEST_PATH);
|
|
220
|
+
}
|
|
221
|
+
else if (activeProvider === 'rclone') {
|
|
222
|
+
await rclone_1.rcloneService.downloadFile(remoteFolderName, dbName, dbPath);
|
|
223
|
+
await rclone_1.rcloneService.downloadFile(remoteFolderName, config_1.CONFIG.MANIFEST_FILENAME, config_1.CONFIG.LOCAL_MANIFEST_PATH);
|
|
224
|
+
}
|
|
225
|
+
if ((0, fs_1.existsSync)(backupPath)) {
|
|
226
|
+
await fs.unlink(backupPath);
|
|
227
|
+
}
|
|
228
|
+
console.log('[SYNC_END]');
|
|
229
|
+
log.info('Sync down complete.');
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
console.log('[SYNC_END]');
|
|
234
|
+
log.error({ err }, 'Sync down failed. Restoring backup.');
|
|
235
|
+
if ((0, fs_1.existsSync)(backupPath)) {
|
|
236
|
+
try {
|
|
237
|
+
await fs.copyFile(backupPath, dbPath);
|
|
238
|
+
log.info('Backup restored successfully after failed sync down.');
|
|
239
|
+
}
|
|
240
|
+
catch (restoreErr) {
|
|
241
|
+
log.error({ err: restoreErr }, 'Critical: restore from backup also failed.');
|
|
242
|
+
// The DB may be corrupt – propagate so the caller can exit cleanly.
|
|
243
|
+
throw new Error('Sync down and restore both failed. Database may be corrupt.', {
|
|
244
|
+
cause: restoreErr,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// DB was already closed before the attempt; re-open is required regardless.
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
log.info('Local DB is up to date.');
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
console.log('[SYNC_END]');
|
|
259
|
+
log.error({ err }, 'Sync boot error.');
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
isSyncing = false;
|
|
264
|
+
syncMutex.unlock();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function syncUp(db, dbPath, remoteFolderName) {
|
|
268
|
+
if (activeProvider === 'none')
|
|
269
|
+
return;
|
|
270
|
+
await syncMutex.lock();
|
|
271
|
+
if (isSyncing) {
|
|
272
|
+
syncMutex.unlock();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
isSyncing = true;
|
|
276
|
+
try {
|
|
277
|
+
const localVersion = await getLocalManifestVersion();
|
|
278
|
+
console.log(`[SYNC_START] Syncing up (Local v${localVersion})`);
|
|
279
|
+
const { version: remoteVersion, fileId: manifestId } = await getRemoteManifestVersion(remoteFolderName);
|
|
280
|
+
if (localVersion > remoteVersion) {
|
|
281
|
+
const dbName = path_1.default.basename(dbPath);
|
|
282
|
+
const syncDbPath = `${dbPath}.sync.db`;
|
|
283
|
+
try {
|
|
284
|
+
if (activeProvider === 'github') {
|
|
285
|
+
await github_sync_1.githubSyncService.syncUp(db);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
db.backup(syncDbPath);
|
|
289
|
+
}
|
|
290
|
+
if (activeProvider === 'google') {
|
|
291
|
+
const folderId = await google_1.googleDriveService.ensureFolder(remoteFolderName);
|
|
292
|
+
const remoteDbFile = await google_1.googleDriveService.findFile(dbName, folderId);
|
|
293
|
+
await google_1.googleDriveService.uploadFile(syncDbPath, dbName, 'application/x-sqlite3', folderId, remoteDbFile?.id);
|
|
294
|
+
await google_1.googleDriveService.uploadFile(config_1.CONFIG.LOCAL_MANIFEST_PATH, config_1.CONFIG.MANIFEST_FILENAME, 'application/json', folderId, manifestId);
|
|
295
|
+
}
|
|
296
|
+
else if (activeProvider === 'rclone') {
|
|
297
|
+
await rclone_1.rcloneService.uploadFile(syncDbPath, remoteFolderName, dbName);
|
|
298
|
+
await rclone_1.rcloneService.uploadFile(config_1.CONFIG.LOCAL_MANIFEST_PATH, remoteFolderName, config_1.CONFIG.MANIFEST_FILENAME);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
if ((0, fs_1.existsSync)(syncDbPath))
|
|
303
|
+
await fs.unlink(syncDbPath).catch(() => { });
|
|
304
|
+
}
|
|
305
|
+
console.log('[SYNC_END]');
|
|
306
|
+
log.info('Sync up complete.');
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
console.log('[SYNC_END]');
|
|
310
|
+
log.info('No changes to sync up or remote is newer.');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
console.log('[SYNC_END]');
|
|
315
|
+
log.error({ err }, 'Sync up failed.');
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
isSyncing = false;
|
|
319
|
+
syncMutex.unlock();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function performWriteTransaction(db, runnable) {
|
|
323
|
+
// node:sqlite is fully synchronous — serialize/run/get all complete before returning,
|
|
324
|
+
// so we can avoid the nested Promise/callback pattern that could hang forever if
|
|
325
|
+
// setLocalManifestVersion threw after resolve/reject was no longer reachable.
|
|
326
|
+
db.serialize(() => {
|
|
327
|
+
runnable(db);
|
|
328
|
+
db.run("UPDATE sync_metadata SET value = value + 1 WHERE key = 'db_version'");
|
|
329
|
+
});
|
|
330
|
+
// Capture version synchronously; the callback fires immediately with node:sqlite.
|
|
331
|
+
const row = await (0, db_utils_1.dbGet)(db, "SELECT value FROM sync_metadata WHERE key = 'db_version'");
|
|
332
|
+
const newVersion = row?.value ?? 1;
|
|
333
|
+
// Only the manifest write is truly async (disk I\O); errors here propagate normally.
|
|
334
|
+
await setLocalManifestVersion(newVersion);
|
|
335
|
+
}
|
|
336
|
+
async function initializeDatabase(dbPath) {
|
|
337
|
+
try {
|
|
338
|
+
const db = await db_1.DatabaseWrapper.create(dbPath);
|
|
339
|
+
db.configure('busyTimeout', 5000);
|
|
340
|
+
db.run('PRAGMA journal_mode = WAL;');
|
|
341
|
+
db.run('PRAGMA synchronous = NORMAL;');
|
|
342
|
+
db.run('PRAGMA cache_size = -20000;');
|
|
343
|
+
db.run('PRAGMA temp_store = MEMORY;');
|
|
344
|
+
db.run('PRAGMA mmap_size = 268435456;');
|
|
345
|
+
db.run('PRAGMA foreign_keys = ON;');
|
|
346
|
+
const initOpts = { skipSave: true };
|
|
347
|
+
db.run(`CREATE TABLE IF NOT EXISTS watchlist (id TEXT NOT NULL, name TEXT, thumbnail TEXT, status TEXT, nativeName TEXT, englishName TEXT, type TEXT, PRIMARY KEY (id))`, undefined, undefined, initOpts);
|
|
348
|
+
db.run(`CREATE TABLE IF NOT EXISTS watched_episodes (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, watchedAt DATETIME DEFAULT CURRENT_TIMESTAMP, currentTime REAL DEFAULT 0, duration REAL DEFAULT 0, PRIMARY KEY (showId, episodeNumber))`, undefined, undefined, initOpts);
|
|
349
|
+
db.run(`CREATE TABLE IF NOT EXISTS settings (key TEXT NOT NULL, value TEXT, PRIMARY KEY (key))`, undefined, undefined, initOpts);
|
|
350
|
+
db.run(`CREATE TABLE IF NOT EXISTS shows_meta (id TEXT PRIMARY KEY, name TEXT, thumbnail TEXT, nativeName TEXT, englishName TEXT, episodeCount INTEGER, status TEXT, genres TEXT, popularityScore INTEGER, type TEXT)`, undefined, undefined, initOpts);
|
|
351
|
+
db.run(`CREATE TABLE IF NOT EXISTS dismissed_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, dismissedAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))`, undefined, undefined, initOpts);
|
|
352
|
+
db.run(`CREATE TABLE IF NOT EXISTS discovered_notifications (showId TEXT NOT NULL, episodeNumber TEXT NOT NULL, discoveredAt DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (showId, episodeNumber))`, undefined, undefined, initOpts);
|
|
353
|
+
db.run(`CREATE TABLE IF NOT EXISTS sync_metadata (key TEXT PRIMARY KEY, value INTEGER)`, undefined, undefined, initOpts);
|
|
354
|
+
db.run(`INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('db_version', 1)`, undefined, undefined, initOpts);
|
|
355
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId ON watched_episodes(showId)`, undefined, undefined, initOpts);
|
|
356
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_watched_episodes_showId_episodeNumber ON watched_episodes(showId, episodeNumber)`, undefined, undefined, initOpts);
|
|
357
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_watched_episodes_watchedAt ON watched_episodes(watchedAt)`, undefined, undefined, initOpts);
|
|
358
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_watchlist_status ON watchlist(status)`, undefined, undefined, initOpts);
|
|
359
|
+
const addCol = (tbl, col, type) => {
|
|
360
|
+
db.all(`PRAGMA table_info(${tbl})`, (e, r) => {
|
|
361
|
+
const columns = r;
|
|
362
|
+
if (!columns.some((c) => c.name === col))
|
|
363
|
+
db.run(`ALTER TABLE ${tbl} ADD COLUMN ${col} ${type}`, undefined, undefined, initOpts);
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
addCol('watchlist', 'nativeName', 'TEXT');
|
|
367
|
+
addCol('watchlist', 'englishName', 'TEXT');
|
|
368
|
+
addCol('shows_meta', 'nativeName', 'TEXT');
|
|
369
|
+
addCol('shows_meta', 'englishName', 'TEXT');
|
|
370
|
+
addCol('shows_meta', 'episodeCount', 'INTEGER');
|
|
371
|
+
addCol('shows_meta', 'status', 'TEXT');
|
|
372
|
+
addCol('shows_meta', 'genres', 'TEXT');
|
|
373
|
+
addCol('shows_meta', 'popularityScore', 'INTEGER');
|
|
374
|
+
addCol('watchlist', 'type', 'TEXT');
|
|
375
|
+
addCol('shows_meta', 'type', 'TEXT');
|
|
376
|
+
await db.saveNow();
|
|
377
|
+
return db;
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
log.error({ err }, 'Database opening error');
|
|
381
|
+
throw err;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dbRun = exports.dbGet = exports.dbAll = void 0;
|
|
4
|
+
const dbAll = (db, sql, params = []) => {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
db.all(sql, params, (err, rows) => {
|
|
7
|
+
if (err)
|
|
8
|
+
reject(err);
|
|
9
|
+
else
|
|
10
|
+
resolve(rows);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
exports.dbAll = dbAll;
|
|
15
|
+
const dbGet = (db, sql, params = []) => {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
db.get(sql, params, (err, row) => {
|
|
18
|
+
if (err)
|
|
19
|
+
reject(err);
|
|
20
|
+
else
|
|
21
|
+
resolve(row);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
exports.dbGet = dbGet;
|
|
26
|
+
const dbRun = (db, sql, params = []) => {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
db.run(sql, params, (err) => {
|
|
29
|
+
if (err)
|
|
30
|
+
reject(err);
|
|
31
|
+
else
|
|
32
|
+
resolve();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
exports.dbRun = dbRun;
|