@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,126 @@
|
|
|
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.rcloneService = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const logger_1 = __importDefault(require("./logger"));
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
class RcloneService {
|
|
11
|
+
activeRemote = null;
|
|
12
|
+
executeCommand(args) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
(0, child_process_1.execFile)('rclone', args, (err, stdout, stderr) => {
|
|
15
|
+
if (err) {
|
|
16
|
+
if (stderr)
|
|
17
|
+
logger_1.default.warn({ stderr }, 'Rclone command warning');
|
|
18
|
+
return reject(new Error(stderr || err.message));
|
|
19
|
+
}
|
|
20
|
+
resolve(stdout.trim());
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
executeRcloneArgs(args) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const process = (0, child_process_1.spawn)('rclone', args, { stdio: 'ignore' });
|
|
27
|
+
process.on('close', (code) => {
|
|
28
|
+
if (code === 0)
|
|
29
|
+
resolve();
|
|
30
|
+
else
|
|
31
|
+
reject(new Error(`Rclone exited with code ${code}`));
|
|
32
|
+
});
|
|
33
|
+
process.on('error', (err) => reject(err));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async listRemotes() {
|
|
37
|
+
try {
|
|
38
|
+
const remotesStr = await this.executeCommand(['listremotes']);
|
|
39
|
+
return remotesStr
|
|
40
|
+
.split('\n')
|
|
41
|
+
.map((r) => r.trim())
|
|
42
|
+
.filter((r) => r !== '')
|
|
43
|
+
.map((r) => r.replace(/:$/, ''));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async init() {
|
|
50
|
+
try {
|
|
51
|
+
await this.executeCommand(['version']);
|
|
52
|
+
const remotes = await this.listRemotes();
|
|
53
|
+
if (config_1.CONFIG.RCLONE_REMOTE) {
|
|
54
|
+
const found = remotes.find((r) => r.toLowerCase() === config_1.CONFIG.RCLONE_REMOTE?.toLowerCase());
|
|
55
|
+
if (found) {
|
|
56
|
+
this.activeRemote = found;
|
|
57
|
+
logger_1.default.info(`Rclone initialized with manual remote: ${this.activeRemote}`);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
logger_1.default.warn(`Configured RCLONE_REMOTE '${config_1.CONFIG.RCLONE_REMOTE}' not found in rclone listremotes.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (remotes.length > 0 && !config_1.CONFIG.RCLONE_REMOTE) {
|
|
65
|
+
logger_1.default.info({ remotes }, 'Rclone available but no manual remote is configured in settings.');
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logger_1.default.warn({ err: error }, 'Rclone initialization failed');
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
isActive() {
|
|
76
|
+
return this.activeRemote !== null;
|
|
77
|
+
}
|
|
78
|
+
getRemoteName() {
|
|
79
|
+
return this.activeRemote || 'unknown';
|
|
80
|
+
}
|
|
81
|
+
async downloadFile(remoteFolder, fileName, localPath) {
|
|
82
|
+
if (!this.activeRemote)
|
|
83
|
+
throw new Error('Rclone not active');
|
|
84
|
+
const remotePath = `${this.activeRemote}:${remoteFolder}/${fileName}`;
|
|
85
|
+
await this.executeRcloneArgs(['copyto', remotePath, localPath]);
|
|
86
|
+
}
|
|
87
|
+
async uploadFile(localPath, remoteFolder, fileName) {
|
|
88
|
+
if (!this.activeRemote)
|
|
89
|
+
throw new Error('Rclone not active');
|
|
90
|
+
const remotePath = `${this.activeRemote}:${remoteFolder}/${fileName}`;
|
|
91
|
+
await this.executeRcloneArgs(['copyto', localPath, remotePath]);
|
|
92
|
+
}
|
|
93
|
+
executeRcloneArgsWithOutput(args) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const process = (0, child_process_1.spawn)('rclone', args, { stdio: 'pipe' });
|
|
96
|
+
let stdout = '';
|
|
97
|
+
let stderr = '';
|
|
98
|
+
process.stdout?.on('data', (data) => (stdout += data));
|
|
99
|
+
process.stderr?.on('data', (data) => (stderr += data));
|
|
100
|
+
process.on('close', (code) => {
|
|
101
|
+
if (code === 0)
|
|
102
|
+
resolve(stdout.trim());
|
|
103
|
+
else {
|
|
104
|
+
if (stderr)
|
|
105
|
+
logger_1.default.warn({ stderr }, 'Rclone command warning');
|
|
106
|
+
reject(new Error(stderr || `Rclone exited with code ${code}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
process.on('error', (err) => reject(err));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async fileExists(remoteFolder, fileName) {
|
|
113
|
+
if (!this.activeRemote)
|
|
114
|
+
return false;
|
|
115
|
+
try {
|
|
116
|
+
const remotePath = `${this.activeRemote}:${remoteFolder}/${fileName}`;
|
|
117
|
+
const output = await this.executeRcloneArgsWithOutput(['lsjson', remotePath]);
|
|
118
|
+
const json = JSON.parse(output);
|
|
119
|
+
return json && json.length > 0;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.rcloneService = new RcloneService();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InsightsRepository = void 0;
|
|
4
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
5
|
+
exports.InsightsRepository = {
|
|
6
|
+
getCoreStats: (db) => (0, db_utils_1.dbGet)(db, `SELECT
|
|
7
|
+
(SELECT SUM(currentTime) FROM watched_episodes) as totalSeconds,
|
|
8
|
+
(SELECT COUNT(*) FROM watched_episodes) as totalEpisodes,
|
|
9
|
+
(SELECT COUNT(DISTINCT showId) FROM watched_episodes) as totalAnime,
|
|
10
|
+
(SELECT COUNT(*) FROM watchlist WHERE status = 'Completed') as completedCount,
|
|
11
|
+
(SELECT COUNT(*) FROM watchlist) as totalWatchlist`),
|
|
12
|
+
getActivityGrid: (db) => (0, db_utils_1.dbAll)(db, `SELECT date(watchedAt) as day, COUNT(*) as count FROM watched_episodes GROUP BY day`),
|
|
13
|
+
getHourlyDist: (db) => (0, db_utils_1.dbAll)(db, `SELECT strftime('%H', watchedAt) as hour, COUNT(*) as count FROM watched_episodes GROUP BY hour`),
|
|
14
|
+
getSeasonality: (db) => (0, db_utils_1.dbAll)(db, `SELECT strftime('%m', watchedAt) as month, SUM(currentTime) as seconds FROM watched_episodes GROUP BY month`),
|
|
15
|
+
getAllWatches: (db) => (0, db_utils_1.dbAll)(db, 'SELECT watchedAt, currentTime FROM watched_episodes ORDER BY watchedAt ASC'),
|
|
16
|
+
getWatchedShowsMeta: (db) => (0, db_utils_1.dbAll)(db, `SELECT DISTINCT sm.id, sm.genres, sm.popularityScore
|
|
17
|
+
FROM shows_meta sm
|
|
18
|
+
JOIN watched_episodes we ON sm.id = we.showId`),
|
|
19
|
+
getDroppedShows: (db) => (0, db_utils_1.dbAll)(db, `SELECT w.id, w.name, MAX(we.watchedAt) as lastActivity
|
|
20
|
+
FROM watchlist w
|
|
21
|
+
JOIN watched_episodes we ON w.id = we.showId
|
|
22
|
+
WHERE w.status = 'Watching'
|
|
23
|
+
GROUP BY w.id
|
|
24
|
+
HAVING lastActivity < date('now', '-90 days')`),
|
|
25
|
+
getCompletionVelocities: (db) => (0, db_utils_1.dbAll)(db, `SELECT
|
|
26
|
+
(julianday(MAX(we.watchedAt)) - julianday(MIN(we.watchedAt))) as daysToFinish
|
|
27
|
+
FROM watchlist w
|
|
28
|
+
JOIN watched_episodes we ON w.id = we.showId
|
|
29
|
+
WHERE w.status = 'Completed'
|
|
30
|
+
GROUP BY w.id`),
|
|
31
|
+
getWatchedEpisodesWithMeta: (db) => (0, db_utils_1.dbAll)(db, `SELECT
|
|
32
|
+
we.showId,
|
|
33
|
+
we.currentTime,
|
|
34
|
+
we.duration,
|
|
35
|
+
sm.genres,
|
|
36
|
+
sm.popularityScore,
|
|
37
|
+
sm.name,
|
|
38
|
+
sm.nativeName,
|
|
39
|
+
sm.englishName,
|
|
40
|
+
sm.thumbnail
|
|
41
|
+
FROM watched_episodes we
|
|
42
|
+
JOIN shows_meta sm ON we.showId = sm.id`),
|
|
43
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
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.MAL_CACHE_MAX_ROWS = void 0;
|
|
7
|
+
exports.malCacheGet = malCacheGet;
|
|
8
|
+
exports.malCachePut = malCachePut;
|
|
9
|
+
exports.malCachePruneExpired = malCachePruneExpired;
|
|
10
|
+
exports.malCacheDelete = malCacheDelete;
|
|
11
|
+
exports.malCacheStore = malCacheStore;
|
|
12
|
+
const node_sqlite_1 = require("node:sqlite");
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const config_1 = require("../config");
|
|
16
|
+
const db_1 = require("../db");
|
|
17
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
18
|
+
exports.MAL_CACHE_MAX_ROWS = 5000;
|
|
19
|
+
const DEFAULT_TTL_SECONDS = 6 * 3600;
|
|
20
|
+
function cacheFile() {
|
|
21
|
+
return path_1.default.join(config_1.CONFIG.ROOT, config_1.CONFIG.IS_DEV ? 'mal-cache.dev.db' : 'mal-cache.db');
|
|
22
|
+
}
|
|
23
|
+
let handle = null;
|
|
24
|
+
function db() {
|
|
25
|
+
if (!handle) {
|
|
26
|
+
const file = cacheFile();
|
|
27
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
28
|
+
handle = new db_1.DatabaseWrapper(file, new node_sqlite_1.DatabaseSync(file));
|
|
29
|
+
(0, db_utils_1.dbRun)(handle, `CREATE TABLE IF NOT EXISTS mal_cache (key TEXT PRIMARY KEY, payload TEXT NOT NULL, fetched_at TEXT NOT NULL, expires_at TEXT NOT NULL)`);
|
|
30
|
+
}
|
|
31
|
+
return handle;
|
|
32
|
+
}
|
|
33
|
+
function malCacheGet(key) {
|
|
34
|
+
const row = (0, db_utils_1.dbGet)(db(), 'SELECT payload, fetched_at AS fetchedAt, expires_at AS expiresAt FROM mal_cache WHERE key = ?', [key]);
|
|
35
|
+
if (!row)
|
|
36
|
+
return null;
|
|
37
|
+
return { payload: row.payload, fresh: Date.now() < Date.parse(row.expiresAt) };
|
|
38
|
+
}
|
|
39
|
+
function malCachePut(key, payload, ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
40
|
+
const database = db();
|
|
41
|
+
const now = new Date();
|
|
42
|
+
(0, db_utils_1.dbRun)(database, 'INSERT OR REPLACE INTO mal_cache (key, payload, fetched_at, expires_at) VALUES (?, ?, ?, ?)', [key, payload, now.toISOString(), new Date(now.getTime() + ttlSeconds * 1000).toISOString()]);
|
|
43
|
+
const count = (0, db_utils_1.dbGet)(database, 'SELECT COUNT(*) AS rows FROM mal_cache')?.rows ?? 0;
|
|
44
|
+
if (count < exports.MAL_CACHE_MAX_ROWS)
|
|
45
|
+
return;
|
|
46
|
+
malCachePruneExpired();
|
|
47
|
+
const remaining = (0, db_utils_1.dbGet)(database, 'SELECT COUNT(*) AS rows FROM mal_cache')?.rows ?? 0;
|
|
48
|
+
if (remaining < exports.MAL_CACHE_MAX_ROWS)
|
|
49
|
+
return;
|
|
50
|
+
(0, db_utils_1.dbRun)(database, `DELETE FROM mal_cache WHERE key NOT IN (SELECT key FROM mal_cache ORDER BY fetched_at DESC LIMIT ${exports.MAL_CACHE_MAX_ROWS})`);
|
|
51
|
+
}
|
|
52
|
+
function malCachePruneExpired() {
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
const row = (0, db_utils_1.dbGet)(db(), 'SELECT COUNT(*) AS rows FROM mal_cache WHERE expires_at < ?', [now]);
|
|
55
|
+
(0, db_utils_1.dbRun)(db(), 'DELETE FROM mal_cache WHERE expires_at < ?', [now]);
|
|
56
|
+
return row?.rows ?? 0;
|
|
57
|
+
}
|
|
58
|
+
function malCacheDelete(key) {
|
|
59
|
+
(0, db_utils_1.dbRun)(db(), 'DELETE FROM mal_cache WHERE key = ?', [key]);
|
|
60
|
+
}
|
|
61
|
+
function malCacheStore() {
|
|
62
|
+
return {
|
|
63
|
+
get: (key) => malCacheGet(key),
|
|
64
|
+
put: (key, payload, ttl) => malCachePut(key, payload, ttl),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotificationsRepository = void 0;
|
|
4
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
5
|
+
exports.NotificationsRepository = {
|
|
6
|
+
getDismissedByShow: (db, showId) => (0, db_utils_1.dbAll)(db, 'SELECT episodeNumber FROM dismissed_notifications WHERE showId = ?', [showId]),
|
|
7
|
+
getDiscoveredByShow: (db, showId) => (0, db_utils_1.dbAll)(db, 'SELECT episodeNumber FROM discovered_notifications WHERE showId = ?', [showId]),
|
|
8
|
+
addDiscovered: (db, showId, episodeNumber) => (0, db_utils_1.dbRun)(db, 'INSERT OR IGNORE INTO discovered_notifications (showId, episodeNumber) VALUES (?, ?)', [showId, episodeNumber]),
|
|
9
|
+
addDismissed: (db, showId, episodeNumber) => (0, db_utils_1.dbRun)(db, 'INSERT OR IGNORE INTO dismissed_notifications (showId, episodeNumber) VALUES (?, ?)', [showId, episodeNumber]),
|
|
10
|
+
dismissFromDiscovered: (db, showId) => {
|
|
11
|
+
if (showId) {
|
|
12
|
+
return (0, db_utils_1.dbRun)(db, 'INSERT OR IGNORE INTO dismissed_notifications (showId, episodeNumber) SELECT showId, episodeNumber FROM discovered_notifications WHERE showId = ?', [showId]);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
return (0, db_utils_1.dbRun)(db, 'INSERT OR IGNORE INTO dismissed_notifications (showId, episodeNumber) SELECT showId, episodeNumber FROM discovered_notifications');
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
deleteByShow: (db, showId) => Promise.all([
|
|
19
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM dismissed_notifications WHERE showId = ?', [showId]),
|
|
20
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM discovered_notifications WHERE showId = ?', [showId]),
|
|
21
|
+
]),
|
|
22
|
+
deleteSpecificDismissed: (db, showId, episodeNumber) => (0, db_utils_1.dbRun)(db, 'DELETE FROM dismissed_notifications WHERE showId = ? AND episodeNumber = ?', [
|
|
23
|
+
showId,
|
|
24
|
+
episodeNumber,
|
|
25
|
+
]),
|
|
26
|
+
cleanupWatchedNotifications: (db) => Promise.all([
|
|
27
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM dismissed_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = dismissed_notifications.showId AND we.episodeNumber = dismissed_notifications.episodeNumber)'),
|
|
28
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM discovered_notifications WHERE EXISTS (SELECT 1 FROM watched_episodes we WHERE we.showId = discovered_notifications.showId AND we.episodeNumber = discovered_notifications.episodeNumber)'),
|
|
29
|
+
]),
|
|
30
|
+
hasAnyDiscovered: (db) => Promise.resolve((0, db_utils_1.dbGet)(db, 'SELECT COUNT(*) as count FROM discovered_notifications')).then((row) => (row?.count || 0) > 0),
|
|
31
|
+
hasActiveNotifications: (db) => Promise.resolve((0, db_utils_1.dbGet)(db, `SELECT COUNT(*) as count FROM discovered_notifications dn
|
|
32
|
+
WHERE NOT EXISTS (
|
|
33
|
+
SELECT 1 FROM dismissed_notifications dn2
|
|
34
|
+
WHERE dn2.showId = dn.showId AND dn2.episodeNumber = dn.episodeNumber
|
|
35
|
+
)
|
|
36
|
+
AND NOT EXISTS (
|
|
37
|
+
SELECT 1 FROM watched_episodes we
|
|
38
|
+
WHERE we.showId = dn.showId AND we.episodeNumber = dn.episodeNumber
|
|
39
|
+
)`)).then((row) => (row?.count || 0) > 0),
|
|
40
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueueRepository = void 0;
|
|
4
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
5
|
+
exports.QueueRepository = {
|
|
6
|
+
getAll: (db) => (0, db_utils_1.dbAll)(db, `SELECT
|
|
7
|
+
q.id,
|
|
8
|
+
q.showId,
|
|
9
|
+
q.episodeNumber,
|
|
10
|
+
q.queue_order,
|
|
11
|
+
COALESCE(NULLIF(sm.name, ''), w.name) as name,
|
|
12
|
+
COALESCE(NULLIF(sm.thumbnail, ''), NULLIF(w.thumbnail, ''), '') as thumbnail,
|
|
13
|
+
COALESCE(NULLIF(sm.nativeName, ''), w.nativeName) as nativeName,
|
|
14
|
+
COALESCE(NULLIF(sm.englishName, ''), w.englishName) as englishName,
|
|
15
|
+
COALESCE(NULLIF(sm.type, ''), w.type) as type
|
|
16
|
+
FROM queue q
|
|
17
|
+
LEFT JOIN shows_meta sm ON q.showId = sm.id
|
|
18
|
+
LEFT JOIN watchlist w ON q.showId = w.id
|
|
19
|
+
ORDER BY q.queue_order ASC, q.id ASC`),
|
|
20
|
+
getByEpisode: (db, showId, episodeNumber) => (0, db_utils_1.dbGet)(db, 'SELECT * FROM queue WHERE showId = ? AND episodeNumber = ?', [
|
|
21
|
+
showId,
|
|
22
|
+
episodeNumber,
|
|
23
|
+
]),
|
|
24
|
+
getByShow: (db, showId) => (0, db_utils_1.dbAll)(db, 'SELECT * FROM queue WHERE showId = ? ORDER BY queue_order ASC', [showId]),
|
|
25
|
+
getMaxOrder: async (db) => {
|
|
26
|
+
const row = await (0, db_utils_1.dbGet)(db, 'SELECT COALESCE(MAX(queue_order), -1) as maxOrder FROM queue');
|
|
27
|
+
return row?.maxOrder ?? -1;
|
|
28
|
+
},
|
|
29
|
+
addToEnd: (db, showId, episodeNumber) => (0, db_utils_1.dbRun)(db, 'INSERT INTO queue (showId, episodeNumber, queue_order) VALUES (?, ?, (SELECT COALESCE(MAX(queue_order), -1) + 1 FROM queue))', [showId, episodeNumber]),
|
|
30
|
+
removeEpisode: (db, showId, episodeNumber) => (0, db_utils_1.dbRun)(db, 'DELETE FROM queue WHERE showId = ? AND episodeNumber = ?', [showId, episodeNumber]),
|
|
31
|
+
addManyToEnd: async (db, episodes) => {
|
|
32
|
+
if (episodes.length === 0)
|
|
33
|
+
return;
|
|
34
|
+
let nextOrder = (await exports.QueueRepository.getMaxOrder(db)) + 1;
|
|
35
|
+
for (const episode of episodes) {
|
|
36
|
+
const existing = await exports.QueueRepository.getByEpisode(db, episode.showId, episode.episodeNumber);
|
|
37
|
+
if (existing)
|
|
38
|
+
continue;
|
|
39
|
+
await (0, db_utils_1.dbRun)(db, 'INSERT INTO queue (showId, episodeNumber, queue_order) VALUES (?, ?, ?)', [
|
|
40
|
+
episode.showId,
|
|
41
|
+
episode.episodeNumber,
|
|
42
|
+
nextOrder,
|
|
43
|
+
]);
|
|
44
|
+
nextOrder += 1;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
removeMany: (db, showId, episodeNumbers) => {
|
|
48
|
+
if (episodeNumbers.length === 0)
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
const placeholders = episodeNumbers.map(() => '?').join(', ');
|
|
51
|
+
return (0, db_utils_1.dbRun)(db, `DELETE FROM queue WHERE showId = ? AND episodeNumber IN (${placeholders})`, [
|
|
52
|
+
showId,
|
|
53
|
+
...episodeNumbers,
|
|
54
|
+
]);
|
|
55
|
+
},
|
|
56
|
+
clear: (db) => (0, db_utils_1.dbRun)(db, 'DELETE FROM queue'),
|
|
57
|
+
reorder: (db, items) => Promise.all(items.map((item, index) => {
|
|
58
|
+
if (item.id !== undefined) {
|
|
59
|
+
return (0, db_utils_1.dbRun)(db, 'UPDATE queue SET queue_order = ? WHERE id = ?', [index, item.id]);
|
|
60
|
+
}
|
|
61
|
+
return (0, db_utils_1.dbRun)(db, 'UPDATE queue SET queue_order = ? WHERE showId = ? AND episodeNumber = ?', [index, item.showId, item.episodeNumber]);
|
|
62
|
+
})),
|
|
63
|
+
cleanupOrphanedShowsMeta: (db) => (0, db_utils_1.dbRun)(db, 'DELETE FROM shows_meta WHERE id NOT IN (SELECT id FROM watchlist) AND id NOT IN (SELECT showId FROM queue)'),
|
|
64
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SettingsRepository = void 0;
|
|
4
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
5
|
+
exports.SettingsRepository = {
|
|
6
|
+
getByKey: (db, key) => (0, db_utils_1.dbGet)(db, 'SELECT value FROM settings WHERE key = ?', [key]),
|
|
7
|
+
upsert: (db, key, value) => (0, db_utils_1.dbRun)(db, 'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)', [key, value]),
|
|
8
|
+
deleteByKey: (db, key) => (0, db_utils_1.dbRun)(db, 'DELETE FROM settings WHERE key = ?', [key]),
|
|
9
|
+
clearWatchlist: (db) => (0, db_utils_1.dbRun)(db, 'DELETE FROM watchlist'),
|
|
10
|
+
upsertWatchlistBatch: (db, shows) => {
|
|
11
|
+
for (const show of shows) {
|
|
12
|
+
(0, db_utils_1.dbRun)(db, 'INSERT OR REPLACE INTO watchlist (id, name, thumbnail, status) VALUES (?, ?, ?, ?)', [show.id, show.name, show.thumbnail ?? null, show.status]);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ShowsMetaRepository = void 0;
|
|
4
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
5
|
+
exports.ShowsMetaRepository = {
|
|
6
|
+
getById: (db, id) => (0, db_utils_1.dbGet)(db, 'SELECT * FROM shows_meta WHERE id = ?', [id]),
|
|
7
|
+
getStatus: async (db, id) => {
|
|
8
|
+
const row = await (0, db_utils_1.dbGet)(db, 'SELECT status FROM shows_meta WHERE id = ?', [
|
|
9
|
+
id,
|
|
10
|
+
]);
|
|
11
|
+
return row?.status;
|
|
12
|
+
},
|
|
13
|
+
upsert: (db, data) => (0, db_utils_1.dbRun)(db, `INSERT INTO shows_meta (id, name, thumbnail, nativeName, englishName, genres, popularityScore, status, episodeCount, type, anilistId, isAdult)
|
|
14
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
15
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
16
|
+
name = COALESCE(NULLIF(EXCLUDED.name, ''), shows_meta.name),
|
|
17
|
+
thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), shows_meta.thumbnail),
|
|
18
|
+
nativeName = COALESCE(NULLIF(EXCLUDED.nativeName, ''), shows_meta.nativeName),
|
|
19
|
+
englishName = COALESCE(NULLIF(EXCLUDED.englishName, ''), shows_meta.englishName),
|
|
20
|
+
genres = COALESCE(NULLIF(EXCLUDED.genres, ''), shows_meta.genres),
|
|
21
|
+
popularityScore = COALESCE(EXCLUDED.popularityScore, shows_meta.popularityScore),
|
|
22
|
+
status = COALESCE(NULLIF(EXCLUDED.status, ''), shows_meta.status),
|
|
23
|
+
episodeCount = COALESCE(EXCLUDED.episodeCount, shows_meta.episodeCount),
|
|
24
|
+
type = COALESCE(NULLIF(EXCLUDED.type, ''), shows_meta.type),
|
|
25
|
+
anilistId = COALESCE(EXCLUDED.anilistId, shows_meta.anilistId),
|
|
26
|
+
isAdult = COALESCE(EXCLUDED.isAdult, shows_meta.isAdult)`, [
|
|
27
|
+
data.id,
|
|
28
|
+
data.name ?? null,
|
|
29
|
+
data.thumbnail ?? null,
|
|
30
|
+
data.nativeName ?? null,
|
|
31
|
+
data.englishName ?? null,
|
|
32
|
+
data.genres ?? null,
|
|
33
|
+
data.popularityScore ?? null,
|
|
34
|
+
data.status ?? null,
|
|
35
|
+
data.episodeCount ?? null,
|
|
36
|
+
data.type ?? null,
|
|
37
|
+
data.anilistId ?? null,
|
|
38
|
+
data.isAdult ?? null,
|
|
39
|
+
]),
|
|
40
|
+
updateEpisodeCount: (db, id, episodeCount) => (0, db_utils_1.dbRun)(db, 'UPDATE shows_meta SET episodeCount = ? WHERE id = ?', [episodeCount, id]),
|
|
41
|
+
updateStatus: (db, id, status) => (0, db_utils_1.dbRun)(db, 'UPDATE shows_meta SET status = ? WHERE id = ?', [status, id]),
|
|
42
|
+
cleanupOrphanedMeta: (db) => (0, db_utils_1.dbRun)(db, 'DELETE FROM shows_meta WHERE id NOT IN (SELECT id FROM watchlist) AND id NOT IN (SELECT showId FROM queue)'),
|
|
43
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TempShowIdsRepository = void 0;
|
|
4
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
5
|
+
const temp_ids_1 = require("../lib/temp-ids");
|
|
6
|
+
exports.TempShowIdsRepository = {
|
|
7
|
+
getById: (db, id) => (0, db_utils_1.dbGet)(db, 'SELECT * FROM temp_show_ids WHERE id = ?', [id]),
|
|
8
|
+
getByProviderNative: (db, provider, nativeId) => (0, db_utils_1.dbGet)(db, 'SELECT * FROM temp_show_ids WHERE provider = ? AND nativeId = ?', [
|
|
9
|
+
provider,
|
|
10
|
+
nativeId,
|
|
11
|
+
]),
|
|
12
|
+
count: (db) => {
|
|
13
|
+
const row = (0, db_utils_1.dbGet)(db, 'SELECT COUNT(*) as total FROM temp_show_ids');
|
|
14
|
+
return row?.total || 0;
|
|
15
|
+
},
|
|
16
|
+
allocate: (db, data) => {
|
|
17
|
+
const existing = exports.TempShowIdsRepository.getByProviderNative(db, data.provider, data.nativeId);
|
|
18
|
+
if (existing)
|
|
19
|
+
return existing;
|
|
20
|
+
const prefixLen = temp_ids_1.TEMP_SHOW_ID_PREFIX.length + 1;
|
|
21
|
+
const maxRow = (0, db_utils_1.dbGet)(db, `SELECT MAX(CAST(SUBSTR(id, ?) AS INTEGER)) as maxId FROM temp_show_ids WHERE id LIKE '${temp_ids_1.TEMP_SHOW_ID_PREFIX}%'`, [prefixLen]);
|
|
22
|
+
const next = (maxRow?.maxId || 0) + 1;
|
|
23
|
+
const id = `${temp_ids_1.TEMP_SHOW_ID_PREFIX}${next}`;
|
|
24
|
+
try {
|
|
25
|
+
(0, db_utils_1.dbRun)(db, 'INSERT INTO temp_show_ids (id, provider, nativeId, title, thumbnail, createdAt) VALUES (?, ?, ?, ?, ?, ?)', [id, data.provider, data.nativeId, data.title, data.thumbnail || '', Date.now()]);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Lost a race with a concurrent allocate — return whatever won.
|
|
29
|
+
const winner = exports.TempShowIdsRepository.getByProviderNative(db, data.provider, data.nativeId);
|
|
30
|
+
if (winner)
|
|
31
|
+
return winner;
|
|
32
|
+
throw new Error('temp id allocation failed');
|
|
33
|
+
}
|
|
34
|
+
return exports.TempShowIdsRepository.getById(db, id);
|
|
35
|
+
},
|
|
36
|
+
purge: (db) => {
|
|
37
|
+
const total = exports.TempShowIdsRepository.count(db);
|
|
38
|
+
if (total === 0)
|
|
39
|
+
return 0;
|
|
40
|
+
const like = `${temp_ids_1.TEMP_SHOW_ID_PREFIX}%`;
|
|
41
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM watchlist WHERE id LIKE ?', [like]);
|
|
42
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM watched_episodes WHERE showId LIKE ?', [like]);
|
|
43
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM queue WHERE showId LIKE ?', [like]);
|
|
44
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM shows_meta WHERE id LIKE ?', [like]);
|
|
45
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM dismissed_notifications WHERE showId LIKE ?', [like]);
|
|
46
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM discovered_notifications WHERE showId LIKE ?', [like]);
|
|
47
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM legacy_id_mapping WHERE legacyId LIKE ? OR numericId LIKE ?', [
|
|
48
|
+
like,
|
|
49
|
+
like,
|
|
50
|
+
]);
|
|
51
|
+
(0, db_utils_1.dbRun)(db, 'DELETE FROM temp_show_ids');
|
|
52
|
+
return total;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WatchedEpisodesRepository = void 0;
|
|
4
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
5
|
+
exports.WatchedEpisodesRepository = {
|
|
6
|
+
getByShowAndEpisode: (db, showId, episodeNumber) => (0, db_utils_1.dbGet)(db, 'SELECT currentTime, duration FROM watched_episodes WHERE showId = ? AND episodeNumber = ?', [showId, episodeNumber]),
|
|
7
|
+
getWatchedEpisodeNumbers: async (db, showId) => {
|
|
8
|
+
const rows = await (0, db_utils_1.dbAll)(db, 'SELECT episodeNumber FROM watched_episodes WHERE showId = ?', [showId]);
|
|
9
|
+
return rows.map((r) => r.episodeNumber);
|
|
10
|
+
},
|
|
11
|
+
getByShow: (db, showId) => (0, db_utils_1.dbAll)(db, 'SELECT showId, episodeNumber, currentTime, duration, watchedAt FROM watched_episodes WHERE showId = ? ORDER BY CAST(episodeNumber AS REAL) ASC', [showId]),
|
|
12
|
+
getLatestResumeProgress: (db, showId) => (0, db_utils_1.dbGet)(db, `SELECT showId, episodeNumber, currentTime, duration, watchedAt
|
|
13
|
+
FROM watched_episodes
|
|
14
|
+
WHERE showId = ? AND currentTime > 5 AND (duration <= 0 OR currentTime < duration * 0.8)
|
|
15
|
+
ORDER BY watchedAt DESC
|
|
16
|
+
LIMIT 1`, [showId]),
|
|
17
|
+
upsert: (db, data) => (0, db_utils_1.dbRun)(db, 'INSERT OR REPLACE INTO watched_episodes (showId, episodeNumber, watchedAt, currentTime, duration) VALUES (?, ?, CURRENT_TIMESTAMP, ?, ?)', [data.showId, data.episodeNumber, data.currentTime, data.duration]),
|
|
18
|
+
deleteByShow: (db, showId) => (0, db_utils_1.dbRun)(db, 'DELETE FROM watched_episodes WHERE showId = ?', [showId]),
|
|
19
|
+
getContinueWatching: (db, limit) => {
|
|
20
|
+
const limitClause = typeof limit === 'number' ? `LIMIT ${limit}` : '';
|
|
21
|
+
const query = `
|
|
22
|
+
SELECT
|
|
23
|
+
we.showId as _id,
|
|
24
|
+
we.showId as id,
|
|
25
|
+
COALESCE(NULLIF(w.name, ''), sm.name) as name,
|
|
26
|
+
COALESCE(NULLIF(w.thumbnail, ''), NULLIF(sm.thumbnail, ''), '') as thumbnail,
|
|
27
|
+
COALESCE(NULLIF(w.nativeName, ''), sm.nativeName) as nativeName,
|
|
28
|
+
COALESCE(NULLIF(w.englishName, ''), sm.englishName) as englishName,
|
|
29
|
+
COALESCE(NULLIF(w.type, ''), sm.type) as type,
|
|
30
|
+
sm.episodeCount,
|
|
31
|
+
sm.type as smType,
|
|
32
|
+
sm.isAdult as isAdult,
|
|
33
|
+
w.status as watchlistStatus,
|
|
34
|
+
(SELECT COUNT(DISTINCT episodeNumber) FROM watched_episodes WHERE showId = we.showId) as watchedCount,
|
|
35
|
+
we.episodeNumber, we.currentTime, we.duration, we.watchedAt
|
|
36
|
+
FROM (
|
|
37
|
+
SELECT *, ROW_NUMBER() OVER(PARTITION BY showId ORDER BY watchedAt DESC) as rn
|
|
38
|
+
FROM watched_episodes
|
|
39
|
+
) we
|
|
40
|
+
LEFT JOIN watchlist w ON we.showId = w.id
|
|
41
|
+
LEFT JOIN shows_meta sm ON we.showId = sm.id
|
|
42
|
+
WHERE we.rn = 1
|
|
43
|
+
AND (w.status IS NULL OR w.status = 'Watching')
|
|
44
|
+
AND (w.id IS NOT NULL OR sm.id IS NOT NULL)
|
|
45
|
+
ORDER BY we.watchedAt DESC
|
|
46
|
+
${limitClause}
|
|
47
|
+
`;
|
|
48
|
+
return (0, db_utils_1.dbAll)(db, query);
|
|
49
|
+
},
|
|
50
|
+
getEpisodesForShows: (db, showIds) => {
|
|
51
|
+
const placeholders = showIds.map(() => '?').join(',');
|
|
52
|
+
return (0, db_utils_1.dbAll)(db, `SELECT showId, episodeNumber, currentTime, duration, watchedAt FROM watched_episodes WHERE showId IN (${placeholders})`, showIds);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WatchlistRepository = void 0;
|
|
4
|
+
const db_utils_1 = require("../utils/db-utils");
|
|
5
|
+
exports.WatchlistRepository = {
|
|
6
|
+
getById: (db, id) => (0, db_utils_1.dbGet)(db, 'SELECT * FROM watchlist WHERE id = ?', [id]),
|
|
7
|
+
exists: (db, id) => {
|
|
8
|
+
const row = (0, db_utils_1.dbGet)(db, 'SELECT EXISTS(SELECT 1 FROM watchlist WHERE id = ?) as inWatchlist', [id]);
|
|
9
|
+
return !!(row && row.inWatchlist);
|
|
10
|
+
},
|
|
11
|
+
getAll: (db, status, limit, offset) => {
|
|
12
|
+
let query = 'SELECT * FROM watchlist';
|
|
13
|
+
const params = [];
|
|
14
|
+
if (status && status !== 'All') {
|
|
15
|
+
query += ' WHERE status = ?';
|
|
16
|
+
params.push(status);
|
|
17
|
+
}
|
|
18
|
+
query += ' ORDER BY rowid DESC';
|
|
19
|
+
if (limit !== undefined && offset !== undefined) {
|
|
20
|
+
query += ' LIMIT ? OFFSET ?';
|
|
21
|
+
params.push(limit, offset);
|
|
22
|
+
}
|
|
23
|
+
return (0, db_utils_1.dbAll)(db, query, params);
|
|
24
|
+
},
|
|
25
|
+
getCount: (db, status) => {
|
|
26
|
+
let query = 'SELECT COUNT(*) as total FROM watchlist';
|
|
27
|
+
const params = [];
|
|
28
|
+
if (status && status !== 'All') {
|
|
29
|
+
query += ' WHERE status = ?';
|
|
30
|
+
params.push(status);
|
|
31
|
+
}
|
|
32
|
+
const row = (0, db_utils_1.dbGet)(db, query, params);
|
|
33
|
+
return row?.total || 0;
|
|
34
|
+
},
|
|
35
|
+
upsert: (db, data) => (0, db_utils_1.dbRun)(db, `INSERT INTO watchlist (id, name, thumbnail, status, nativeName, englishName, type) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
36
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
37
|
+
name = COALESCE(NULLIF(EXCLUDED.name, ''), watchlist.name),
|
|
38
|
+
thumbnail = COALESCE(NULLIF(EXCLUDED.thumbnail, ''), watchlist.thumbnail),
|
|
39
|
+
status = COALESCE(NULLIF(EXCLUDED.status, ''), watchlist.status),
|
|
40
|
+
nativeName = COALESCE(NULLIF(EXCLUDED.nativeName, ''), watchlist.nativeName),
|
|
41
|
+
englishName = COALESCE(NULLIF(EXCLUDED.englishName, ''), watchlist.englishName),
|
|
42
|
+
type = COALESCE(NULLIF(EXCLUDED.type, ''), watchlist.type)`, [
|
|
43
|
+
data.id,
|
|
44
|
+
data.name,
|
|
45
|
+
data.thumbnail,
|
|
46
|
+
data.status,
|
|
47
|
+
data.nativeName,
|
|
48
|
+
data.englishName,
|
|
49
|
+
data.type,
|
|
50
|
+
]),
|
|
51
|
+
updateStatus: (db, id, status) => (0, db_utils_1.dbRun)(db, 'UPDATE watchlist SET status = ? WHERE id = ?', [status, id]),
|
|
52
|
+
updateThumbnail: (db, id, thumbnail) => {
|
|
53
|
+
if (!thumbnail || thumbnail.trim() === '')
|
|
54
|
+
return Promise.resolve();
|
|
55
|
+
return (0, db_utils_1.dbRun)(db, 'UPDATE watchlist SET thumbnail = ? WHERE id = ?', [thumbnail, id]);
|
|
56
|
+
},
|
|
57
|
+
delete: (db, id) => (0, db_utils_1.dbRun)(db, 'DELETE FROM watchlist WHERE id = ?', [id]),
|
|
58
|
+
deleteMany: (db, ids) => {
|
|
59
|
+
if (ids.length === 0)
|
|
60
|
+
return Promise.resolve();
|
|
61
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
62
|
+
return (0, db_utils_1.dbRun)(db, `DELETE FROM watchlist WHERE id IN (${placeholders})`, ids);
|
|
63
|
+
},
|
|
64
|
+
updateStatusMany: (db, ids, status) => {
|
|
65
|
+
if (ids.length === 0)
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
68
|
+
return (0, db_utils_1.dbRun)(db, `UPDATE watchlist SET status = ? WHERE id IN (${placeholders})`, [
|
|
69
|
+
status,
|
|
70
|
+
...ids,
|
|
71
|
+
]);
|
|
72
|
+
},
|
|
73
|
+
getWatchingShows: (db) => (0, db_utils_1.dbAll)(db, "SELECT id, name, thumbnail, nativeName, englishName FROM watchlist WHERE status = 'Watching'"),
|
|
74
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
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.createAsmrRouter = createAsmrRouter;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
9
|
+
function makeCacheMiddleware(cache, keyFn, ttl) {
|
|
10
|
+
return (req, res, next) => {
|
|
11
|
+
const cacheKey = keyFn(req);
|
|
12
|
+
const cached = cache.get(cacheKey);
|
|
13
|
+
if (cached)
|
|
14
|
+
return res.json(cached);
|
|
15
|
+
const originalJson = res.json.bind(res);
|
|
16
|
+
res.json = (data) => {
|
|
17
|
+
if (res.statusCode >= 200 && res.statusCode < 400) {
|
|
18
|
+
if (ttl !== undefined)
|
|
19
|
+
cache.set(cacheKey, data, ttl);
|
|
20
|
+
else
|
|
21
|
+
cache.set(cacheKey, data);
|
|
22
|
+
}
|
|
23
|
+
return originalJson(data);
|
|
24
|
+
};
|
|
25
|
+
next();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createAsmrRouter(apiCache, provider) {
|
|
29
|
+
const router = (0, express_1.Router)();
|
|
30
|
+
router.get('/asmr/browse', makeCacheMiddleware(apiCache, (req) => `route-asmr-browse-${req.query.q || ''}-${req.query.page || 1}-${req.query.sort || ''}-${req.query.rating || ''}-${req.query.sort === 'random' ? Date.now() : ''}`, 300), async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await provider.browse({
|
|
33
|
+
query: req.query.q,
|
|
34
|
+
page: parseInt(req.query.page) || 1,
|
|
35
|
+
sort: req.query.sort,
|
|
36
|
+
rating: req.query.rating,
|
|
37
|
+
});
|
|
38
|
+
res.json(result);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err.message === 'AUTH_REQUIRED') {
|
|
42
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'jasmr' });
|
|
43
|
+
}
|
|
44
|
+
logger_1.default.error({ err }, '[Asmr] browse failed');
|
|
45
|
+
res.json({ shows: [], hasNext: false });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
router.get('/asmr/work/:rj', makeCacheMiddleware(apiCache, (req) => `route-asmr-work-${req.params.rj}`, 1800), async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const rjCode = String(req.params.rj).trim().toUpperCase();
|
|
51
|
+
const episodes = await provider.getEpisodes(rjCode);
|
|
52
|
+
const streams = await provider.getStreamUrls(rjCode, '1');
|
|
53
|
+
const images = await provider.getImages(rjCode);
|
|
54
|
+
const chapters = await provider.getChapters(rjCode);
|
|
55
|
+
res.json({
|
|
56
|
+
rjCode,
|
|
57
|
+
description: episodes?.description || '',
|
|
58
|
+
tracks: streams?.[0]?.links || [],
|
|
59
|
+
images,
|
|
60
|
+
chapters,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err.message === 'AUTH_REQUIRED') {
|
|
65
|
+
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'jasmr' });
|
|
66
|
+
}
|
|
67
|
+
logger_1.default.error({ err, rj: req.params.rj }, '[Asmr] work fetch failed');
|
|
68
|
+
res.json({ rjCode: req.params.rj, description: '', tracks: [], images: [], chapters: [] });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return router;
|
|
72
|
+
}
|