ani-web 1.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ani-web might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/client/dist/assets/AnimeInfo-C7DQp7Oo.js +1 -0
- package/client/dist/assets/AnimeInfo-Sb3YiXHJ.css +1 -0
- package/client/dist/assets/AnimeInfoPage-DJA7AJQ8.js +2 -0
- package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
- package/client/dist/assets/Button-o0l9V_NG.js +1 -0
- package/client/dist/assets/ErrorMessage-Ddf2zmRx.js +1 -0
- package/client/dist/assets/ErrorMessage-FOxXyZC9.css +1 -0
- package/client/dist/assets/Home-CKHJA97j.css +1 -0
- package/client/dist/assets/Home-Dey0azy1.js +1 -0
- package/client/dist/assets/Insights-BSRcCkDs.css +1 -0
- package/client/dist/assets/Insights-CogjPOd_.js +1 -0
- package/client/dist/assets/MAL-CYArH4yf.js +1 -0
- package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
- package/client/dist/assets/Player-BWFN9gud.js +9 -0
- package/client/dist/assets/Player-CBCYW7uG.css +1 -0
- package/client/dist/assets/PlayerSettings-BgStUrrP.css +1 -0
- package/client/dist/assets/PlayerSettings-rWZuATQf.js +1 -0
- package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
- package/client/dist/assets/RemoveConfirmationModal-CLYqyGOv.js +1 -0
- package/client/dist/assets/Search-DZAWgKwq.js +1 -0
- package/client/dist/assets/Search-lWsVQ0Ke.css +1 -0
- package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
- package/client/dist/assets/Settings-DyisJGeD.js +1 -0
- package/client/dist/assets/ToggleSwitch-CLnWnAuY.js +1 -0
- package/client/dist/assets/ToggleSwitch-DInRb7iM.css +1 -0
- package/client/dist/assets/Watchlist-2dVYksxq.css +1 -0
- package/client/dist/assets/Watchlist-CuqJISI3.js +1 -0
- package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
- package/client/dist/assets/index-BK_Zaqaw.css +1 -0
- package/client/dist/assets/index-CHVF4D4L.js +178 -0
- package/client/dist/assets/useAnimeInfoData-Cr58brCY.js +1 -0
- package/client/dist/assets/useIsMobile-gHo4t6g6.js +1 -0
- package/client/dist/assets/vendor-DdbgYKo4.js +3 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/index.html +35 -0
- package/client/dist/logo.png +0 -0
- package/client/dist/placeholder.svg +4 -0
- package/client/dist/robots.txt +3 -0
- package/client/package.json +54 -0
- package/orchestrator.js +302 -0
- package/package.json +69 -0
- package/server/dist/config.js +86 -0
- package/server/dist/constants.json +1359 -0
- package/server/dist/controllers/auth.controller.js +213 -0
- package/server/dist/controllers/data.controller.js +126 -0
- package/server/dist/controllers/insights.controller.js +125 -0
- package/server/dist/controllers/proxy.controller.js +235 -0
- package/server/dist/controllers/settings.controller.js +147 -0
- package/server/dist/controllers/watchlist.controller.js +499 -0
- package/server/dist/db.js +231 -0
- package/server/dist/github-sync.js +279 -0
- package/server/dist/google.js +274 -0
- package/server/dist/logger.js +21 -0
- package/server/dist/providers/123anime.provider.js +229 -0
- package/server/dist/providers/allanime.provider.js +773 -0
- package/server/dist/providers/animepahe.provider.js +313 -0
- package/server/dist/providers/animeya.provider.js +799 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +30 -0
- package/server/dist/repositories/notifications.repository.js +22 -0
- package/server/dist/repositories/settings.repository.js +13 -0
- package/server/dist/repositories/shows-meta.repository.js +39 -0
- package/server/dist/repositories/watched-episodes.repository.js +60 -0
- package/server/dist/repositories/watchlist.repository.js +49 -0
- package/server/dist/routes/auth.routes.js +23 -0
- package/server/dist/routes/data.routes.js +43 -0
- package/server/dist/routes/insights.routes.js +11 -0
- package/server/dist/routes/proxy.routes.js +13 -0
- package/server/dist/routes/settings.routes.js +26 -0
- package/server/dist/routes/watchlist.routes.js +26 -0
- package/server/dist/server.js +179 -0
- package/server/dist/sync-config.js +28 -0
- package/server/dist/sync.js +383 -0
- package/server/dist/utils/db-utils.js +36 -0
- package/server/dist/utils/env.utils.js +70 -0
- package/server/package.json +54 -0
|
@@ -0,0 +1,499 @@
|
|
|
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.WatchlistController = void 0;
|
|
7
|
+
const logger_1 = __importDefault(require("../logger"));
|
|
8
|
+
const sync_1 = require("../sync");
|
|
9
|
+
const watchlist_repository_1 = require("../repositories/watchlist.repository");
|
|
10
|
+
const watched_episodes_repository_1 = require("../repositories/watched-episodes.repository");
|
|
11
|
+
const shows_meta_repository_1 = require("../repositories/shows-meta.repository");
|
|
12
|
+
const notifications_repository_1 = require("../repositories/notifications.repository");
|
|
13
|
+
class WatchlistController {
|
|
14
|
+
provider;
|
|
15
|
+
activeTypeFetches = new Set();
|
|
16
|
+
constructor(provider) {
|
|
17
|
+
this.provider = provider;
|
|
18
|
+
}
|
|
19
|
+
async getContinueWatchingData(req, limit) {
|
|
20
|
+
const rows = await watched_episodes_repository_1.WatchedEpisodesRepository.getContinueWatching(req.db, limit);
|
|
21
|
+
const showsNeedingEpisodes = rows.filter((show) => {
|
|
22
|
+
const watchedCount = show.watchedCount || 0;
|
|
23
|
+
return !show.episodeCount || (watchedCount > 0 && show.episodeCount <= watchedCount);
|
|
24
|
+
});
|
|
25
|
+
const episodeFetchResults = new Map();
|
|
26
|
+
if (showsNeedingEpisodes.length > 0) {
|
|
27
|
+
const BATCH_SIZE = 5;
|
|
28
|
+
for (let i = 0; i < showsNeedingEpisodes.length; i += BATCH_SIZE) {
|
|
29
|
+
const batch = showsNeedingEpisodes.slice(i, i + BATCH_SIZE);
|
|
30
|
+
const batchResults = await Promise.allSettled(batch.map((show) => this.provider.getEpisodes(show.id, 'sub')));
|
|
31
|
+
batch.forEach((show, index) => {
|
|
32
|
+
const result = batchResults[index];
|
|
33
|
+
if (result.status === 'fulfilled' && result.value?.episodes) {
|
|
34
|
+
const epCount = result.value.episodes.length;
|
|
35
|
+
episodeFetchResults.set(show.id, epCount);
|
|
36
|
+
shows_meta_repository_1.ShowsMetaRepository.updateEpisodeCount(req.db, show.id, epCount).catch((e) => {
|
|
37
|
+
logger_1.default.error({ err: e, showId: show.id }, 'Failed to update episode count in DB');
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const enrichedRows = rows.map((show) => {
|
|
44
|
+
const epCount = episodeFetchResults.get(show.id) ?? show.episodeCount;
|
|
45
|
+
return {
|
|
46
|
+
...show,
|
|
47
|
+
episodeCount: epCount,
|
|
48
|
+
type: show.type || show.smType,
|
|
49
|
+
thumbnail: this.provider.deobfuscateUrl(show.thumbnail ?? ''),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
setImmediate(async () => {
|
|
53
|
+
if (req.db.isClosedCheck())
|
|
54
|
+
return;
|
|
55
|
+
const delay = () => new Promise((res) => setImmediate(res));
|
|
56
|
+
for (const show of enrichedRows) {
|
|
57
|
+
const currentThumbnail = show.thumbnail || '';
|
|
58
|
+
const fixedThumbnail = this.provider.deobfuscateUrl(currentThumbnail);
|
|
59
|
+
const needsThumbnailUpdate = fixedThumbnail !== currentThumbnail;
|
|
60
|
+
if ((!show.type || needsThumbnailUpdate) && !this.activeTypeFetches.has(show.id)) {
|
|
61
|
+
this.activeTypeFetches.add(show.id);
|
|
62
|
+
try {
|
|
63
|
+
let didUpdate = false;
|
|
64
|
+
if (needsThumbnailUpdate && !req.db.isClosedCheck()) {
|
|
65
|
+
await watchlist_repository_1.WatchlistRepository.updateThumbnail(req.db, show.id, fixedThumbnail);
|
|
66
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateThumbnail(req.db, show.id, fixedThumbnail);
|
|
67
|
+
didUpdate = true;
|
|
68
|
+
}
|
|
69
|
+
if (!show.type) {
|
|
70
|
+
const meta = await this.provider.getShowMeta(show.id);
|
|
71
|
+
if (meta && meta.type && !req.db.isClosedCheck()) {
|
|
72
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateType(req.db, show.id, meta.type);
|
|
73
|
+
await watchlist_repository_1.WatchlistRepository.updateType(req.db, show.id, meta.type);
|
|
74
|
+
didUpdate = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (didUpdate)
|
|
78
|
+
req.db.scheduleSave();
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
logger_1.default.error({ err: e, showId: show.id }, 'Lazy migration error for show');
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.activeTypeFetches.delete(show.id);
|
|
85
|
+
}
|
|
86
|
+
await delay();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return enrichedRows;
|
|
91
|
+
}
|
|
92
|
+
async getUpNextShowsData(req) {
|
|
93
|
+
const watchingShows = await watched_episodes_repository_1.WatchedEpisodesRepository.getUpNextShows(req.db);
|
|
94
|
+
if (watchingShows.length === 0)
|
|
95
|
+
return [];
|
|
96
|
+
const showIds = watchingShows.map((s) => s.id);
|
|
97
|
+
const allWatchedEps = await watched_episodes_repository_1.WatchedEpisodesRepository.getEpisodesForShows(req.db, showIds);
|
|
98
|
+
const watchedByShow = new Map();
|
|
99
|
+
for (const ep of allWatchedEps) {
|
|
100
|
+
const arr = watchedByShow.get(ep.showId);
|
|
101
|
+
if (arr)
|
|
102
|
+
arr.push(ep);
|
|
103
|
+
else
|
|
104
|
+
watchedByShow.set(ep.showId, [ep]);
|
|
105
|
+
}
|
|
106
|
+
const BATCH_SIZE = 5;
|
|
107
|
+
const upNextShows = [];
|
|
108
|
+
for (let i = 0; i < watchingShows.length; i += BATCH_SIZE) {
|
|
109
|
+
const batch = watchingShows.slice(i, i + BATCH_SIZE);
|
|
110
|
+
const batchResults = await Promise.allSettled(batch.map(async (show) => {
|
|
111
|
+
try {
|
|
112
|
+
const epDetails = await this.provider.getEpisodes(show.id, 'sub');
|
|
113
|
+
const watchedEpisodesResult = watchedByShow.get(show.id) ?? [];
|
|
114
|
+
const allEps = epDetails?.episodes?.sort((a, b) => parseFloat(a) - parseFloat(b)) || [];
|
|
115
|
+
const watchedEpsMap = new Map(watchedEpisodesResult.map((r) => [r.episodeNumber.toString(), r]));
|
|
116
|
+
const unwatchedEps = allEps.filter((ep) => !watchedEpsMap.has(ep));
|
|
117
|
+
if (unwatchedEps.length > 0) {
|
|
118
|
+
return {
|
|
119
|
+
_id: show.id,
|
|
120
|
+
id: show.id,
|
|
121
|
+
name: show.name,
|
|
122
|
+
thumbnail: this.provider.deobfuscateUrl(show.thumbnail ?? ''),
|
|
123
|
+
nativeName: show.nativeName,
|
|
124
|
+
englishName: show.englishName,
|
|
125
|
+
type: show.type || show.smType,
|
|
126
|
+
nextEpisodeToWatch: unwatchedEps[0],
|
|
127
|
+
newEpisodesCount: unwatchedEps.length,
|
|
128
|
+
episodeCount: allEps.length,
|
|
129
|
+
watchedCount: watchedEpsMap.size,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
logger_1.default.error({ err: e, showId: show.id }, 'Error processing show for Up Next list');
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}));
|
|
139
|
+
for (const result of batchResults) {
|
|
140
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
141
|
+
upNextShows.push(result.value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (i + BATCH_SIZE < watchingShows.length) {
|
|
145
|
+
await new Promise((res) => setImmediate(res));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return upNextShows;
|
|
149
|
+
}
|
|
150
|
+
getContinueWatchingFast = async (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
153
|
+
const data = await this.getContinueWatchingData(req, limit);
|
|
154
|
+
res.json(data);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
res.status(500).json({ error: 'DB error' });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
getContinueWatchingUpNext = async (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const data = await this.getUpNextShowsData(req);
|
|
163
|
+
res.json(data);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
res.status(500).json({ error: 'DB error' });
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
getContinueWatching = async (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
172
|
+
const data = await this.getContinueWatchingData(req);
|
|
173
|
+
res.json(data.slice(0, limit));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
res.status(500).json({ error: 'DB error' });
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
getAllContinueWatching = async (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const page = parseInt(req.query.page) || 1;
|
|
182
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
183
|
+
const offset = (page - 1) * limit;
|
|
184
|
+
const data = await this.getContinueWatchingData(req);
|
|
185
|
+
res.json({
|
|
186
|
+
data: data.slice(offset, offset + limit),
|
|
187
|
+
total: data.length,
|
|
188
|
+
page,
|
|
189
|
+
limit,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
res.status(500).json({ error: 'DB error' });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
updateProgress = async (req, res) => {
|
|
197
|
+
const { showId, episodeNumber, currentTime, duration, showName, showThumbnail, nativeName, englishName, genres, popularityScore, type, status, episodeCount, } = req.body;
|
|
198
|
+
try {
|
|
199
|
+
const genresStr = Array.isArray(genres) ? JSON.stringify(genres) : genres;
|
|
200
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
201
|
+
shows_meta_repository_1.ShowsMetaRepository.upsert(tx, {
|
|
202
|
+
id: showId,
|
|
203
|
+
name: showName,
|
|
204
|
+
thumbnail: this.provider.deobfuscateUrl(showThumbnail),
|
|
205
|
+
nativeName,
|
|
206
|
+
englishName,
|
|
207
|
+
genres: genresStr,
|
|
208
|
+
popularityScore,
|
|
209
|
+
status,
|
|
210
|
+
episodeCount,
|
|
211
|
+
type,
|
|
212
|
+
});
|
|
213
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.upsert(tx, {
|
|
214
|
+
showId,
|
|
215
|
+
episodeNumber,
|
|
216
|
+
currentTime,
|
|
217
|
+
duration,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
req.db.scheduleSave();
|
|
221
|
+
res.json({ success: true });
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
logger_1.default.error({ err: error, showId }, 'Update progress failed');
|
|
225
|
+
res.status(500).json({ error: 'DB error' });
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
removeContinueWatching = async (req, res) => {
|
|
229
|
+
const { showId } = req.body;
|
|
230
|
+
try {
|
|
231
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
232
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.deleteByShow(tx, showId);
|
|
233
|
+
notifications_repository_1.NotificationsRepository.deleteByShow(tx, showId);
|
|
234
|
+
});
|
|
235
|
+
res.json({ success: true });
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
res.status(500).json({ error: 'DB error' });
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
getWatchlist = async (req, res) => {
|
|
242
|
+
const { status, page: pageStr, limit: limitStr } = req.query;
|
|
243
|
+
const page = parseInt(pageStr) || 1;
|
|
244
|
+
const limit = parseInt(limitStr) || 10;
|
|
245
|
+
const offset = (page - 1) * limit;
|
|
246
|
+
try {
|
|
247
|
+
const [rows, total] = await Promise.all([
|
|
248
|
+
watchlist_repository_1.WatchlistRepository.getAll(req.db, status, limit, offset),
|
|
249
|
+
watchlist_repository_1.WatchlistRepository.getCount(req.db, status),
|
|
250
|
+
]);
|
|
251
|
+
res.json({
|
|
252
|
+
data: rows.map((row) => ({
|
|
253
|
+
...row,
|
|
254
|
+
_id: row.id,
|
|
255
|
+
thumbnail: this.provider.deobfuscateUrl(row.thumbnail || ''),
|
|
256
|
+
})),
|
|
257
|
+
total,
|
|
258
|
+
page,
|
|
259
|
+
limit,
|
|
260
|
+
});
|
|
261
|
+
setImmediate(async () => {
|
|
262
|
+
if (req.db.isClosedCheck())
|
|
263
|
+
return;
|
|
264
|
+
const delay = () => new Promise((res) => setImmediate(res));
|
|
265
|
+
for (const row of rows) {
|
|
266
|
+
const currentThumbnail = row.thumbnail || '';
|
|
267
|
+
const fixedThumbnail = this.provider.deobfuscateUrl(currentThumbnail);
|
|
268
|
+
const needsThumbnailUpdate = fixedThumbnail !== currentThumbnail;
|
|
269
|
+
if ((!row.type || needsThumbnailUpdate) && !this.activeTypeFetches.has(row.id)) {
|
|
270
|
+
this.activeTypeFetches.add(row.id);
|
|
271
|
+
try {
|
|
272
|
+
let didUpdate = false;
|
|
273
|
+
if (needsThumbnailUpdate && !req.db.isClosedCheck()) {
|
|
274
|
+
await watchlist_repository_1.WatchlistRepository.updateThumbnail(req.db, row.id, fixedThumbnail);
|
|
275
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateThumbnail(req.db, row.id, fixedThumbnail);
|
|
276
|
+
didUpdate = true;
|
|
277
|
+
}
|
|
278
|
+
if (!row.type) {
|
|
279
|
+
const meta = await this.provider.getShowMeta(row.id);
|
|
280
|
+
if (meta && !req.db.isClosedCheck()) {
|
|
281
|
+
if (meta.type) {
|
|
282
|
+
await watchlist_repository_1.WatchlistRepository.updateType(req.db, row.id, meta.type);
|
|
283
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateType(req.db, row.id, meta.type);
|
|
284
|
+
didUpdate = true;
|
|
285
|
+
}
|
|
286
|
+
if (meta.thumbnail) {
|
|
287
|
+
const metaThumb = this.provider.deobfuscateUrl(meta.thumbnail);
|
|
288
|
+
if (metaThumb !== fixedThumbnail) {
|
|
289
|
+
await watchlist_repository_1.WatchlistRepository.updateThumbnail(req.db, row.id, metaThumb);
|
|
290
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateThumbnail(req.db, row.id, metaThumb);
|
|
291
|
+
didUpdate = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (didUpdate)
|
|
297
|
+
req.db.scheduleSave();
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
logger_1.default.error({ err: e, showId: row.id }, 'Watchlist lazy migration error');
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
this.activeTypeFetches.delete(row.id);
|
|
304
|
+
}
|
|
305
|
+
await delay();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
312
|
+
res.status(500).json({ error: 'DB error', details: message });
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
checkWatchlist = async (req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const item = await watchlist_repository_1.WatchlistRepository.getById(req.db, req.params.showId);
|
|
318
|
+
res.json({ inWatchlist: !!item, status: item?.status ?? null });
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
res.status(500).json({ error: 'DB error' });
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
getEpisodeProgress = async (req, res) => {
|
|
325
|
+
try {
|
|
326
|
+
const progress = await watched_episodes_repository_1.WatchedEpisodesRepository.getByShowAndEpisode(req.db, req.params.showId, req.params.episodeNumber);
|
|
327
|
+
res.json(progress || { currentTime: 0, duration: 0 });
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
res.status(500).json({ error: 'DB error' });
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
getWatchedEpisodes = async (req, res) => {
|
|
334
|
+
try {
|
|
335
|
+
const episodes = await watched_episodes_repository_1.WatchedEpisodesRepository.getWatchedEpisodeNumbers(req.db, req.params.showId);
|
|
336
|
+
res.json(episodes);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
res.status(500).json({ error: 'DB error' });
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
addToWatchlist = async (req, res) => {
|
|
343
|
+
const { id, status, nativeName, englishName } = req.body;
|
|
344
|
+
let { name, thumbnail, type } = req.body;
|
|
345
|
+
if (id && !id.startsWith('show_')) {
|
|
346
|
+
try {
|
|
347
|
+
const meta = await this.provider.getShowMeta(id);
|
|
348
|
+
if (meta && meta.type) {
|
|
349
|
+
if (!type || type === 'TV')
|
|
350
|
+
type = meta.type;
|
|
351
|
+
if (meta.name && !name)
|
|
352
|
+
name = meta.name;
|
|
353
|
+
if (meta.thumbnail && !thumbnail)
|
|
354
|
+
thumbnail = meta.thumbnail;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
logger_1.default.warn({ id, err: e }, 'Failed to fetch metadata, proceeding with provided data');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
363
|
+
watchlist_repository_1.WatchlistRepository.upsert(tx, {
|
|
364
|
+
id,
|
|
365
|
+
name,
|
|
366
|
+
thumbnail: this.provider.deobfuscateUrl(thumbnail),
|
|
367
|
+
status: status || 'Watching',
|
|
368
|
+
nativeName: nativeName || '',
|
|
369
|
+
englishName: englishName || '',
|
|
370
|
+
type: type || 'TV',
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
await req.db.saveNow();
|
|
374
|
+
res.json({ success: true });
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
logger_1.default.error({ err: error, id, name, payload: req.body }, 'Add to watchlist failed');
|
|
378
|
+
res.status(500).json({ error: 'DB error' });
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
removeFromWatchlist = async (req, res) => {
|
|
382
|
+
const { id } = req.body;
|
|
383
|
+
try {
|
|
384
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
385
|
+
watchlist_repository_1.WatchlistRepository.delete(tx, id);
|
|
386
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.deleteByShow(tx, id);
|
|
387
|
+
notifications_repository_1.NotificationsRepository.deleteByShow(tx, id);
|
|
388
|
+
});
|
|
389
|
+
res.json({ success: true });
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
res.status(500).json({ error: 'DB error' });
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
updateWatchlistStatus = async (req, res) => {
|
|
396
|
+
const { id, status } = req.body;
|
|
397
|
+
try {
|
|
398
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
399
|
+
watchlist_repository_1.WatchlistRepository.updateStatus(tx, id, status);
|
|
400
|
+
});
|
|
401
|
+
res.json({ success: true });
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
res.status(500).json({ error: 'DB error' });
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
getNotifications = async (req, res) => {
|
|
408
|
+
try {
|
|
409
|
+
const db = req.db;
|
|
410
|
+
const watchingShows = await watchlist_repository_1.WatchlistRepository.getWatchingShows(db);
|
|
411
|
+
const notifications = [];
|
|
412
|
+
const BATCH_SIZE = 5;
|
|
413
|
+
for (let i = 0; i < watchingShows.length; i += BATCH_SIZE) {
|
|
414
|
+
const batch = watchingShows.slice(i, i + BATCH_SIZE);
|
|
415
|
+
await Promise.allSettled(batch.map(async (show) => {
|
|
416
|
+
try {
|
|
417
|
+
const [epDetails, watchedEps, dismissedEps, showStatus, discoveredEps] = await Promise.all([
|
|
418
|
+
this.provider.getEpisodes(show.id, 'sub'),
|
|
419
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.getWatchedEpisodeNumbers(db, show.id),
|
|
420
|
+
notifications_repository_1.NotificationsRepository.getDismissedByShow(db, show.id),
|
|
421
|
+
shows_meta_repository_1.ShowsMetaRepository.getStatus(db, show.id),
|
|
422
|
+
notifications_repository_1.NotificationsRepository.getDiscoveredByShow(db, show.id),
|
|
423
|
+
]);
|
|
424
|
+
if (!epDetails || !epDetails.episodes || epDetails.episodes.length === 0)
|
|
425
|
+
return;
|
|
426
|
+
if (showStatus &&
|
|
427
|
+
!['Ongoing', 'Releasing', 'Currently Airing'].includes(showStatus)) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const watchedSet = new Set(watchedEps.map((e) => e.toString()));
|
|
431
|
+
const dismissedSet = new Set(dismissedEps.map((e) => e.episodeNumber.toString()));
|
|
432
|
+
const discoveredSet = new Set(discoveredEps.map((e) => e.episodeNumber.toString()));
|
|
433
|
+
const maxWatched = Math.max(0, ...Array.from(watchedSet).map((e) => parseFloat(e)));
|
|
434
|
+
const episodes = epDetails.episodes;
|
|
435
|
+
const sortedEpisodes = [...episodes].sort((a, b) => parseFloat(a) - parseFloat(b));
|
|
436
|
+
const latestAvailable = sortedEpisodes[sortedEpisodes.length - 1];
|
|
437
|
+
if (parseFloat(latestAvailable) > maxWatched &&
|
|
438
|
+
!watchedSet.has(latestAvailable.toString()) &&
|
|
439
|
+
!dismissedSet.has(latestAvailable.toString()) &&
|
|
440
|
+
!discoveredSet.has(latestAvailable.toString())) {
|
|
441
|
+
await notifications_repository_1.NotificationsRepository.addDiscovered(db, show.id, latestAvailable.toString());
|
|
442
|
+
discoveredSet.add(latestAvailable.toString());
|
|
443
|
+
}
|
|
444
|
+
Array.from(discoveredSet).forEach((epStr) => {
|
|
445
|
+
const epNum = parseFloat(epStr);
|
|
446
|
+
if (epNum > maxWatched && !watchedSet.has(epStr) && !dismissedSet.has(epStr)) {
|
|
447
|
+
notifications.push({
|
|
448
|
+
showId: show.id,
|
|
449
|
+
name: show.name,
|
|
450
|
+
nativeName: show.nativeName,
|
|
451
|
+
englishName: show.englishName,
|
|
452
|
+
thumbnail: this.provider.deobfuscateUrl(show.thumbnail),
|
|
453
|
+
episodeNumber: epStr,
|
|
454
|
+
id: `${show.id}-${epStr}`,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
logger_1.default.error({ err: e, showId: show.id }, 'Failed to fetch notifications for show');
|
|
461
|
+
}
|
|
462
|
+
}));
|
|
463
|
+
if (i + BATCH_SIZE < watchingShows.length) {
|
|
464
|
+
await new Promise((res) => setImmediate(res));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
res.json(notifications.sort((a, b) => parseFloat(b.episodeNumber) - parseFloat(a.episodeNumber)));
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
logger_1.default.error({ err: e }, 'Get notifications failed');
|
|
471
|
+
res.status(500).json({ error: 'Failed to fetch notifications' });
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
dismissNotification = async (req, res) => {
|
|
475
|
+
const { showId, episodeNumber } = req.body;
|
|
476
|
+
try {
|
|
477
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
478
|
+
notifications_repository_1.NotificationsRepository.addDismissed(tx, showId, episodeNumber);
|
|
479
|
+
});
|
|
480
|
+
res.json({ success: true });
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
res.status(500).json({ error: 'DB error' });
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
clearAllNotifications = async (req, res) => {
|
|
487
|
+
const { showId } = req.body;
|
|
488
|
+
try {
|
|
489
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
490
|
+
notifications_repository_1.NotificationsRepository.dismissFromDiscovered(tx, showId);
|
|
491
|
+
});
|
|
492
|
+
res.json({ success: true });
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
res.status(500).json({ error: 'DB error' });
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
exports.WatchlistController = WatchlistController;
|