ani-web 2.0.7

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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +220 -0
  3. package/client/dist/assets/AnimeInfo-B88ZA3gl.js +1 -0
  4. package/client/dist/assets/AnimeInfo-R63luGTP.css +1 -0
  5. package/client/dist/assets/AnimeInfoPage-xGVarrXG.js +2 -0
  6. package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
  7. package/client/dist/assets/Button-lkEUHIDS.js +1 -0
  8. package/client/dist/assets/ErrorMessage-BVWNgHMx.css +1 -0
  9. package/client/dist/assets/ErrorMessage-BXKDLzn8.js +1 -0
  10. package/client/dist/assets/Home-O1FbN8t_.js +1 -0
  11. package/client/dist/assets/Home-r0eYbWNc.css +1 -0
  12. package/client/dist/assets/Insights-CF4K-oO5.css +1 -0
  13. package/client/dist/assets/Insights-opcB-aTo.js +1 -0
  14. package/client/dist/assets/MAL-BGM33N5c.js +1 -0
  15. package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
  16. package/client/dist/assets/Player-BarbgKjI.css +1 -0
  17. package/client/dist/assets/Player-CSyGax33.js +9 -0
  18. package/client/dist/assets/PlayerSettings-BaCVQyw6.js +1 -0
  19. package/client/dist/assets/PlayerSettings-l6aLKrHh.css +1 -0
  20. package/client/dist/assets/QueueRail-Cxn5U8kE.js +1 -0
  21. package/client/dist/assets/QueueRail-DOM_pWkE.css +1 -0
  22. package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
  23. package/client/dist/assets/RemoveConfirmationModal-DatCZQKq.js +1 -0
  24. package/client/dist/assets/Search-BzO-aRP7.css +1 -0
  25. package/client/dist/assets/Search-DJxo3BYH.js +1 -0
  26. package/client/dist/assets/SearchableSelect-BkCrf6E8.css +1 -0
  27. package/client/dist/assets/SearchableSelect-BzYsMz8B.js +1 -0
  28. package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
  29. package/client/dist/assets/Settings-C5adinOe.js +1 -0
  30. package/client/dist/assets/SynopsisText-C3AK-aRc.js +1 -0
  31. package/client/dist/assets/SynopsisText-DsI3mW5v.css +1 -0
  32. package/client/dist/assets/ToggleSwitch-BIlQxIjg.css +1 -0
  33. package/client/dist/assets/ToggleSwitch-CrXim14A.js +1 -0
  34. package/client/dist/assets/Watchlist-CXw0vbNx.js +1 -0
  35. package/client/dist/assets/Watchlist-a2RHQogs.css +1 -0
  36. package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
  37. package/client/dist/assets/index-BzX_xmnf.css +1 -0
  38. package/client/dist/assets/index-Ciivz6fh.js +178 -0
  39. package/client/dist/assets/useAnimeInfoData-Dqthchpa.js +1 -0
  40. package/client/dist/assets/useIsMobile-BviODivc.js +1 -0
  41. package/client/dist/assets/vendor-Bc4EraM_.js +3 -0
  42. package/client/dist/favicon.ico +0 -0
  43. package/client/dist/index.html +35 -0
  44. package/client/dist/logo.png +0 -0
  45. package/client/dist/placeholder.svg +4 -0
  46. package/client/dist/robots.txt +3 -0
  47. package/client/package.json +58 -0
  48. package/orchestrator.js +323 -0
  49. package/package.json +88 -0
  50. package/server/.env +1 -0
  51. package/server/dist/config.js +89 -0
  52. package/server/dist/constants.json +1359 -0
  53. package/server/dist/controllers/auth.controller.js +215 -0
  54. package/server/dist/controllers/data.controller.js +232 -0
  55. package/server/dist/controllers/insights.controller.js +200 -0
  56. package/server/dist/controllers/proxy.controller.js +353 -0
  57. package/server/dist/controllers/settings.controller.js +159 -0
  58. package/server/dist/controllers/watchlist.controller.js +749 -0
  59. package/server/dist/db.js +152 -0
  60. package/server/dist/discord-rpc.js +279 -0
  61. package/server/dist/github-sync.js +342 -0
  62. package/server/dist/google.js +310 -0
  63. package/server/dist/logger.js +21 -0
  64. package/server/dist/providers/123anime.provider.js +226 -0
  65. package/server/dist/providers/allanime.provider.js +736 -0
  66. package/server/dist/providers/animepahe.provider.js +457 -0
  67. package/server/dist/providers/animeya.provider.js +787 -0
  68. package/server/dist/providers/megaplay.provider.js +264 -0
  69. package/server/dist/providers/provider.interface.js +2 -0
  70. package/server/dist/rclone.js +126 -0
  71. package/server/dist/repositories/insights.repository.js +42 -0
  72. package/server/dist/repositories/notifications.repository.js +30 -0
  73. package/server/dist/repositories/queue.repository.js +38 -0
  74. package/server/dist/repositories/settings.repository.js +14 -0
  75. package/server/dist/repositories/shows-meta.repository.js +41 -0
  76. package/server/dist/repositories/watched-episodes.repository.js +67 -0
  77. package/server/dist/repositories/watchlist.repository.js +80 -0
  78. package/server/dist/routes/auth.routes.js +26 -0
  79. package/server/dist/routes/data.routes.js +42 -0
  80. package/server/dist/routes/insights.routes.js +12 -0
  81. package/server/dist/routes/proxy.routes.js +14 -0
  82. package/server/dist/routes/settings.routes.js +27 -0
  83. package/server/dist/routes/watchlist.routes.js +46 -0
  84. package/server/dist/server.js +229 -0
  85. package/server/dist/sync-config.js +28 -0
  86. package/server/dist/sync.js +427 -0
  87. package/server/dist/utils/db-utils.js +15 -0
  88. package/server/dist/utils/env.utils.js +79 -0
  89. package/server/dist/utils/machine-id.js +46 -0
  90. package/server/dist/utils/request-context.js +5 -0
  91. package/server/package.json +19 -0
