@serifpersia/dango 3.0.3
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 +247 -0
- package/client/dist/assets/AnimeInfo-CMxs0nRH.css +1 -0
- package/client/dist/assets/AnimeInfo-DVdaocCl.js +1 -0
- package/client/dist/assets/AnimeInfoPage-Bx5EvGt-.js +2 -0
- package/client/dist/assets/Asmr-C2i9GKD4.css +1 -0
- package/client/dist/assets/Asmr-CIjmFtRH.js +2 -0
- package/client/dist/assets/Asmr.module-DPG2JhDJ.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-CIYcyon5.css +1 -0
- package/client/dist/assets/AvSyncCalibrator-vqErH7Cm.js +10 -0
- package/client/dist/assets/ErrorMessage-BwSjzblw.js +1 -0
- package/client/dist/assets/ErrorMessage-CXoRbrLN.css +1 -0
- package/client/dist/assets/Home-D7YfGBUk.js +1 -0
- package/client/dist/assets/Home-DKCfEiwl.css +1 -0
- package/client/dist/assets/Insights-BljI64Ua.css +1 -0
- package/client/dist/assets/Insights-CLQGrlpX.js +1 -0
- package/client/dist/assets/Manga-DQ9HYnpk.js +1 -0
- package/client/dist/assets/Manga-H8Wb04w3.css +1 -0
- package/client/dist/assets/Map-CXFqXmsZ.css +1 -0
- package/client/dist/assets/Map-L_ryVBTq.js +1 -0
- package/client/dist/assets/Mature-BeWzAJ7B.js +1 -0
- package/client/dist/assets/Mature-DJmUUtlF.css +1 -0
- package/client/dist/assets/Player-CYpd__FP.js +30 -0
- package/client/dist/assets/Player-DreAhybc.css +1 -0
- package/client/dist/assets/PlayerRedirect-fxUAp9JV.js +1 -0
- package/client/dist/assets/PlayerSettings-BcsotxP9.js +1 -0
- package/client/dist/assets/PlayerSettings-Cqua1CDD.css +1 -0
- package/client/dist/assets/QueueRail-B9Pgbqc3.js +1 -0
- package/client/dist/assets/QueueRail-BnaltOP8.css +1 -0
- package/client/dist/assets/Radio-BvdY5twD.js +1 -0
- package/client/dist/assets/Radio-CUuYy3DY.css +1 -0
- package/client/dist/assets/Search-BzO-aRP7.css +1 -0
- package/client/dist/assets/Search-R9YLAXS1.js +1 -0
- package/client/dist/assets/Search.module-D8ZbQAiK.js +1 -0
- package/client/dist/assets/Settings-9kYcXdTR.js +21 -0
- package/client/dist/assets/Settings-DF8JThUL.css +1 -0
- package/client/dist/assets/SkeletonGrid-D5vRvX5q.js +1 -0
- package/client/dist/assets/SkeletonGrid-DEbYtO4H.css +1 -0
- package/client/dist/assets/SynopsisText-BlFw0R-0.js +1 -0
- package/client/dist/assets/SynopsisText-CUgOjOxt.css +1 -0
- package/client/dist/assets/Trackers-DfZlnnVq.js +2 -0
- package/client/dist/assets/Trackers-Dt9lR_uP.css +1 -0
- package/client/dist/assets/Tv-CVKC2YBs.css +1 -0
- package/client/dist/assets/Tv-Db1Wx_b7.js +15 -0
- package/client/dist/assets/Watchlist-B2nkNZqM.css +1 -0
- package/client/dist/assets/Watchlist-a77CYtzu.js +1 -0
- package/client/dist/assets/hls-6Owgw2Aj.js +1 -0
- package/client/dist/assets/hls-BOLXTnjL.js +42 -0
- package/client/dist/assets/index-B6qdXFtl.css +1 -0
- package/client/dist/assets/index-B8TPVaIS.js +30354 -0
- package/client/dist/assets/index-CpjgU8ou.js +178 -0
- package/client/dist/assets/useAnimeInfoData-BOfyi9UJ.js +1 -0
- package/client/dist/assets/useIsMobile-CM15uQn8.css +1 -0
- package/client/dist/assets/useIsMobile-Cb8HcKRj.js +1 -0
- package/client/dist/assets/useMatureConsent-CZMYBVXd.js +1 -0
- package/client/dist/assets/vendor-rLNfZm7Y.js +1 -0
- package/client/dist/av-sync-test.mp4 +0 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +31 -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 +416 -0
- package/package.json +89 -0
- package/server/dist/app-auth.js +231 -0
- package/server/dist/config.js +133 -0
- package/server/dist/controllers/auth.controller.js +247 -0
- package/server/dist/controllers/data.controller.js +785 -0
- package/server/dist/controllers/insights.controller.js +197 -0
- package/server/dist/controllers/proxy.controller.js +665 -0
- package/server/dist/controllers/settings.controller.js +267 -0
- package/server/dist/controllers/watchlist.controller.js +967 -0
- package/server/dist/db.js +152 -0
- package/server/dist/discord-gateway.js +381 -0
- package/server/dist/discord-rpc.js +424 -0
- package/server/dist/github-sync.js +403 -0
- package/server/dist/google.js +549 -0
- package/server/dist/lib/anilist.js +1519 -0
- package/server/dist/lib/anischedule.js +202 -0
- package/server/dist/lib/ipc.js +32 -0
- package/server/dist/lib/kitsu.js +552 -0
- package/server/dist/lib/mal.js +886 -0
- package/server/dist/lib/migration.js +335 -0
- package/server/dist/lib/temp-ids.js +11 -0
- package/server/dist/lib/tmdb.js +71 -0
- package/server/dist/lib/tracker/anilist-tracker.js +250 -0
- package/server/dist/lib/tracker/sync.service.js +312 -0
- package/server/dist/logger.js +19 -0
- package/server/dist/providers/123anime.provider.js +240 -0
- package/server/dist/providers/anilight.provider.js +366 -0
- package/server/dist/providers/animepahe.provider.js +531 -0
- package/server/dist/providers/animeya.provider.js +746 -0
- package/server/dist/providers/base-provider.js +45 -0
- package/server/dist/providers/hn.provider.js +473 -0
- package/server/dist/providers/ht.provider.js +439 -0
- package/server/dist/providers/jasmr.provider.js +504 -0
- package/server/dist/providers/kaa.provider.js +388 -0
- package/server/dist/providers/manga/manga.types.js +10 -0
- package/server/dist/providers/manga/mangadex.provider.js +227 -0
- package/server/dist/providers/manga/mangapill.provider.js +232 -0
- package/server/dist/providers/megaplay.provider.js +583 -0
- package/server/dist/providers/op.provider.js +440 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/providers/title-matching.js +234 -0
- package/server/dist/providers/wh.provider.js +600 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +43 -0
- package/server/dist/repositories/mal-cache.repository.js +66 -0
- package/server/dist/repositories/notifications.repository.js +40 -0
- package/server/dist/repositories/queue.repository.js +64 -0
- package/server/dist/repositories/settings.repository.js +15 -0
- package/server/dist/repositories/shows-meta.repository.js +43 -0
- package/server/dist/repositories/temp-show-ids.repository.js +54 -0
- package/server/dist/repositories/watched-episodes.repository.js +54 -0
- package/server/dist/repositories/watchlist.repository.js +74 -0
- package/server/dist/routes/asmr.routes.js +72 -0
- package/server/dist/routes/auth.routes.js +28 -0
- package/server/dist/routes/data.routes.js +50 -0
- package/server/dist/routes/discord-gateway.routes.js +57 -0
- package/server/dist/routes/insights.routes.js +12 -0
- package/server/dist/routes/lan-auth.routes.js +49 -0
- package/server/dist/routes/manga.routes.js +115 -0
- package/server/dist/routes/proxy.routes.js +15 -0
- package/server/dist/routes/radio.routes.js +143 -0
- package/server/dist/routes/settings.routes.js +27 -0
- package/server/dist/routes/tracker.routes.js +143 -0
- package/server/dist/routes/translate.routes.js +90 -0
- package/server/dist/routes/tv.routes.js +843 -0
- package/server/dist/routes/watchlist.routes.js +312 -0
- package/server/dist/server.js +310 -0
- package/server/dist/shipped-defaults.js +8 -0
- package/server/dist/sync-config.js +32 -0
- package/server/dist/sync.js +506 -0
- package/server/dist/utils/cookie.utils.js +17 -0
- package/server/dist/utils/db-utils.js +15 -0
- package/server/dist/utils/env.utils.js +83 -0
- package/server/dist/utils/machine-id.js +46 -0
- package/server/dist/utils/request-context.js +5 -0
- package/server/dist/utils/security.utils.js +99 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAuthRouter = createAuthRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const auth_controller_1 = require("../controllers/auth.controller");
|
|
6
|
+
function createAuthRouter(runSyncSequence) {
|
|
7
|
+
const router = (0, express_1.Router)();
|
|
8
|
+
const controller = new auth_controller_1.AuthController(runSyncSequence);
|
|
9
|
+
router.get('/config-status', controller.getConfigStatus);
|
|
10
|
+
router.get('/google-auth', controller.getGoogleAuthSettings);
|
|
11
|
+
router.post('/google-auth', controller.updateGoogleAuthSettings);
|
|
12
|
+
router.get('/github/status', controller.getGitHubAuthStatus);
|
|
13
|
+
router.post('/github/start', controller.startGitHubDeviceAuth);
|
|
14
|
+
router.get('/github/poll', controller.pollGitHubDeviceAuth);
|
|
15
|
+
router.post('/github/logout', controller.logoutGitHub);
|
|
16
|
+
router.get('/github/auth', controller.getGitHubAuthOverride);
|
|
17
|
+
router.post('/github/auth', controller.updateGitHubAuthSettings);
|
|
18
|
+
router.get('/settings/rclone', controller.getRcloneSettings);
|
|
19
|
+
router.post('/settings/rclone', controller.updateRcloneSettings);
|
|
20
|
+
router.get('/settings/sync', controller.getSyncSettings);
|
|
21
|
+
router.post('/settings/sync', controller.updateSyncProvider);
|
|
22
|
+
router.get('/google', controller.getAuthUrl);
|
|
23
|
+
router.post('/google/login', controller.loginGoogle);
|
|
24
|
+
router.get('/google/callback', controller.handleCallback);
|
|
25
|
+
router.get('/user', controller.getUserProfile);
|
|
26
|
+
router.post('/logout', controller.logout);
|
|
27
|
+
return router;
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDataRouter = createDataRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const data_controller_1 = require("../controllers/data.controller");
|
|
6
|
+
function makeCacheMiddleware(cache, keyFn, ttl, validate = (d) => Array.isArray(d) && d.length > 0) {
|
|
7
|
+
return (req, res, next) => {
|
|
8
|
+
const cacheKey = keyFn(req);
|
|
9
|
+
const cached = cache.get(cacheKey);
|
|
10
|
+
if (cached)
|
|
11
|
+
return res.json(cached);
|
|
12
|
+
const originalJson = res.json.bind(res);
|
|
13
|
+
res.json = (data) => {
|
|
14
|
+
if (validate(data)) {
|
|
15
|
+
if (ttl !== undefined) {
|
|
16
|
+
cache.set(cacheKey, data, ttl);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
cache.set(cacheKey, data);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return originalJson(data);
|
|
23
|
+
};
|
|
24
|
+
next();
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function createDataRouter(apiCache, providers) {
|
|
28
|
+
const router = (0, express_1.Router)();
|
|
29
|
+
const controller = new data_controller_1.DataController(providers);
|
|
30
|
+
router.get('/schedule/:date', makeCacheMiddleware(apiCache, (req) => `schedule-${req.params.date}-${req.query.format || 'TV'}`, 1800), controller.getSchedule);
|
|
31
|
+
router.get('/latest-releases', makeCacheMiddleware(apiCache, (req) => `latest-releases-${req.query.format || 'TV'}-${req.query.page || 1}-${req.query.size || 12}`, 300), controller.getLatestReleases);
|
|
32
|
+
router.get('/search', makeCacheMiddleware(apiCache, (req) => `search-${JSON.stringify(req.query)}`, 1800), controller.search);
|
|
33
|
+
router.get('/mature/search', makeCacheMiddleware(apiCache, (req) => `mature-search-${JSON.stringify(req.query)}`, 600, (d) => !!d && typeof d === 'object' && Array.isArray(d.data)), controller.matureSearch);
|
|
34
|
+
router.get('/mature/resolve', makeCacheMiddleware(apiCache, (req) => `mature-resolve-${req.query.title || ''}`, 3600), controller.resolveMature);
|
|
35
|
+
router.get('/mature/filters', controller.getMatureFilters);
|
|
36
|
+
router.post('/mature/allocate', controller.allocateTempShow);
|
|
37
|
+
router.get('/skip-times/:showId/:episodeNumber', controller.getSkipTimes);
|
|
38
|
+
router.get('/video', controller.getVideo);
|
|
39
|
+
router.get('/episodes', makeCacheMiddleware(apiCache, (req) => `episodes-${req.query.showId || ''}`, 3600), controller.getEpisodes);
|
|
40
|
+
router.get('/seasonal', makeCacheMiddleware(apiCache, (req) => `seasonal-${req.query.format || 'ALL'}-${req.query.page || 1}-${req.query.size || 14}`, 300), controller.getSeasonal);
|
|
41
|
+
router.get('/show-meta/:id', makeCacheMiddleware(apiCache, (req) => `meta-${req.params.id}`, 3600, (d) => !!d), controller.getShowMeta);
|
|
42
|
+
router.get('/popular-list', makeCacheMiddleware(apiCache, (req) => `popular-list-${req.query.sort || 'TRENDING_DESC'}-${req.query.page || 1}-${req.query.size || 20}`, 300), controller.getPopularList);
|
|
43
|
+
router.get('/trending', makeCacheMiddleware(apiCache, () => 'trending', 300), controller.getTrending);
|
|
44
|
+
router.get('/spotlight', makeCacheMiddleware(apiCache, () => 'spotlight', 300), controller.getSpotlight);
|
|
45
|
+
router.get('/home', makeCacheMiddleware(apiCache, (req) => `home-${req.query.format || 'TV'}`, 300), controller.getBatchedHome);
|
|
46
|
+
router.get('/genres-and-tags', controller.getGenresAndTags);
|
|
47
|
+
router.get('/anilist-status', controller.getAnilistStatus);
|
|
48
|
+
router.get('/system-notifications', controller.getSystemNotifications);
|
|
49
|
+
return router;
|
|
50
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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.createDiscordGatewayRouter = createDiscordGatewayRouter;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
9
|
+
const discord_gateway_1 = require("../discord-gateway");
|
|
10
|
+
const env_utils_1 = require("../utils/env.utils");
|
|
11
|
+
const log = logger_1.default.child({ module: 'DiscordGatewayRoutes' });
|
|
12
|
+
function mask(t) {
|
|
13
|
+
if (!t)
|
|
14
|
+
return 'none';
|
|
15
|
+
return t.slice(0, 8) + '...' + t.slice(-4);
|
|
16
|
+
}
|
|
17
|
+
function createDiscordGatewayRouter() {
|
|
18
|
+
const router = (0, express_1.Router)();
|
|
19
|
+
router.get('/discord/gateway/status', (_req, res) => {
|
|
20
|
+
const hasToken = discord_gateway_1.discordGatewayService.hasToken();
|
|
21
|
+
res.json({
|
|
22
|
+
hasToken,
|
|
23
|
+
masked: hasToken ? mask(process.env.DISCORD_GATEWAY_TOKEN) : null,
|
|
24
|
+
enabled: discord_gateway_1.discordGatewayService.serviceEnabled,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
router.post('/discord/gateway/save', async (req, res) => {
|
|
28
|
+
let token = (req.body.token || '').trim();
|
|
29
|
+
if (token.startsWith('"') && token.endsWith('"'))
|
|
30
|
+
token = token.slice(1, -1);
|
|
31
|
+
if (token.length < 30)
|
|
32
|
+
return res.status(400).json({ error: 'Token too short' });
|
|
33
|
+
try {
|
|
34
|
+
await (0, env_utils_1.updateEnvFile)({ DISCORD_GATEWAY_TOKEN: token });
|
|
35
|
+
discord_gateway_1.discordGatewayService.reloadToken();
|
|
36
|
+
log.info(`Discord Gateway token saved ${mask(token)} (not logged)`);
|
|
37
|
+
res.json({ ok: true, masked: mask(token) });
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
log.error({ err: e }, 'Failed to save Discord Gateway token');
|
|
41
|
+
res.status(500).json({ error: 'Failed to save token' });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
router.post('/discord/gateway/remove', async (_req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
await (0, env_utils_1.updateEnvFile)({ DISCORD_GATEWAY_TOKEN: '' });
|
|
47
|
+
discord_gateway_1.discordGatewayService.reloadToken();
|
|
48
|
+
log.info('Discord Gateway token removed');
|
|
49
|
+
res.json({ ok: true });
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
log.error({ err: e }, 'Failed to remove Discord Gateway token');
|
|
53
|
+
res.status(500).json({ error: 'Failed to remove token' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return router;
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createInsightsRouter = createInsightsRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const insights_controller_1 = require("../controllers/insights.controller");
|
|
6
|
+
function createInsightsRouter() {
|
|
7
|
+
const router = (0, express_1.Router)();
|
|
8
|
+
const controller = new insights_controller_1.InsightsController();
|
|
9
|
+
router.get('/insights', controller.getWatchInsights);
|
|
10
|
+
router.get('/insights/genre-cards', controller.getGenreCards);
|
|
11
|
+
return router;
|
|
12
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLanAuthRouter = createLanAuthRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const app_auth_1 = require("../app-auth");
|
|
6
|
+
function createLanAuthRouter() {
|
|
7
|
+
const router = (0, express_1.Router)();
|
|
8
|
+
router.get('/app-status', (req, res) => {
|
|
9
|
+
res.json({
|
|
10
|
+
hasPassword: (0, app_auth_1.hasAppPassword)(),
|
|
11
|
+
isAuthenticated: (0, app_auth_1.isLanAuthenticated)(req),
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
router.post('/app-login', (req, res) => {
|
|
15
|
+
const password = String(req.body?.password || '');
|
|
16
|
+
if (!(0, app_auth_1.hasAppPassword)()) {
|
|
17
|
+
return res.json({ success: true });
|
|
18
|
+
}
|
|
19
|
+
if (!password) {
|
|
20
|
+
return res.status(400).json({ error: 'Password required' });
|
|
21
|
+
}
|
|
22
|
+
if (!(0, app_auth_1.verifyAppPassword)(password)) {
|
|
23
|
+
return res.status(401).json({ error: 'Invalid password' });
|
|
24
|
+
}
|
|
25
|
+
const session = (0, app_auth_1.createLanSession)();
|
|
26
|
+
res.setHeader('Set-Cookie', (0, app_auth_1.buildLanCookie)(session.token, session.expiry));
|
|
27
|
+
res.json({ success: true });
|
|
28
|
+
});
|
|
29
|
+
router.post('/app-logout', (req, res) => {
|
|
30
|
+
(0, app_auth_1.revokeLanSession)((0, app_auth_1.getRequestToken)(req));
|
|
31
|
+
res.setHeader('Set-Cookie', (0, app_auth_1.clearLanCookie)());
|
|
32
|
+
res.json({ success: true });
|
|
33
|
+
});
|
|
34
|
+
router.post('/app-setup', (req, res) => {
|
|
35
|
+
if (!(0, app_auth_1.isLoopbackRequest)(req)) {
|
|
36
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
37
|
+
}
|
|
38
|
+
const password = String(req.body?.password || '');
|
|
39
|
+
(0, app_auth_1.setAppPassword)(password)
|
|
40
|
+
.then(() => {
|
|
41
|
+
(0, app_auth_1.clearAllLanSessions)();
|
|
42
|
+
res.json({ success: true, hasPassword: (0, app_auth_1.hasAppPassword)() });
|
|
43
|
+
})
|
|
44
|
+
.catch((err) => {
|
|
45
|
+
res.status(500).json({ error: 'Failed to update password' });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return router;
|
|
49
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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.createMangaRouter = createMangaRouter;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
9
|
+
const mangadex_provider_1 = require("../providers/manga/mangadex.provider");
|
|
10
|
+
const mangapill_provider_1 = require("../providers/manga/mangapill.provider");
|
|
11
|
+
function makeCacheMiddleware(cache, keyFn, ttl) {
|
|
12
|
+
return (req, res, next) => {
|
|
13
|
+
const cacheKey = keyFn(req);
|
|
14
|
+
const cached = cache.get(cacheKey);
|
|
15
|
+
if (cached)
|
|
16
|
+
return res.json(cached);
|
|
17
|
+
const originalJson = res.json.bind(res);
|
|
18
|
+
res.json = (data) => {
|
|
19
|
+
if (res.statusCode >= 200 && res.statusCode < 400) {
|
|
20
|
+
if (ttl !== undefined)
|
|
21
|
+
cache.set(cacheKey, data, ttl);
|
|
22
|
+
else
|
|
23
|
+
cache.set(cacheKey, data);
|
|
24
|
+
}
|
|
25
|
+
return originalJson(data);
|
|
26
|
+
};
|
|
27
|
+
next();
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const SAFE = ['safe'];
|
|
31
|
+
function parseRatings(req) {
|
|
32
|
+
const mature = req.query.mature === '1';
|
|
33
|
+
const raw = String(req.query.rating || 'safe')
|
|
34
|
+
.split(',')
|
|
35
|
+
.map((s) => s.trim().toLowerCase())
|
|
36
|
+
.filter((s) => ['safe', 'suggestive', 'erotica', 'pornographic'].includes(s));
|
|
37
|
+
if (!mature)
|
|
38
|
+
return SAFE;
|
|
39
|
+
const exact = [...new Set(raw)];
|
|
40
|
+
return exact.length > 0 ? exact : SAFE;
|
|
41
|
+
}
|
|
42
|
+
function createMangaRouter(apiCache) {
|
|
43
|
+
const router = (0, express_1.Router)();
|
|
44
|
+
const dex = new mangadex_provider_1.MangaDexProvider(apiCache);
|
|
45
|
+
const pill = new mangapill_provider_1.MangaPillProvider(apiCache);
|
|
46
|
+
const pick = (name) => (name === 'mangapill' ? pill : dex);
|
|
47
|
+
router.get('/manga/search', makeCacheMiddleware(apiCache, (req) => `route-manga-search-${req.query.provider || ''}-${req.query.q || ''}-${req.query.page || 1}-${req.query.sort || ''}-${req.query.status || ''}-${req.query.type || ''}-${req.query.rating || ''}-${req.query.mature || ''}`, 300), async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const provider = req.query.provider;
|
|
50
|
+
const ratings = parseRatings(req);
|
|
51
|
+
const rawType = req.query.type || '';
|
|
52
|
+
const type = rawType === 'doujinshi' && req.query.mature !== '1' ? '' : rawType;
|
|
53
|
+
const options = {
|
|
54
|
+
query: req.query.q || '',
|
|
55
|
+
page: parseInt(req.query.page) || 1,
|
|
56
|
+
limit: Math.min(parseInt(req.query.limit) || 24, 50),
|
|
57
|
+
sort: req.query.sort || 'popular',
|
|
58
|
+
status: req.query.status || '',
|
|
59
|
+
type,
|
|
60
|
+
ratings,
|
|
61
|
+
};
|
|
62
|
+
const result = await pick(provider).search(options);
|
|
63
|
+
res.json(result);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
logger_1.default.error({ err }, '[Manga] search failed');
|
|
67
|
+
res.json({ items: [], hasNext: false });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
router.get('/manga/info', makeCacheMiddleware(apiCache, (req) => `route-manga-info-${req.query.provider || ''}-${req.query.id || ''}-${req.query.rating || ''}-${req.query.mature || ''}`, 600), async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
const provider = req.query.provider;
|
|
73
|
+
const id = String(req.query.id || '');
|
|
74
|
+
if (!id)
|
|
75
|
+
return res.status(400).json({ error: 'Missing id' });
|
|
76
|
+
const detail = await pick(provider).getDetail(id, parseRatings(req));
|
|
77
|
+
if (!detail)
|
|
78
|
+
return res.status(404).json({ error: 'Not found' });
|
|
79
|
+
res.json(detail);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
logger_1.default.error({ err }, '[Manga] info failed');
|
|
83
|
+
res.status(500).json({ error: 'Failed to load manga' });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
router.get('/manga/chapters', makeCacheMiddleware(apiCache, (req) => `route-manga-chapters-${req.query.provider || ''}-${req.query.id || ''}-${req.query.rating || ''}-${req.query.mature || ''}`, 600), async (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const provider = req.query.provider;
|
|
89
|
+
const id = String(req.query.id || '');
|
|
90
|
+
if (!id)
|
|
91
|
+
return res.status(400).json({ error: 'Missing id' });
|
|
92
|
+
const chapters = await pick(provider).getChapters(id, parseRatings(req));
|
|
93
|
+
res.json({ chapters });
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger_1.default.error({ err }, '[Manga] chapters failed');
|
|
97
|
+
res.json({ chapters: [] });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
router.get('/manga/pages', makeCacheMiddleware(apiCache, (req) => `route-manga-pages-${req.query.provider || ''}-${req.query.id || ''}`, 300), async (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const provider = req.query.provider;
|
|
103
|
+
const id = String(req.query.id || '');
|
|
104
|
+
if (!id)
|
|
105
|
+
return res.status(400).json({ error: 'Missing id' });
|
|
106
|
+
const pages = await pick(provider).getPages(id);
|
|
107
|
+
res.json({ pages });
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
logger_1.default.error({ err }, '[Manga] pages failed');
|
|
111
|
+
res.json({ pages: [] });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return router;
|
|
115
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createProxyRouter = createProxyRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const proxy_controller_1 = require("../controllers/proxy.controller");
|
|
6
|
+
function createProxyRouter() {
|
|
7
|
+
const router = (0, express_1.Router)();
|
|
8
|
+
const controller = new proxy_controller_1.ProxyController();
|
|
9
|
+
router.get('/proxy', controller.handleProxy);
|
|
10
|
+
router.get('/embed-proxy', controller.handleEmbedProxy);
|
|
11
|
+
router.get('/subtitle-proxy', controller.handleSubtitleProxy);
|
|
12
|
+
router.get('/image-proxy', controller.handleImageProxy);
|
|
13
|
+
router.get('/resolve', controller.handleResolve);
|
|
14
|
+
return router;
|
|
15
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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.createRadioRouter = createRadioRouter;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
9
|
+
const LISTEN_MOE_STATIONS = [
|
|
10
|
+
{
|
|
11
|
+
id: 'listenmoe-jpop',
|
|
12
|
+
name: 'LISTEN.moe JPOP',
|
|
13
|
+
streamUrl: 'https://listen.moe/fallback',
|
|
14
|
+
homepage: 'https://listen.moe/',
|
|
15
|
+
tags: 'anime,j-pop,jpop',
|
|
16
|
+
codec: 'MP3',
|
|
17
|
+
source: 'listen.moe',
|
|
18
|
+
gateway: 'wss://listen.moe/gateway_v2',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'listenmoe-kpop',
|
|
22
|
+
name: 'LISTEN.moe KPOP',
|
|
23
|
+
streamUrl: 'https://listen.moe/kpop/fallback',
|
|
24
|
+
homepage: 'https://listen.moe/',
|
|
25
|
+
tags: 'k-pop,kpop',
|
|
26
|
+
codec: 'MP3',
|
|
27
|
+
source: 'listen.moe',
|
|
28
|
+
gateway: 'wss://listen.moe/kpop/gateway_v2',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
const FEATURED_STATIONS = [
|
|
32
|
+
{
|
|
33
|
+
id: 'stereo-anime',
|
|
34
|
+
name: 'stereo anime',
|
|
35
|
+
streamUrl: 'https://radio.stereoanime.com/listen/stereoanime/128',
|
|
36
|
+
tags: 'anime',
|
|
37
|
+
codec: 'MP3',
|
|
38
|
+
bitrate: 128,
|
|
39
|
+
source: 'radio-browser',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'jpop-sakura',
|
|
43
|
+
name: 'J-Pop Sakura (asia DREAM radio)',
|
|
44
|
+
streamUrl: 'https://quincy.torontocast.com:2070/stream.mp3',
|
|
45
|
+
tags: 'j-pop,jpop',
|
|
46
|
+
codec: 'MP3',
|
|
47
|
+
bitrate: 128,
|
|
48
|
+
source: 'radio-browser',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'n-kpop',
|
|
52
|
+
name: '0 N - K-Pop on Radio',
|
|
53
|
+
streamUrl: 'https://0n-kpop.radionetz.de/0n-kpop.mp3',
|
|
54
|
+
tags: 'k-pop,kpop',
|
|
55
|
+
codec: 'MP3',
|
|
56
|
+
bitrate: 128,
|
|
57
|
+
source: 'radio-browser',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'bigb-kpop',
|
|
61
|
+
name: 'Big B Radio - Kpop',
|
|
62
|
+
streamUrl: 'https://antares.dribbcast.com/proxy/kpop?mp=/s',
|
|
63
|
+
tags: 'k-pop,kpop',
|
|
64
|
+
codec: 'MP3',
|
|
65
|
+
bitrate: 192,
|
|
66
|
+
source: 'radio-browser',
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
const RB_MIRRORS = ['https://de1.api.radio-browser.info', 'https://nl1.api.radio-browser.info'];
|
|
70
|
+
async function rbFetch(path) {
|
|
71
|
+
let lastErr = null;
|
|
72
|
+
for (const base of RB_MIRRORS) {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(`${base}${path}`, {
|
|
75
|
+
headers: { 'User-Agent': 'dango/1.0' },
|
|
76
|
+
signal: AbortSignal.timeout(10000),
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new Error(`radio-browser ${res.status}`);
|
|
80
|
+
return (await res.json());
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
lastErr = err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw lastErr;
|
|
87
|
+
}
|
|
88
|
+
function toStation(s) {
|
|
89
|
+
const url = s.url_resolved || '';
|
|
90
|
+
if (!url.startsWith('https://'))
|
|
91
|
+
return null;
|
|
92
|
+
return {
|
|
93
|
+
id: s.stationuuid || url,
|
|
94
|
+
name: s.name || 'Unknown station',
|
|
95
|
+
streamUrl: url,
|
|
96
|
+
homepage: s.homepage || undefined,
|
|
97
|
+
favicon: s.favicon && s.favicon.startsWith('https://') ? s.favicon : undefined,
|
|
98
|
+
tags: s.tags || undefined,
|
|
99
|
+
codec: s.codec || undefined,
|
|
100
|
+
bitrate: s.bitrate || undefined,
|
|
101
|
+
source: 'radio-browser',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function createRadioRouter(apiCache) {
|
|
105
|
+
const router = (0, express_1.Router)();
|
|
106
|
+
router.get('/radio/stations', async (_req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
const cached = apiCache.get('route-radio-stations');
|
|
109
|
+
if (cached)
|
|
110
|
+
return res.json({ stations: cached });
|
|
111
|
+
const stations = [...LISTEN_MOE_STATIONS, ...FEATURED_STATIONS];
|
|
112
|
+
apiCache.set('route-radio-stations', stations, 3600);
|
|
113
|
+
res.json({ stations });
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
logger_1.default.error({ err }, '[Radio] stations failed');
|
|
117
|
+
res.json({ stations: LISTEN_MOE_STATIONS });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
router.get('/radio/search', async (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const q = String(req.query.q || '').trim();
|
|
123
|
+
if (!q)
|
|
124
|
+
return res.json({ stations: [] });
|
|
125
|
+
const cacheKey = `route-radio-search-${q.toLowerCase()}`;
|
|
126
|
+
const cached = apiCache.get(cacheKey);
|
|
127
|
+
if (cached)
|
|
128
|
+
return res.json({ stations: cached });
|
|
129
|
+
const raw = (await rbFetch(`/json/stations/search?name=${encodeURIComponent(q)}&hidebroken=true&order=clickcount&reverse=true&limit=25`));
|
|
130
|
+
const stations = raw
|
|
131
|
+
.map(toStation)
|
|
132
|
+
.filter((s) => s !== null)
|
|
133
|
+
.slice(0, 25);
|
|
134
|
+
apiCache.set(cacheKey, stations, 300);
|
|
135
|
+
res.json({ stations });
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
logger_1.default.error({ err }, '[Radio] search failed');
|
|
139
|
+
res.json({ stations: [] });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return router;
|
|
143
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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.createSettingsRouter = createSettingsRouter;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const settings_controller_1 = require("../controllers/settings.controller");
|
|
9
|
+
const multer_1 = __importDefault(require("multer"));
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
function createSettingsRouter(getDb, initializeDatabase, setDb) {
|
|
12
|
+
const router = (0, express_1.Router)();
|
|
13
|
+
const controller = new settings_controller_1.SettingsController();
|
|
14
|
+
router.get('/settings', controller.getSettings);
|
|
15
|
+
router.post('/settings', controller.updateSettings);
|
|
16
|
+
router.get('/backup-db', controller.backupDatabase);
|
|
17
|
+
router.get('/installation-id', controller.getInstallationId);
|
|
18
|
+
const restoreStorage = (0, multer_1.default)({
|
|
19
|
+
storage: multer_1.default.diskStorage({
|
|
20
|
+
destination: (_req, _f, cb) => cb(null, config_1.CONFIG.ROOT),
|
|
21
|
+
filename: (_r, _f, cb) => cb(null, `restore_temp.db`),
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
router.post('/restore-db', restoreStorage.single('dbfile'), (req, res) => controller.restoreDatabase(req, res, getDb(), initializeDatabase, setDb));
|
|
25
|
+
router.post('/import/mal-xml', (0, multer_1.default)().single('xmlfile'), controller.importMalXml);
|
|
26
|
+
return router;
|
|
27
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTrackerRouter = createTrackerRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const anilist_tracker_1 = require("../lib/tracker/anilist-tracker");
|
|
6
|
+
const sync_service_1 = require("../lib/tracker/sync.service");
|
|
7
|
+
const settings_repository_1 = require("../repositories/settings.repository");
|
|
8
|
+
const sync_1 = require("../sync");
|
|
9
|
+
const TOKEN_KEY = 'tracker_anilist_token';
|
|
10
|
+
const USER_KEY = 'tracker_anilist_user';
|
|
11
|
+
const CLIENT_ID_KEY = 'tracker_anilist_client_id';
|
|
12
|
+
function createTrackerRouter() {
|
|
13
|
+
const router = (0, express_1.Router)();
|
|
14
|
+
router.get('/tracker/anilist/callback', (_req, res) => {
|
|
15
|
+
res.type('html').send(`<!doctype html>
|
|
16
|
+
<html><head><meta charset="utf-8"><title>AniList — completing login</title></head>
|
|
17
|
+
<body style="font-family:system-ui;padding:24px;background:#0b1426;color:#e5eefc"><p>Completing AniList login…</p>
|
|
18
|
+
<script>
|
|
19
|
+
(function(){
|
|
20
|
+
var qs = new URLSearchParams(location.search);
|
|
21
|
+
var state = qs.get('state');
|
|
22
|
+
var hash = location.hash || '';
|
|
23
|
+
var hp = new URLSearchParams(hash.slice(1));
|
|
24
|
+
if (!state) state = hp.get('state');
|
|
25
|
+
var token = hp.get('access_token');
|
|
26
|
+
var error = hp.get('error') || qs.get('error');
|
|
27
|
+
var frontend = location.origin + '/trackers';
|
|
28
|
+
if (state) {
|
|
29
|
+
try {
|
|
30
|
+
var candidate = decodeURIComponent(state);
|
|
31
|
+
if (candidate.startsWith('/') && !candidate.startsWith('//')) {
|
|
32
|
+
frontend = location.origin + candidate;
|
|
33
|
+
} else {
|
|
34
|
+
var parsed = new URL(candidate, location.origin);
|
|
35
|
+
if (parsed.origin === location.origin) {
|
|
36
|
+
frontend = parsed.href;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch(e) {}
|
|
40
|
+
}
|
|
41
|
+
if (frontend.indexOf('/api/tracker/anilist/callback') !== -1) {
|
|
42
|
+
frontend = location.origin + '/trackers';
|
|
43
|
+
}
|
|
44
|
+
if (error) {
|
|
45
|
+
location.replace(frontend + (frontend.indexOf('?') !== -1 ? '&' : '?') + 'anilist=error&reason=' + encodeURIComponent(error));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (token) {
|
|
49
|
+
location.replace(frontend + hash);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
var code = qs.get('code');
|
|
53
|
+
if (code) {
|
|
54
|
+
location.replace(frontend + (frontend.indexOf('?') !== -1 ? '&' : '?') + 'anilist=error&reason=code_flow_removed');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
location.replace(frontend + (frontend.indexOf('?') !== -1 ? '&' : '?') + 'anilist=error&reason=no_token');
|
|
58
|
+
})();
|
|
59
|
+
</script></body></html>`);
|
|
60
|
+
});
|
|
61
|
+
router.get('/tracker/status', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
const tokenRow = await settings_repository_1.SettingsRepository.getByKey(req.db, TOKEN_KEY);
|
|
64
|
+
const userRow = await settings_repository_1.SettingsRepository.getByKey(req.db, USER_KEY);
|
|
65
|
+
let user = null;
|
|
66
|
+
if (userRow?.value) {
|
|
67
|
+
try {
|
|
68
|
+
user = JSON.parse(userRow.value);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
user = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
res.json({
|
|
75
|
+
anilist: { connected: !!tokenRow?.value, user },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
res.status(500).json({ error: 'Failed to read tracker status' });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
router.post('/tracker/anilist/auth', async (req, res) => {
|
|
83
|
+
const { token } = req.body ?? {};
|
|
84
|
+
const accessToken = typeof token === 'string' ? token.trim() : '';
|
|
85
|
+
if (!accessToken) {
|
|
86
|
+
return res.status(400).json({ error: 'Access token is required' });
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const tracker = new anilist_tracker_1.AniListTracker(accessToken);
|
|
90
|
+
const viewer = await tracker.getViewer();
|
|
91
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
92
|
+
settings_repository_1.SettingsRepository.upsert(tx, TOKEN_KEY, accessToken);
|
|
93
|
+
settings_repository_1.SettingsRepository.upsert(tx, USER_KEY, JSON.stringify(viewer));
|
|
94
|
+
});
|
|
95
|
+
res.json({ success: true, user: viewer });
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : 'Authentication failed';
|
|
99
|
+
res.status(401).json({ error: message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
router.post('/tracker/anilist/disconnect', async (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
105
|
+
settings_repository_1.SettingsRepository.upsert(tx, TOKEN_KEY, '');
|
|
106
|
+
settings_repository_1.SettingsRepository.upsert(tx, USER_KEY, '');
|
|
107
|
+
});
|
|
108
|
+
res.json({ success: true });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
res.status(500).json({ error: 'Failed to disconnect' });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
router.post('/tracker/sync', async (req, res) => {
|
|
115
|
+
const { provider = 'anilist' } = req.body ?? {};
|
|
116
|
+
if (provider !== 'anilist') {
|
|
117
|
+
return res.status(400).json({ error: `Provider "${provider}" is not supported yet` });
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const summary = await (0, sync_service_1.syncAniList)(req.db);
|
|
121
|
+
res.json({ success: true, summary });
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const message = err instanceof Error ? err.message : 'Sync failed';
|
|
125
|
+
res.status(500).json({ error: message });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
router.post('/tracker/anilist/import', async (req, res) => {
|
|
129
|
+
const { username } = req.body ?? {};
|
|
130
|
+
if (!username || typeof username !== 'string') {
|
|
131
|
+
return res.status(400).json({ error: 'Username is required' });
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const count = await (0, sync_service_1.importFromUsername)(req.db, username.trim());
|
|
135
|
+
res.json({ success: true, count });
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const message = err instanceof Error ? err.message : 'Import failed';
|
|
139
|
+
res.status(500).json({ error: message });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return router;
|
|
143
|
+
}
|