@@ -0,0 +1,264 @@
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.MegaPlayProvider = void 0;
7
+ const logger_1 = __importDefault(require("../logger"));
8
+ class MegaPlayProvider {
9
+ name = 'MegaPlay';
10
+ jikanBase = 'https://api.jikan.moe/v4';
11
+ megaPlayBase = 'https://megaplay.buzz/stream/mal';
12
+ cache;
13
+ constructor(cache) {
14
+ this.cache = cache;
15
+ }
16
+ normalizeTitle(title) {
17
+ return title
18
+ .toLowerCase()
19
+ .replace(/[^\w\s-]/g, '')
20
+ .replace(/\s+/g, ' ')
21
+ .trim();
22
+ }
23
+ bestMatch(results, query) {
24
+ const q = this.normalizeTitle(query);
25
+ let best = results[0];
26
+ let bestScore = -1;
27
+ for (const anime of results) {
28
+ const title = this.normalizeTitle(anime.title);
29
+ const englishTitle = anime.title_english ? this.normalizeTitle(anime.title_english) : '';
30
+ let score = -1;
31
+ if (title === q || englishTitle === q) {
32
+ score = 3;
33
+ }
34
+ else if (title.startsWith(q) || englishTitle.startsWith(q)) {
35
+ score = 2;
36
+ }
37
+ else if (title.includes(q) || englishTitle.includes(q)) {
38
+ score = 1;
39
+ }
40
+ if (score > bestScore) {
41
+ bestScore = score;
42
+ best = anime;
43
+ if (score === 3)
44
+ break;
45
+ }
46
+ }
47
+ return best;
48
+ }
49
+ async search(options) {
50
+ try {
51
+ const rawQuery = options.query || '';
52
+ const query = rawQuery.replace(/[""]/g, '').replace(/[']/g, '').replace(/\s+/g, ' ').trim();
53
+ const url = `${this.jikanBase}/anime?q=${encodeURIComponent(query)}`;
54
+ const response = await fetch(url);
55
+ const data = (await response.json());
56
+ if (!data.data || data.data.length === 0)
57
+ return [];
58
+ const results = data.data.map((anime) => ({
59
+ _id: anime.mal_id.toString(),
60
+ id: anime.mal_id.toString(),
61
+ name: anime.title,
62
+ englishName: anime.title_english || anime.title,
63
+ thumbnail: anime.images?.jpg?.image_url,
64
+ type: anime.type,
65
+ year: anime.year,
66
+ episodeCount: anime.episodes,
67
+ }));
68
+ if (query && results.length > 0) {
69
+ const best = this.bestMatch(data.data, query);
70
+ const bestIndex = data.data.findIndex((a) => a.mal_id === best.mal_id);
71
+ if (bestIndex > 0) {
72
+ const [bestItem] = results.splice(bestIndex, 1);
73
+ results.unshift(bestItem);
74
+ }
75
+ }
76
+ return results;
77
+ }
78
+ catch (error) {
79
+ logger_1.default.error({ error }, 'MegaPlay (Jikan) search failed');
80
+ return [];
81
+ }
82
+ }
83
+ async getEpisodes(showId, _mode) {
84
+ try {
85
+ if (!/^\d+$/.test(showId))
86
+ return null;
87
+ const cacheKey = `megaplay_eps_${showId}`;
88
+ const cached = this.cache.get(cacheKey);
89
+ if (cached)
90
+ return cached;
91
+ const url = `${this.jikanBase}/anime/${showId}`;
92
+ const response = await fetch(url);
93
+ if (!response.ok)
94
+ return null;
95
+ const data = (await response.json());
96
+ if (!data.data)
97
+ return null;
98
+ const episodeCount = data.data.episodes || 0;
99
+ let count = episodeCount;
100
+ if (count === 0) {
101
+ if (data.data.status === 'Currently Airing' || data.data.status === 'Finished Airing') {
102
+ count = 12;
103
+ }
104
+ }
105
+ const episodes = Array.from({ length: count }, (_, i) => (i + 1).toString());
106
+ const result = {
107
+ episodes,
108
+ description: data.data.synopsis || '',
109
+ };
110
+ this.cache.set(cacheKey, result, 86400);
111
+ return result;
112
+ }
113
+ catch (error) {
114
+ logger_1.default.error({ error, showId }, 'MegaPlay getEpisodes failed');
115
+ return null;
116
+ }
117
+ }
118
+ async getStreamUrls(showId, episodeNumber, mode) {
119
+ if (!/^\d+$/.test(showId))
120
+ return null;
121
+ let targetEpisode = episodeNumber;
122
+ if (episodeNumber === '0') {
123
+ targetEpisode = '1';
124
+ }
125
+ const streamUrl = `${this.megaPlayBase}/${showId}/${targetEpisode}/${mode}`;
126
+ return [
127
+ {
128
+ sourceName: `MegaPlay (${mode.toUpperCase()})`,
129
+ links: [
130
+ {
131
+ resolutionStr: 'Auto',
132
+ link: streamUrl,
133
+ hls: false,
134
+ },
135
+ ],
136
+ type: 'iframe',
137
+ actualEpisodeNumber: targetEpisode,
138
+ },
139
+ ];
140
+ }
141
+ async getShowMeta(showId) {
142
+ try {
143
+ if (!/^\d+$/.test(showId))
144
+ return null;
145
+ const url = `${this.jikanBase}/anime/${showId}`;
146
+ const response = await fetch(url);
147
+ const data = (await response.json());
148
+ if (!data.data)
149
+ return null;
150
+ const anime = data.data;
151
+ return {
152
+ _id: anime.mal_id.toString(),
153
+ name: anime.title,
154
+ englishName: anime.title_english,
155
+ thumbnail: anime.images?.jpg?.image_url,
156
+ description: anime.synopsis,
157
+ type: anime.type,
158
+ year: anime.year,
159
+ episodeCount: anime.episodes,
160
+ status: anime.status,
161
+ genres: anime.genres?.map((g) => ({ name: g.name })),
162
+ score: anime.score,
163
+ };
164
+ }
165
+ catch (error) {
166
+ logger_1.default.error({ error, showId }, 'MegaPlay getShowMeta failed');
167
+ return null;
168
+ }
169
+ }
170
+ async getPopular(_timeframe, page, size) {
171
+ try {
172
+ const url = `${this.jikanBase}/top/anime?page=${page || 1}&limit=${size || 10}`;
173
+ const response = await fetch(url);
174
+ const data = (await response.json());
175
+ if (!data.data)
176
+ return [];
177
+ return data.data.map((anime) => ({
178
+ _id: anime.mal_id.toString(),
179
+ id: anime.mal_id.toString(),
180
+ name: anime.title,
181
+ englishName: anime.title_english || anime.title,
182
+ thumbnail: anime.images?.jpg?.image_url,
183
+ type: anime.type,
184
+ year: anime.year,
185
+ episodeCount: anime.episodes,
186
+ }));
187
+ }
188
+ catch {
189
+ return [];
190
+ }
191
+ }
192
+ async getSchedule(date) {
193
+ try {
194
+ const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
195
+ const day = days[date.getDay()];
196
+ const url = `${this.jikanBase}/schedules?filter=${day}`;
197
+ const response = await fetch(url);
198
+ const data = (await response.json());
199
+ if (!data.data)
200
+ return [];
201
+ return data.data.map((anime) => ({
202
+ _id: anime.mal_id.toString(),
203
+ id: anime.mal_id.toString(),
204
+ name: anime.title,
205
+ englishName: anime.title_english || anime.title,
206
+ thumbnail: anime.images?.jpg?.image_url,
207
+ type: anime.type,
208
+ year: anime.year,
209
+ episodeCount: anime.episodes,
210
+ }));
211
+ }
212
+ catch {
213
+ return [];
214
+ }
215
+ }
216
+ async getSeasonal(page) {
217
+ try {
218
+ const url = `${this.jikanBase}/seasons/now?page=${page}`;
219
+ const response = await fetch(url);
220
+ const data = (await response.json());
221
+ if (!data.data)
222
+ return [];
223
+ return data.data.map((anime) => ({
224
+ _id: anime.mal_id.toString(),
225
+ id: anime.mal_id.toString(),
226
+ name: anime.title,
227
+ englishName: anime.title_english || anime.title,
228
+ thumbnail: anime.images?.jpg?.image_url,
229
+ type: anime.type,
230
+ year: anime.year,
231
+ episodeCount: anime.episodes,
232
+ }));
233
+ }
234
+ catch {
235
+ return [];
236
+ }
237
+ }
238
+ async getLatestReleases(page, size) {
239
+ try {
240
+ const url = `${this.jikanBase}/top/anime?filter=airing&page=${page || 1}&limit=${size || 10}`;
241
+ const response = await fetch(url);
242
+ const data = (await response.json());
243
+ if (!data.data)
244
+ return [];
245
+ return data.data.map((anime) => ({
246
+ _id: anime.mal_id.toString(),
247
+ id: anime.mal_id.toString(),
248
+ name: anime.title,
249
+ englishName: anime.title_english || anime.title,
250
+ thumbnail: anime.images?.jpg?.image_url,
251
+ type: anime.type,
252
+ year: anime.year,
253
+ episodeCount: anime.episodes,
254
+ }));
255
+ }
256
+ catch {
257
+ return [];
258
+ }
259
+ }
260
+ async getSkipTimes(_showId, _episodeNumber) {
261
+ return { found: false, results: [] };
262
+ }
263
+ }
264
+ exports.MegaPlayProvider = MegaPlayProvider;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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(command) {
13
+ return new Promise((resolve, reject) => {
14
+ (0, child_process_1.exec)(command, (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('rclone 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('rclone 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,42 @@
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(*) FROM watchlist WHERE status = 'Completed') as completedCount,
10
+ (SELECT COUNT(*) FROM watchlist) as totalWatchlist`),
11
+ getActivityGrid: (db) => (0, db_utils_1.dbAll)(db, `SELECT date(watchedAt) as day, COUNT(*) as count FROM watched_episodes GROUP BY day`),
12
+ getHourlyDist: (db) => (0, db_utils_1.dbAll)(db, `SELECT strftime('%H', watchedAt) as hour, COUNT(*) as count FROM watched_episodes GROUP BY hour`),
13
+ getSeasonality: (db) => (0, db_utils_1.dbAll)(db, `SELECT strftime('%m', watchedAt) as month, SUM(currentTime) as seconds FROM watched_episodes GROUP BY month`),
14
+ getAllWatches: (db) => (0, db_utils_1.dbAll)(db, 'SELECT watchedAt, currentTime FROM watched_episodes ORDER BY watchedAt ASC'),
15
+ getWatchedShowsMeta: (db) => (0, db_utils_1.dbAll)(db, `SELECT DISTINCT sm.id, sm.genres, sm.popularityScore
16
+ FROM shows_meta sm
17
+ JOIN watched_episodes we ON sm.id = we.showId`),
18
+ getDroppedShows: (db) => (0, db_utils_1.dbAll)(db, `SELECT w.id, w.name, MAX(we.watchedAt) as lastActivity
19
+ FROM watchlist w
20
+ JOIN watched_episodes we ON w.id = we.showId
21
+ WHERE w.status = 'Watching'
22
+ GROUP BY w.id
23
+ HAVING lastActivity < date('now', '-90 days')`),
24
+ getCompletionVelocities: (db) => (0, db_utils_1.dbAll)(db, `SELECT
25
+ (julianday(MAX(we.watchedAt)) - julianday(MIN(we.watchedAt))) as daysToFinish
26
+ FROM watchlist w
27
+ JOIN watched_episodes we ON w.id = we.showId
28
+ WHERE w.status = 'Completed'
29
+ GROUP BY w.id`),
30
+ getWatchedEpisodesWithMeta: (db) => (0, db_utils_1.dbAll)(db, `SELECT
31
+ we.showId,
32
+ we.currentTime,
33
+ we.duration,
34
+ sm.genres,
35
+ sm.popularityScore,
36
+ sm.name,
37
+ sm.nativeName,
38
+ sm.englishName,
39
+ sm.thumbnail
40
+ FROM watched_episodes we
41
+ JOIN shows_meta sm ON we.showId = sm.id`),
42
+ };
@@ -0,0 +1,30 @@
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
+ };
@@ -0,0 +1,38 @@
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(sm.name, w.name) as name,
12
+ COALESCE(sm.thumbnail, w.thumbnail) as thumbnail,
13
+ COALESCE(sm.nativeName, w.nativeName) as nativeName,
14
+ COALESCE(sm.englishName, w.englishName) as englishName,
15
+ COALESCE(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
+ getMaxOrder: async (db) => {
25
+ const row = await (0, db_utils_1.dbGet)(db, 'SELECT COALESCE(MAX(queue_order), -1) as maxOrder FROM queue');
26
+ return row?.maxOrder ?? -1;
27
+ },
28
+ 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]),
29
+ removeEpisode: (db, showId, episodeNumber) => (0, db_utils_1.dbRun)(db, 'DELETE FROM queue WHERE showId = ? AND episodeNumber = ?', [showId, episodeNumber]),
30
+ clear: (db) => (0, db_utils_1.dbRun)(db, 'DELETE FROM queue'),
31
+ reorder: (db, items) => Promise.all(items.map((item, index) => {
32
+ if (item.id !== undefined) {
33
+ return (0, db_utils_1.dbRun)(db, 'UPDATE queue SET queue_order = ? WHERE id = ?', [index, item.id]);
34
+ }
35
+ return (0, db_utils_1.dbRun)(db, 'UPDATE queue SET queue_order = ? WHERE showId = ? AND episodeNumber = ?', [index, item.showId, item.episodeNumber]);
36
+ })),
37
+ 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)'),
38
+ };
@@ -0,0 +1,14 @@
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
+ clearWatchlist: (db) => (0, db_utils_1.dbRun)(db, 'DELETE FROM watchlist'),
9
+ upsertWatchlistBatch: (db, shows) => {
10
+ for (const show of shows) {
11
+ (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]);
12
+ }
13
+ },
14
+ };
@@ -0,0 +1,41 @@
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)
14
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
15
+ ON CONFLICT(id) DO UPDATE SET
16
+ name = COALESCE(EXCLUDED.name, shows_meta.name),
17
+ thumbnail = COALESCE(EXCLUDED.thumbnail, shows_meta.thumbnail),
18
+ nativeName = COALESCE(EXCLUDED.nativeName, shows_meta.nativeName),
19
+ englishName = COALESCE(EXCLUDED.englishName, shows_meta.englishName),
20
+ genres = COALESCE(EXCLUDED.genres, shows_meta.genres),
21
+ popularityScore = COALESCE(EXCLUDED.popularityScore, shows_meta.popularityScore),
22
+ status = COALESCE(EXCLUDED.status, shows_meta.status),
23
+ episodeCount = COALESCE(EXCLUDED.episodeCount, shows_meta.episodeCount),
24
+ type = COALESCE(EXCLUDED.type, shows_meta.type)`, [
25
+ data.id,
26
+ data.name ?? null,
27
+ data.thumbnail ?? null,
28
+ data.nativeName ?? null,
29
+ data.englishName ?? null,
30
+ data.genres ?? null,
31
+ data.popularityScore ?? null,
32
+ data.status ?? null,
33
+ data.episodeCount ?? null,
34
+ data.type ?? null,
35
+ ]),
36
+ updateEpisodeCount: (db, id, episodeCount) => (0, db_utils_1.dbRun)(db, 'UPDATE shows_meta SET episodeCount = ? WHERE id = ?', [episodeCount, id]),
37
+ updateType: (db, id, type) => (0, db_utils_1.dbRun)(db, 'UPDATE shows_meta SET type = ? WHERE id = ?', [type, id]),
38
+ updateStatus: (db, id, status) => (0, db_utils_1.dbRun)(db, 'UPDATE shows_meta SET status = ? WHERE id = ?', [status, id]),
39
+ updateThumbnail: (db, id, thumbnail) => (0, db_utils_1.dbRun)(db, 'UPDATE shows_meta SET thumbnail = ? WHERE id = ?', [thumbnail, id]),
40
+ 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)'),
41
+ };
@@ -0,0 +1,67 @@
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
+ cleanupOrphanedProgress: (db) => (0, db_utils_1.dbRun)(db, 'DELETE FROM watched_episodes WHERE showId NOT IN (SELECT id FROM watchlist)'),
20
+ getContinueWatching: (db, limit) => {
21
+ const limitClause = typeof limit === 'number' ? `LIMIT ${limit}` : '';
22
+ const query = `
23
+ SELECT
24
+ w.id as _id,
25
+ w.id as id,
26
+ w.name as name,
27
+ w.thumbnail as thumbnail,
28
+ w.nativeName as nativeName,
29
+ w.englishName as englishName,
30
+ w.type as type,
31
+ sm.episodeCount,
32
+ sm.type as smType,
33
+ (SELECT COUNT(DISTINCT episodeNumber) FROM watched_episodes WHERE showId = w.id) as watchedCount,
34
+ we.episodeNumber, we.currentTime, we.duration, we.watchedAt
35
+ FROM (
36
+ SELECT *, ROW_NUMBER() OVER(PARTITION BY showId ORDER BY watchedAt DESC) as rn
37
+ FROM watched_episodes
38
+ ) we
39
+ JOIN watchlist w ON we.showId = w.id
40
+ LEFT JOIN shows_meta sm ON we.showId = sm.id
41
+ WHERE we.rn = 1 AND w.status = 'Watching'
42
+ ORDER BY we.watchedAt DESC
43
+ ${limitClause}
44
+ `;
45
+ return (0, db_utils_1.dbAll)(db, query);
46
+ },
47
+ getUpNextShows: (db) => {
48
+ const query = `
49
+ SELECT w.id, w.name, w.thumbnail, w.nativeName, w.englishName, w.type, sm.episodeCount, sm.type as smType
50
+ FROM watchlist w
51
+ LEFT JOIN shows_meta sm ON w.id = sm.id
52
+ LEFT JOIN (
53
+ SELECT showId, MAX(watchedAt) as lastActivity
54
+ FROM watched_episodes
55
+ GROUP BY showId
56
+ ) we ON w.id = we.showId
57
+ WHERE w.status = 'Watching'
58
+ ORDER BY we.lastActivity DESC
59
+ LIMIT 15
60
+ `;
61
+ return (0, db_utils_1.dbAll)(db, query);
62
+ },
63
+ getEpisodesForShows: (db, showIds) => {
64
+ const placeholders = showIds.map(() => '?').join(',');
65
+ return (0, db_utils_1.dbAll)(db, `SELECT showId, episodeNumber, currentTime, duration, watchedAt FROM watched_episodes WHERE showId IN (${placeholders})`, showIds);
66
+ },
67
+ };