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.
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/client/dist/assets/AnimeInfo-B88ZA3gl.js +1 -0
- package/client/dist/assets/AnimeInfo-R63luGTP.css +1 -0
- package/client/dist/assets/AnimeInfoPage-xGVarrXG.js +2 -0
- package/client/dist/assets/Button-Fq9KaUOg.css +1 -0
- package/client/dist/assets/Button-lkEUHIDS.js +1 -0
- package/client/dist/assets/ErrorMessage-BVWNgHMx.css +1 -0
- package/client/dist/assets/ErrorMessage-BXKDLzn8.js +1 -0
- package/client/dist/assets/Home-O1FbN8t_.js +1 -0
- package/client/dist/assets/Home-r0eYbWNc.css +1 -0
- package/client/dist/assets/Insights-CF4K-oO5.css +1 -0
- package/client/dist/assets/Insights-opcB-aTo.js +1 -0
- package/client/dist/assets/MAL-BGM33N5c.js +1 -0
- package/client/dist/assets/MAL-DeQNXXPx.css +1 -0
- package/client/dist/assets/Player-BarbgKjI.css +1 -0
- package/client/dist/assets/Player-CSyGax33.js +9 -0
- package/client/dist/assets/PlayerSettings-BaCVQyw6.js +1 -0
- package/client/dist/assets/PlayerSettings-l6aLKrHh.css +1 -0
- package/client/dist/assets/QueueRail-Cxn5U8kE.js +1 -0
- package/client/dist/assets/QueueRail-DOM_pWkE.css +1 -0
- package/client/dist/assets/RemoveConfirmationModal-BBiogSdf.css +1 -0
- package/client/dist/assets/RemoveConfirmationModal-DatCZQKq.js +1 -0
- package/client/dist/assets/Search-BzO-aRP7.css +1 -0
- package/client/dist/assets/Search-DJxo3BYH.js +1 -0
- package/client/dist/assets/SearchableSelect-BkCrf6E8.css +1 -0
- package/client/dist/assets/SearchableSelect-BzYsMz8B.js +1 -0
- package/client/dist/assets/Settings-Bv9fX-x3.css +1 -0
- package/client/dist/assets/Settings-C5adinOe.js +1 -0
- package/client/dist/assets/SynopsisText-C3AK-aRc.js +1 -0
- package/client/dist/assets/SynopsisText-DsI3mW5v.css +1 -0
- package/client/dist/assets/ToggleSwitch-BIlQxIjg.css +1 -0
- package/client/dist/assets/ToggleSwitch-CrXim14A.js +1 -0
- package/client/dist/assets/Watchlist-CXw0vbNx.js +1 -0
- package/client/dist/assets/Watchlist-a2RHQogs.css +1 -0
- package/client/dist/assets/hls.light-DcbkToIY.js +27 -0
- package/client/dist/assets/index-BzX_xmnf.css +1 -0
- package/client/dist/assets/index-Ciivz6fh.js +178 -0
- package/client/dist/assets/useAnimeInfoData-Dqthchpa.js +1 -0
- package/client/dist/assets/useIsMobile-BviODivc.js +1 -0
- package/client/dist/assets/vendor-Bc4EraM_.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 +58 -0
- package/orchestrator.js +323 -0
- package/package.json +88 -0
- package/server/.env +1 -0
- package/server/dist/config.js +89 -0
- package/server/dist/constants.json +1359 -0
- package/server/dist/controllers/auth.controller.js +215 -0
- package/server/dist/controllers/data.controller.js +232 -0
- package/server/dist/controllers/insights.controller.js +200 -0
- package/server/dist/controllers/proxy.controller.js +353 -0
- package/server/dist/controllers/settings.controller.js +159 -0
- package/server/dist/controllers/watchlist.controller.js +749 -0
- package/server/dist/db.js +152 -0
- package/server/dist/discord-rpc.js +279 -0
- package/server/dist/github-sync.js +342 -0
- package/server/dist/google.js +310 -0
- package/server/dist/logger.js +21 -0
- package/server/dist/providers/123anime.provider.js +226 -0
- package/server/dist/providers/allanime.provider.js +736 -0
- package/server/dist/providers/animepahe.provider.js +457 -0
- package/server/dist/providers/animeya.provider.js +787 -0
- package/server/dist/providers/megaplay.provider.js +264 -0
- package/server/dist/providers/provider.interface.js +2 -0
- package/server/dist/rclone.js +126 -0
- package/server/dist/repositories/insights.repository.js +42 -0
- package/server/dist/repositories/notifications.repository.js +30 -0
- package/server/dist/repositories/queue.repository.js +38 -0
- package/server/dist/repositories/settings.repository.js +14 -0
- package/server/dist/repositories/shows-meta.repository.js +41 -0
- package/server/dist/repositories/watched-episodes.repository.js +67 -0
- package/server/dist/repositories/watchlist.repository.js +80 -0
- package/server/dist/routes/auth.routes.js +26 -0
- package/server/dist/routes/data.routes.js +42 -0
- package/server/dist/routes/insights.routes.js +12 -0
- package/server/dist/routes/proxy.routes.js +14 -0
- package/server/dist/routes/settings.routes.js +27 -0
- package/server/dist/routes/watchlist.routes.js +46 -0
- package/server/dist/server.js +229 -0
- package/server/dist/sync-config.js +28 -0
- package/server/dist/sync.js +427 -0
- package/server/dist/utils/db-utils.js +15 -0
- package/server/dist/utils/env.utils.js +79 -0
- package/server/dist/utils/machine-id.js +46 -0
- package/server/dist/utils/request-context.js +5 -0
- package/server/package.json +19 -0
|
@@ -0,0 +1,749 @@
|
|
|
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
|
+
const queue_repository_1 = require("../repositories/queue.repository");
|
|
14
|
+
const settings_repository_1 = require("../repositories/settings.repository");
|
|
15
|
+
const discord_rpc_1 = require("../discord-rpc");
|
|
16
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
17
|
+
class WatchlistController {
|
|
18
|
+
activeTypeFetches = new Set();
|
|
19
|
+
allAnime;
|
|
20
|
+
animePahe;
|
|
21
|
+
constructor(providers) {
|
|
22
|
+
this.allAnime = providers.allAnime;
|
|
23
|
+
this.animePahe = providers.animePahe;
|
|
24
|
+
}
|
|
25
|
+
getProviderForId(showId) {
|
|
26
|
+
if (UUID_RE.test(showId) && this.animePahe)
|
|
27
|
+
return this.animePahe;
|
|
28
|
+
return this.allAnime;
|
|
29
|
+
}
|
|
30
|
+
deobfuscateUrl(url, showId) {
|
|
31
|
+
if (!showId || !UUID_RE.test(showId)) {
|
|
32
|
+
return this.allAnime.deobfuscateUrl(url);
|
|
33
|
+
}
|
|
34
|
+
return url;
|
|
35
|
+
}
|
|
36
|
+
normalizeFilterValue(value) {
|
|
37
|
+
if (typeof value !== 'string')
|
|
38
|
+
return undefined;
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
return trimmed && trimmed !== 'ALL' ? trimmed : undefined;
|
|
41
|
+
}
|
|
42
|
+
getWatchlistFilters(query) {
|
|
43
|
+
return {
|
|
44
|
+
query: this.normalizeFilterValue(query.query),
|
|
45
|
+
type: this.normalizeFilterValue(query.type),
|
|
46
|
+
season: this.normalizeFilterValue(query.season),
|
|
47
|
+
year: this.normalizeFilterValue(query.year),
|
|
48
|
+
country: this.normalizeFilterValue(query.country),
|
|
49
|
+
translation: this.normalizeFilterValue(query.translation),
|
|
50
|
+
genres: this.normalizeFilterValue(query.genres),
|
|
51
|
+
excludeGenres: this.normalizeFilterValue(query.excludeGenres),
|
|
52
|
+
tags: this.normalizeFilterValue(query.tags),
|
|
53
|
+
excludeTags: this.normalizeFilterValue(query.excludeTags),
|
|
54
|
+
studios: this.normalizeFilterValue(query.studios),
|
|
55
|
+
sortBy: this.normalizeFilterValue(query.sortBy),
|
|
56
|
+
titlePreference: ['name', 'nativeName', 'englishName'].includes(String(query.titlePreference))
|
|
57
|
+
? query.titlePreference
|
|
58
|
+
: 'name',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
hasProviderFilters(filters) {
|
|
62
|
+
return !!(filters.season ||
|
|
63
|
+
filters.year ||
|
|
64
|
+
filters.country ||
|
|
65
|
+
filters.translation ||
|
|
66
|
+
filters.genres ||
|
|
67
|
+
filters.excludeGenres ||
|
|
68
|
+
filters.tags ||
|
|
69
|
+
filters.excludeTags ||
|
|
70
|
+
filters.studios);
|
|
71
|
+
}
|
|
72
|
+
matchesLocalFilters(row, filters) {
|
|
73
|
+
if (filters.query) {
|
|
74
|
+
const needle = filters.query.toLowerCase();
|
|
75
|
+
const haystack = [row.name, row.nativeName, row.englishName]
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join(' ')
|
|
78
|
+
.toLowerCase();
|
|
79
|
+
if (!haystack.includes(needle))
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (filters.type && row.type !== filters.type)
|
|
83
|
+
return false;
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
sortFilteredRows(rows, filters) {
|
|
87
|
+
const getSortTitle = (row) => {
|
|
88
|
+
const preferredTitle = filters.titlePreference ? row[filters.titlePreference] : undefined;
|
|
89
|
+
return preferredTitle || row.name || '';
|
|
90
|
+
};
|
|
91
|
+
if (filters.sortBy === 'name_asc') {
|
|
92
|
+
return [...rows].sort((a, b) => getSortTitle(a).localeCompare(getSortTitle(b)));
|
|
93
|
+
}
|
|
94
|
+
if (filters.sortBy === 'name_desc') {
|
|
95
|
+
return [...rows].sort((a, b) => getSortTitle(b).localeCompare(getSortTitle(a)));
|
|
96
|
+
}
|
|
97
|
+
return rows;
|
|
98
|
+
}
|
|
99
|
+
async getProviderMatchedIds(filters) {
|
|
100
|
+
if (!this.hasProviderFilters(filters))
|
|
101
|
+
return new Set();
|
|
102
|
+
const searchOptions = {
|
|
103
|
+
season: filters.season,
|
|
104
|
+
year: filters.year,
|
|
105
|
+
country: filters.country,
|
|
106
|
+
translation: filters.translation,
|
|
107
|
+
genres: filters.genres,
|
|
108
|
+
excludeGenres: filters.excludeGenres,
|
|
109
|
+
tags: filters.tags,
|
|
110
|
+
excludeTags: filters.excludeTags,
|
|
111
|
+
studios: filters.studios,
|
|
112
|
+
};
|
|
113
|
+
const ids = new Set();
|
|
114
|
+
const maxPages = 25;
|
|
115
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
116
|
+
const results = await this.allAnime.search({ ...searchOptions, page: String(page) });
|
|
117
|
+
for (const show of results)
|
|
118
|
+
ids.add(show._id);
|
|
119
|
+
if (results.length < 28)
|
|
120
|
+
break;
|
|
121
|
+
await new Promise((res) => setImmediate(res));
|
|
122
|
+
}
|
|
123
|
+
return ids;
|
|
124
|
+
}
|
|
125
|
+
async filterWatchlistRows(rows, filters) {
|
|
126
|
+
let filtered = rows.filter((row) => this.matchesLocalFilters(row, filters));
|
|
127
|
+
if (this.hasProviderFilters(filters)) {
|
|
128
|
+
const matchedIds = await this.getProviderMatchedIds(filters);
|
|
129
|
+
filtered = filtered.filter((row) => matchedIds.has(row.id));
|
|
130
|
+
}
|
|
131
|
+
return this.sortFilteredRows(filtered, filters);
|
|
132
|
+
}
|
|
133
|
+
async getContinueWatchingData(req, limit) {
|
|
134
|
+
const rows = await watched_episodes_repository_1.WatchedEpisodesRepository.getContinueWatching(req.db, limit);
|
|
135
|
+
const showsNeedingEpisodes = rows.filter((show) => {
|
|
136
|
+
const isAnimePahe = this.getProviderForId(show.id).name === 'AnimePahe';
|
|
137
|
+
const watchedCount = show.watchedCount || 0;
|
|
138
|
+
return (isAnimePahe || !show.episodeCount || (watchedCount > 0 && show.episodeCount <= watchedCount));
|
|
139
|
+
});
|
|
140
|
+
const episodeFetchResults = new Map();
|
|
141
|
+
const episodeMappingResults = new Map();
|
|
142
|
+
if (showsNeedingEpisodes.length > 0) {
|
|
143
|
+
const BATCH_SIZE = 5;
|
|
144
|
+
for (let i = 0; i < showsNeedingEpisodes.length; i += BATCH_SIZE) {
|
|
145
|
+
const batch = showsNeedingEpisodes.slice(i, i + BATCH_SIZE);
|
|
146
|
+
const batchResults = await Promise.allSettled(batch.map((show) => this.getProviderForId(show.id).getEpisodes(show.id, 'sub')));
|
|
147
|
+
batch.forEach((show, index) => {
|
|
148
|
+
const result = batchResults[index];
|
|
149
|
+
if (result.status === 'fulfilled' && result.value?.episodes) {
|
|
150
|
+
const epList = result.value.episodes;
|
|
151
|
+
const epCount = epList.length;
|
|
152
|
+
episodeFetchResults.set(show.id, epCount);
|
|
153
|
+
episodeMappingResults.set(show.id, epList);
|
|
154
|
+
try {
|
|
155
|
+
shows_meta_repository_1.ShowsMetaRepository.updateEpisodeCount(req.db, show.id, epCount);
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
logger_1.default.error({ err: e, showId: show.id }, 'Failed to update episode count in DB');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const enrichedRows = rows.map((show) => {
|
|
165
|
+
const epCount = episodeFetchResults.get(show.id) ?? show.episodeCount;
|
|
166
|
+
const epList = episodeMappingResults.get(show.id);
|
|
167
|
+
let relativeEpisodeNumber = show.episodeNumber;
|
|
168
|
+
if (epList) {
|
|
169
|
+
const sortedEpList = [...epList].sort((a, b) => Number(a) - Number(b));
|
|
170
|
+
const idx = sortedEpList.indexOf(String(show.episodeNumber));
|
|
171
|
+
if (idx !== -1) {
|
|
172
|
+
relativeEpisodeNumber = (idx + 1).toString();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
...show,
|
|
177
|
+
episodeCount: epCount,
|
|
178
|
+
relativeEpisodeNumber: relativeEpisodeNumber,
|
|
179
|
+
type: show.type || show.smType,
|
|
180
|
+
thumbnail: this.deobfuscateUrl(show.thumbnail ?? '', show.id),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
setImmediate(async () => {
|
|
184
|
+
if (req.db.isClosedCheck())
|
|
185
|
+
return;
|
|
186
|
+
const delay = () => new Promise((res) => setImmediate(res));
|
|
187
|
+
for (const show of enrichedRows) {
|
|
188
|
+
const currentThumbnail = show.thumbnail || '';
|
|
189
|
+
const fixedThumbnail = this.deobfuscateUrl(currentThumbnail, show.id);
|
|
190
|
+
const needsThumbnailUpdate = fixedThumbnail !== currentThumbnail;
|
|
191
|
+
if ((!show.type || needsThumbnailUpdate) && !this.activeTypeFetches.has(show.id)) {
|
|
192
|
+
this.activeTypeFetches.add(show.id);
|
|
193
|
+
try {
|
|
194
|
+
let didUpdate = false;
|
|
195
|
+
if (needsThumbnailUpdate && !req.db.isClosedCheck()) {
|
|
196
|
+
await watchlist_repository_1.WatchlistRepository.updateThumbnail(req.db, show.id, fixedThumbnail);
|
|
197
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateThumbnail(req.db, show.id, fixedThumbnail);
|
|
198
|
+
didUpdate = true;
|
|
199
|
+
}
|
|
200
|
+
if (!show.type) {
|
|
201
|
+
const meta = await this.getProviderForId(show.id).getShowMeta(show.id, req.headers['x-animepahe-ua'], req.headers['x-animepahe-cookie']);
|
|
202
|
+
if (meta && meta.type && !req.db.isClosedCheck()) {
|
|
203
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateType(req.db, show.id, meta.type);
|
|
204
|
+
await watchlist_repository_1.WatchlistRepository.updateType(req.db, show.id, meta.type);
|
|
205
|
+
didUpdate = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (didUpdate)
|
|
209
|
+
req.db.scheduleSave();
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
logger_1.default.error({ err: e, showId: show.id }, 'Lazy migration error for show');
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
this.activeTypeFetches.delete(show.id);
|
|
216
|
+
}
|
|
217
|
+
await delay();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return enrichedRows;
|
|
222
|
+
}
|
|
223
|
+
async getUpNextShowsData(req) {
|
|
224
|
+
const watchingShows = await watched_episodes_repository_1.WatchedEpisodesRepository.getUpNextShows(req.db);
|
|
225
|
+
if (watchingShows.length === 0)
|
|
226
|
+
return [];
|
|
227
|
+
const showIds = watchingShows.map((s) => s.id);
|
|
228
|
+
const allWatchedEps = await watched_episodes_repository_1.WatchedEpisodesRepository.getEpisodesForShows(req.db, showIds);
|
|
229
|
+
const watchedByShow = new Map();
|
|
230
|
+
for (const ep of allWatchedEps) {
|
|
231
|
+
const arr = watchedByShow.get(ep.showId);
|
|
232
|
+
if (arr)
|
|
233
|
+
arr.push(ep);
|
|
234
|
+
else
|
|
235
|
+
watchedByShow.set(ep.showId, [ep]);
|
|
236
|
+
}
|
|
237
|
+
const BATCH_SIZE = 5;
|
|
238
|
+
const upNextShows = [];
|
|
239
|
+
for (let i = 0; i < watchingShows.length; i += BATCH_SIZE) {
|
|
240
|
+
const batch = watchingShows.slice(i, i + BATCH_SIZE);
|
|
241
|
+
const batchResults = await Promise.allSettled(batch.map(async (show) => {
|
|
242
|
+
try {
|
|
243
|
+
const epDetails = await this.getProviderForId(show.id).getEpisodes(show.id, 'sub');
|
|
244
|
+
const watchedEpisodesResult = watchedByShow.get(show.id) ?? [];
|
|
245
|
+
const allEps = epDetails?.episodes?.sort((a, b) => parseFloat(a) - parseFloat(b)) || [];
|
|
246
|
+
const watchedEpsMap = new Map(watchedEpisodesResult.map((r) => [r.episodeNumber.toString(), r]));
|
|
247
|
+
const unwatchedEps = allEps.filter((ep) => !watchedEpsMap.has(ep));
|
|
248
|
+
if (unwatchedEps.length > 0) {
|
|
249
|
+
return {
|
|
250
|
+
_id: show.id,
|
|
251
|
+
id: show.id,
|
|
252
|
+
name: show.name,
|
|
253
|
+
thumbnail: this.deobfuscateUrl(show.thumbnail ?? '', show.id),
|
|
254
|
+
nativeName: show.nativeName,
|
|
255
|
+
englishName: show.englishName,
|
|
256
|
+
type: show.type || show.smType,
|
|
257
|
+
nextEpisodeToWatch: unwatchedEps[0],
|
|
258
|
+
newEpisodesCount: unwatchedEps.length,
|
|
259
|
+
episodeCount: allEps.length,
|
|
260
|
+
watchedCount: watchedEpsMap.size,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
logger_1.default.error({ err: e, showId: show.id }, 'Error processing show for Up Next list');
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}));
|
|
270
|
+
for (const result of batchResults) {
|
|
271
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
272
|
+
upNextShows.push(result.value);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (i + BATCH_SIZE < watchingShows.length) {
|
|
276
|
+
await new Promise((res) => setImmediate(res));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return upNextShows;
|
|
280
|
+
}
|
|
281
|
+
getContinueWatchingFast = async (req, res) => {
|
|
282
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
283
|
+
const data = await this.getContinueWatchingData(req, limit);
|
|
284
|
+
res.json(data);
|
|
285
|
+
};
|
|
286
|
+
getContinueWatchingUpNext = async (req, res) => {
|
|
287
|
+
const data = await this.getUpNextShowsData(req);
|
|
288
|
+
res.json(data);
|
|
289
|
+
};
|
|
290
|
+
getContinueWatching = async (req, res) => {
|
|
291
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
292
|
+
const data = await this.getContinueWatchingData(req);
|
|
293
|
+
res.json(data.slice(0, limit));
|
|
294
|
+
};
|
|
295
|
+
getAllContinueWatching = async (req, res) => {
|
|
296
|
+
const page = parseInt(req.query.page) || 1;
|
|
297
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
298
|
+
const offset = (page - 1) * limit;
|
|
299
|
+
const filters = this.getWatchlistFilters(req.query);
|
|
300
|
+
const data = await this.filterWatchlistRows(await this.getContinueWatchingData(req), filters);
|
|
301
|
+
res.json({
|
|
302
|
+
data: data.slice(offset, offset + limit),
|
|
303
|
+
total: data.length,
|
|
304
|
+
page,
|
|
305
|
+
limit,
|
|
306
|
+
});
|
|
307
|
+
};
|
|
308
|
+
updateProgress = async (req, res) => {
|
|
309
|
+
const { showId, episodeNumber, currentTime, duration, showName, showThumbnail, nativeName, englishName, genres, popularityScore, type, status, episodeCount, isPlaying, sessionId, } = req.body;
|
|
310
|
+
const titlePreferenceRow = await settings_repository_1.SettingsRepository.getByKey(req.db, 'titlePreference');
|
|
311
|
+
const titlePreference = titlePreferenceRow ? titlePreferenceRow.value : 'englishName';
|
|
312
|
+
let displayName = showName;
|
|
313
|
+
if (titlePreference === 'englishName' && englishName) {
|
|
314
|
+
displayName = englishName;
|
|
315
|
+
}
|
|
316
|
+
else if (titlePreference === 'nativeName' && nativeName) {
|
|
317
|
+
displayName = nativeName;
|
|
318
|
+
}
|
|
319
|
+
let actualEpisodeNumber = episodeNumber;
|
|
320
|
+
if (this.getProviderForId(showId).name === 'AnimePahe') {
|
|
321
|
+
const epData = await this.getProviderForId(showId).getEpisodes(showId, 'sub', req.headers['x-animepahe-ua'], req.headers['x-animepahe-cookie']);
|
|
322
|
+
if (epData && epData.episodes) {
|
|
323
|
+
const epList = epData.episodes.sort((a, b) => parseFloat(a) - parseFloat(b));
|
|
324
|
+
const idx = epList.indexOf(String(episodeNumber));
|
|
325
|
+
if (idx !== -1) {
|
|
326
|
+
actualEpisodeNumber = idx + 1;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let discordThumbnails;
|
|
331
|
+
try {
|
|
332
|
+
const meta = await this.getProviderForId(showId).getShowMeta(showId);
|
|
333
|
+
if (meta?.thumbnails)
|
|
334
|
+
discordThumbnails = meta.thumbnails;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Non-critical, Discord will fall back to logo
|
|
338
|
+
}
|
|
339
|
+
discord_rpc_1.discordRPCService.updatePresence({
|
|
340
|
+
title: displayName,
|
|
341
|
+
episode: String(actualEpisodeNumber),
|
|
342
|
+
totalEpisodes: episodeCount ? String(episodeCount) : undefined,
|
|
343
|
+
currentTime: currentTime || 0,
|
|
344
|
+
duration: duration || 0,
|
|
345
|
+
thumbnail: this.deobfuscateUrl(showThumbnail || '', showId),
|
|
346
|
+
isPlaying: isPlaying !== false,
|
|
347
|
+
providerName: this.getProviderForId(showId).name,
|
|
348
|
+
sessionId,
|
|
349
|
+
thumbnails: discordThumbnails,
|
|
350
|
+
});
|
|
351
|
+
const genresStr = Array.isArray(genres) ? JSON.stringify(genres) : genres;
|
|
352
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
353
|
+
shows_meta_repository_1.ShowsMetaRepository.upsert(tx, {
|
|
354
|
+
id: showId,
|
|
355
|
+
name: showName,
|
|
356
|
+
thumbnail: this.deobfuscateUrl(showThumbnail, showId),
|
|
357
|
+
nativeName,
|
|
358
|
+
englishName,
|
|
359
|
+
genres: genresStr,
|
|
360
|
+
popularityScore,
|
|
361
|
+
status,
|
|
362
|
+
episodeCount,
|
|
363
|
+
type,
|
|
364
|
+
});
|
|
365
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.upsert(tx, {
|
|
366
|
+
showId,
|
|
367
|
+
episodeNumber,
|
|
368
|
+
currentTime,
|
|
369
|
+
duration,
|
|
370
|
+
});
|
|
371
|
+
notifications_repository_1.NotificationsRepository.deleteSpecificDismissed(tx, showId, episodeNumber);
|
|
372
|
+
});
|
|
373
|
+
req.db.scheduleSave();
|
|
374
|
+
res.json({ success: true });
|
|
375
|
+
};
|
|
376
|
+
removeContinueWatching = async (req, res) => {
|
|
377
|
+
const { showId } = req.body;
|
|
378
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
379
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.deleteByShow(tx, showId);
|
|
380
|
+
notifications_repository_1.NotificationsRepository.deleteByShow(tx, showId);
|
|
381
|
+
});
|
|
382
|
+
res.json({ success: true });
|
|
383
|
+
};
|
|
384
|
+
getWatchlist = async (req, res) => {
|
|
385
|
+
const { status, page: pageStr, limit: limitStr } = req.query;
|
|
386
|
+
const page = parseInt(pageStr) || 1;
|
|
387
|
+
const limit = parseInt(limitStr) || 10;
|
|
388
|
+
const offset = (page - 1) * limit;
|
|
389
|
+
const filters = this.getWatchlistFilters(req.query);
|
|
390
|
+
const allRows = await watchlist_repository_1.WatchlistRepository.getAll(req.db, status);
|
|
391
|
+
const filteredRows = await this.filterWatchlistRows(allRows, filters);
|
|
392
|
+
const rows = filteredRows.slice(offset, offset + limit);
|
|
393
|
+
res.json({
|
|
394
|
+
data: rows.map((row) => ({
|
|
395
|
+
...row,
|
|
396
|
+
_id: row.id,
|
|
397
|
+
thumbnail: this.deobfuscateUrl(row.thumbnail || '', row.id),
|
|
398
|
+
})),
|
|
399
|
+
total: filteredRows.length,
|
|
400
|
+
page,
|
|
401
|
+
limit,
|
|
402
|
+
});
|
|
403
|
+
setImmediate(async () => {
|
|
404
|
+
if (req.db.isClosedCheck())
|
|
405
|
+
return;
|
|
406
|
+
const delay = () => new Promise((res) => setImmediate(res));
|
|
407
|
+
for (const row of rows) {
|
|
408
|
+
const currentThumbnail = row.thumbnail || '';
|
|
409
|
+
const fixedThumbnail = this.deobfuscateUrl(currentThumbnail, row.id);
|
|
410
|
+
const needsThumbnailUpdate = fixedThumbnail !== currentThumbnail;
|
|
411
|
+
if ((!row.type || needsThumbnailUpdate) && !this.activeTypeFetches.has(row.id)) {
|
|
412
|
+
this.activeTypeFetches.add(row.id);
|
|
413
|
+
try {
|
|
414
|
+
let didUpdate = false;
|
|
415
|
+
if (needsThumbnailUpdate && !req.db.isClosedCheck()) {
|
|
416
|
+
await watchlist_repository_1.WatchlistRepository.updateThumbnail(req.db, row.id, fixedThumbnail);
|
|
417
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateThumbnail(req.db, row.id, fixedThumbnail);
|
|
418
|
+
didUpdate = true;
|
|
419
|
+
}
|
|
420
|
+
if (!row.type) {
|
|
421
|
+
const meta = await this.getProviderForId(row.id).getShowMeta(row.id, req.headers['x-animepahe-ua'], req.headers['x-animepahe-cookie']);
|
|
422
|
+
if (meta && !req.db.isClosedCheck()) {
|
|
423
|
+
if (meta.type) {
|
|
424
|
+
await watchlist_repository_1.WatchlistRepository.updateType(req.db, row.id, meta.type);
|
|
425
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateType(req.db, row.id, meta.type);
|
|
426
|
+
didUpdate = true;
|
|
427
|
+
}
|
|
428
|
+
if (meta.thumbnail) {
|
|
429
|
+
const metaThumb = this.deobfuscateUrl(meta.thumbnail, row.id);
|
|
430
|
+
if (metaThumb !== fixedThumbnail) {
|
|
431
|
+
await watchlist_repository_1.WatchlistRepository.updateThumbnail(req.db, row.id, metaThumb);
|
|
432
|
+
await shows_meta_repository_1.ShowsMetaRepository.updateThumbnail(req.db, row.id, metaThumb);
|
|
433
|
+
didUpdate = true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (didUpdate)
|
|
439
|
+
req.db.scheduleSave();
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
logger_1.default.error({ err: e, showId: row.id }, 'Watchlist lazy migration error');
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
this.activeTypeFetches.delete(row.id);
|
|
446
|
+
}
|
|
447
|
+
await delay();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
checkWatchlist = async (req, res) => {
|
|
453
|
+
const item = await watchlist_repository_1.WatchlistRepository.getById(req.db, req.params.showId);
|
|
454
|
+
res.json({ inWatchlist: !!item, status: item?.status ?? null });
|
|
455
|
+
};
|
|
456
|
+
getQueue = async (req, res) => {
|
|
457
|
+
const rows = await queue_repository_1.QueueRepository.getAll(req.db);
|
|
458
|
+
res.json(rows.map((row) => ({
|
|
459
|
+
...row,
|
|
460
|
+
_id: row.showId,
|
|
461
|
+
id: row.id,
|
|
462
|
+
thumbnail: this.deobfuscateUrl(row.thumbnail || '', row.showId),
|
|
463
|
+
})));
|
|
464
|
+
};
|
|
465
|
+
addToQueue = async (req, res) => {
|
|
466
|
+
const { showId, episodeNumber, showName, showThumbnail, nativeName, englishName, type } = req.body;
|
|
467
|
+
if (!showId || !episodeNumber) {
|
|
468
|
+
return res.status(400).json({ error: 'showId and episodeNumber are required' });
|
|
469
|
+
}
|
|
470
|
+
const existing = await queue_repository_1.QueueRepository.getByEpisode(req.db, showId, String(episodeNumber));
|
|
471
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
472
|
+
if (showName || showThumbnail || nativeName || englishName || type) {
|
|
473
|
+
shows_meta_repository_1.ShowsMetaRepository.upsert(tx, {
|
|
474
|
+
id: showId,
|
|
475
|
+
name: showName || '',
|
|
476
|
+
thumbnail: this.deobfuscateUrl(showThumbnail || '', showId),
|
|
477
|
+
nativeName,
|
|
478
|
+
englishName,
|
|
479
|
+
type,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
if (existing) {
|
|
483
|
+
queue_repository_1.QueueRepository.removeEpisode(tx, showId, String(episodeNumber));
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
queue_repository_1.QueueRepository.addToEnd(tx, showId, String(episodeNumber));
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
req.db.scheduleSave();
|
|
490
|
+
res.json({ success: true, queued: !existing });
|
|
491
|
+
};
|
|
492
|
+
removeFromQueue = async (req, res) => {
|
|
493
|
+
const { showId, episodeNumber } = req.body;
|
|
494
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
495
|
+
queue_repository_1.QueueRepository.removeEpisode(tx, showId, String(episodeNumber));
|
|
496
|
+
});
|
|
497
|
+
res.json({ success: true });
|
|
498
|
+
};
|
|
499
|
+
clearQueue = async (req, res) => {
|
|
500
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
501
|
+
queue_repository_1.QueueRepository.clear(tx);
|
|
502
|
+
});
|
|
503
|
+
res.json({ success: true });
|
|
504
|
+
};
|
|
505
|
+
reorderQueue = async (req, res) => {
|
|
506
|
+
const { items } = req.body;
|
|
507
|
+
if (!Array.isArray(items)) {
|
|
508
|
+
return res.status(400).json({ error: 'items must be an array' });
|
|
509
|
+
}
|
|
510
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
511
|
+
queue_repository_1.QueueRepository.reorder(tx, items);
|
|
512
|
+
});
|
|
513
|
+
res.json({ success: true });
|
|
514
|
+
};
|
|
515
|
+
getSuggestedQueueEpisode = async (req, res) => {
|
|
516
|
+
const showId = req.params.showId;
|
|
517
|
+
const resumeProgress = await watched_episodes_repository_1.WatchedEpisodesRepository.getLatestResumeProgress(req.db, showId);
|
|
518
|
+
if (resumeProgress) {
|
|
519
|
+
return res.json({
|
|
520
|
+
showId,
|
|
521
|
+
episodeNumber: resumeProgress.episodeNumber,
|
|
522
|
+
resumeTime: resumeProgress.currentTime || 0,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
const [watchedEpisodes, episodeData] = await Promise.all([
|
|
526
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.getByShow(req.db, showId),
|
|
527
|
+
this.getProviderForId(showId)
|
|
528
|
+
.getEpisodes(showId, 'sub')
|
|
529
|
+
.catch(() => null),
|
|
530
|
+
]);
|
|
531
|
+
const watchedSet = new Set(watchedEpisodes.map((ep) => ep.episodeNumber.toString()));
|
|
532
|
+
const episodes = episodeData?.episodes?.length
|
|
533
|
+
? [...episodeData.episodes].sort((a, b) => parseFloat(a) - parseFloat(b))
|
|
534
|
+
: [];
|
|
535
|
+
const finishedEpisodes = watchedEpisodes
|
|
536
|
+
.filter((ep) => ep.duration > 0 && ep.currentTime >= ep.duration * 0.8)
|
|
537
|
+
.map((ep) => parseFloat(ep.episodeNumber))
|
|
538
|
+
.filter((ep) => !Number.isNaN(ep));
|
|
539
|
+
const nextAfterFinished = finishedEpisodes.length > 0 ? String(Math.max(...finishedEpisodes) + 1) : undefined;
|
|
540
|
+
const episodeNumber = (nextAfterFinished &&
|
|
541
|
+
episodes.includes(nextAfterFinished) &&
|
|
542
|
+
!watchedSet.has(nextAfterFinished)
|
|
543
|
+
? nextAfterFinished
|
|
544
|
+
: episodes.find((ep) => !watchedSet.has(ep))) ||
|
|
545
|
+
episodes[0] ||
|
|
546
|
+
'1';
|
|
547
|
+
res.json({ showId, episodeNumber, resumeTime: 0 });
|
|
548
|
+
};
|
|
549
|
+
getEpisodeProgress = async (req, res) => {
|
|
550
|
+
const progress = await watched_episodes_repository_1.WatchedEpisodesRepository.getByShowAndEpisode(req.db, req.params.showId, req.params.episodeNumber);
|
|
551
|
+
res.json(progress || { currentTime: 0, duration: 0 });
|
|
552
|
+
};
|
|
553
|
+
getWatchedEpisodes = async (req, res) => {
|
|
554
|
+
const episodes = await watched_episodes_repository_1.WatchedEpisodesRepository.getWatchedEpisodeNumbers(req.db, req.params.showId);
|
|
555
|
+
res.json(episodes);
|
|
556
|
+
};
|
|
557
|
+
addToWatchlist = async (req, res) => {
|
|
558
|
+
const { id, status, nativeName, englishName } = req.body;
|
|
559
|
+
let { name, thumbnail, type } = req.body;
|
|
560
|
+
if (id && !id.startsWith('show_')) {
|
|
561
|
+
try {
|
|
562
|
+
const meta = await this.getProviderForId(id).getShowMeta(id, req.headers['x-animepahe-ua'], req.headers['x-animepahe-cookie']);
|
|
563
|
+
if (meta && meta.type) {
|
|
564
|
+
if (!type || type === 'TV')
|
|
565
|
+
type = meta.type;
|
|
566
|
+
if (meta.name && !name)
|
|
567
|
+
name = meta.name;
|
|
568
|
+
if (meta.thumbnail && !thumbnail)
|
|
569
|
+
thumbnail = meta.thumbnail;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
logger_1.default.warn({ id, err: e }, 'Failed to fetch metadata, proceeding with provided data');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
577
|
+
watchlist_repository_1.WatchlistRepository.upsert(tx, {
|
|
578
|
+
id,
|
|
579
|
+
name,
|
|
580
|
+
thumbnail: this.deobfuscateUrl(thumbnail, id),
|
|
581
|
+
status: status || 'Watching',
|
|
582
|
+
nativeName: nativeName || '',
|
|
583
|
+
englishName: englishName || '',
|
|
584
|
+
type: type || 'TV',
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
await req.db.saveNow();
|
|
588
|
+
res.json({ success: true });
|
|
589
|
+
};
|
|
590
|
+
removeFromWatchlist = async (req, res) => {
|
|
591
|
+
const { id } = req.body;
|
|
592
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
593
|
+
watchlist_repository_1.WatchlistRepository.delete(tx, id);
|
|
594
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.deleteByShow(tx, id);
|
|
595
|
+
notifications_repository_1.NotificationsRepository.deleteByShow(tx, id);
|
|
596
|
+
});
|
|
597
|
+
res.json({ success: true });
|
|
598
|
+
};
|
|
599
|
+
updateWatchlistStatus = async (req, res) => {
|
|
600
|
+
const { id, status } = req.body;
|
|
601
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
602
|
+
watchlist_repository_1.WatchlistRepository.updateStatus(tx, id, status);
|
|
603
|
+
});
|
|
604
|
+
res.json({ success: true });
|
|
605
|
+
};
|
|
606
|
+
getNotifications = async (req, res) => {
|
|
607
|
+
const db = req.db;
|
|
608
|
+
const watchingShows = await watchlist_repository_1.WatchlistRepository.getWatchingShows(db);
|
|
609
|
+
const notifications = [];
|
|
610
|
+
const BATCH_SIZE = 5;
|
|
611
|
+
for (let i = 0; i < watchingShows.length; i += BATCH_SIZE) {
|
|
612
|
+
const batch = watchingShows.slice(i, i + BATCH_SIZE);
|
|
613
|
+
await Promise.allSettled(batch.map(async (show) => {
|
|
614
|
+
try {
|
|
615
|
+
const [epDetails, watchedEps, dismissedEps, showStatus, discoveredEps] = await Promise.all([
|
|
616
|
+
this.getProviderForId(show.id).getEpisodes(show.id, 'sub'),
|
|
617
|
+
watched_episodes_repository_1.WatchedEpisodesRepository.getWatchedEpisodeNumbers(db, show.id),
|
|
618
|
+
notifications_repository_1.NotificationsRepository.getDismissedByShow(db, show.id),
|
|
619
|
+
shows_meta_repository_1.ShowsMetaRepository.getStatus(db, show.id),
|
|
620
|
+
notifications_repository_1.NotificationsRepository.getDiscoveredByShow(db, show.id),
|
|
621
|
+
]);
|
|
622
|
+
if (!epDetails || !epDetails.episodes || epDetails.episodes.length === 0)
|
|
623
|
+
return;
|
|
624
|
+
if (showStatus && !['Ongoing', 'Releasing', 'Currently Airing'].includes(showStatus)) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const watchedSet = new Set(watchedEps.map((e) => e.toString()));
|
|
628
|
+
const dismissedSet = new Set(dismissedEps.map((e) => e.episodeNumber.toString()));
|
|
629
|
+
const discoveredSet = new Set(discoveredEps.map((e) => e.episodeNumber.toString()));
|
|
630
|
+
const maxWatched = Math.max(0, ...Array.from(watchedSet).map((e) => parseFloat(e)));
|
|
631
|
+
const episodes = epDetails.episodes;
|
|
632
|
+
const sortedEpisodes = [...episodes].sort((a, b) => parseFloat(a) - parseFloat(b));
|
|
633
|
+
const latestAvailable = sortedEpisodes[sortedEpisodes.length - 1];
|
|
634
|
+
if (parseFloat(latestAvailable) > maxWatched &&
|
|
635
|
+
!watchedSet.has(latestAvailable.toString()) &&
|
|
636
|
+
!dismissedSet.has(latestAvailable.toString()) &&
|
|
637
|
+
!discoveredSet.has(latestAvailable.toString())) {
|
|
638
|
+
await notifications_repository_1.NotificationsRepository.addDiscovered(db, show.id, latestAvailable.toString());
|
|
639
|
+
discoveredSet.add(latestAvailable.toString());
|
|
640
|
+
}
|
|
641
|
+
Array.from(discoveredSet).forEach((epStr) => {
|
|
642
|
+
const epNum = parseFloat(epStr);
|
|
643
|
+
if (epNum > maxWatched && !watchedSet.has(epStr) && !dismissedSet.has(epStr)) {
|
|
644
|
+
notifications.push({
|
|
645
|
+
showId: show.id,
|
|
646
|
+
name: show.name,
|
|
647
|
+
nativeName: show.nativeName,
|
|
648
|
+
englishName: show.englishName,
|
|
649
|
+
thumbnail: this.deobfuscateUrl(show.thumbnail, show.id),
|
|
650
|
+
episodeNumber: epStr,
|
|
651
|
+
id: `${show.id}-${epStr}`,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
catch (e) {
|
|
657
|
+
logger_1.default.error({ err: e, showId: show.id }, 'Failed to fetch notifications for show');
|
|
658
|
+
}
|
|
659
|
+
}));
|
|
660
|
+
if (i + BATCH_SIZE < watchingShows.length) {
|
|
661
|
+
await new Promise((res) => setImmediate(res));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
res.json(notifications.sort((a, b) => parseFloat(b.episodeNumber) - parseFloat(a.episodeNumber)));
|
|
665
|
+
};
|
|
666
|
+
dismissNotification = async (req, res) => {
|
|
667
|
+
const { showId, episodeNumber } = req.body;
|
|
668
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
669
|
+
notifications_repository_1.NotificationsRepository.addDismissed(tx, showId, episodeNumber);
|
|
670
|
+
});
|
|
671
|
+
res.json({ success: true });
|
|
672
|
+
};
|
|
673
|
+
clearAllNotifications = async (req, res) => {
|
|
674
|
+
const { showId } = req.body;
|
|
675
|
+
await (0, sync_1.performWriteTransaction)(req.db, (tx) => {
|
|
676
|
+
notifications_repository_1.NotificationsRepository.dismissFromDiscovered(tx, showId);
|
|
677
|
+
});
|
|
678
|
+
res.json({ success: true });
|
|
679
|
+
};
|
|
680
|
+
getThisWeekSchedule = async (req, res) => {
|
|
681
|
+
const db = req.db;
|
|
682
|
+
let rows = await watchlist_repository_1.WatchlistRepository.getShowsWithNewEpisodes(db);
|
|
683
|
+
if (rows.length === 0) {
|
|
684
|
+
return res.json([]);
|
|
685
|
+
}
|
|
686
|
+
const showsNeedingStatus = rows.filter((r) => !r.smStatus);
|
|
687
|
+
if (showsNeedingStatus.length > 0) {
|
|
688
|
+
const BATCH_SIZE = 5;
|
|
689
|
+
for (let i = 0; i < showsNeedingStatus.length; i += BATCH_SIZE) {
|
|
690
|
+
const batch = showsNeedingStatus.slice(i, i + BATCH_SIZE);
|
|
691
|
+
await Promise.allSettled(batch.map((show) => this.getProviderForId(show.id)
|
|
692
|
+
.getShowMeta(show.id)
|
|
693
|
+
.then((meta) => {
|
|
694
|
+
if (meta?.status) {
|
|
695
|
+
return shows_meta_repository_1.ShowsMetaRepository.upsert(db, { id: show.id, status: meta.status });
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
.catch(() => { })));
|
|
699
|
+
}
|
|
700
|
+
await db.saveNow();
|
|
701
|
+
}
|
|
702
|
+
rows = await watchlist_repository_1.WatchlistRepository.getShowsWithNewEpisodes(db);
|
|
703
|
+
const filteredRows = rows.filter((r) => r.smStatus === 'Ongoing' || r.smStatus === 'Releasing' || r.smStatus === 'Currently Airing');
|
|
704
|
+
if (filteredRows.length === 0) {
|
|
705
|
+
return res.json([]);
|
|
706
|
+
}
|
|
707
|
+
const BATCH_SIZE = 5;
|
|
708
|
+
const episodeFetchResults = new Map();
|
|
709
|
+
for (let i = 0; i < filteredRows.length; i += BATCH_SIZE) {
|
|
710
|
+
const batch = filteredRows.slice(i, i + BATCH_SIZE);
|
|
711
|
+
const batchResults = await Promise.allSettled(batch.map((show) => this.getProviderForId(show.id).getEpisodes(show.id, 'sub')));
|
|
712
|
+
batch.forEach((show, index) => {
|
|
713
|
+
const result = batchResults[index];
|
|
714
|
+
if (result.status === 'fulfilled' && result.value?.episodes) {
|
|
715
|
+
episodeFetchResults.set(show.id, result.value.episodes);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
const enrichedRows = filteredRows.map((show) => {
|
|
720
|
+
const epList = episodeFetchResults.get(show.id);
|
|
721
|
+
let relativeEpisodeNumber = show.latestDiscoveredEpisode;
|
|
722
|
+
if (epList) {
|
|
723
|
+
const sortedEpList = [...epList].sort((a, b) => Number(a) - Number(b));
|
|
724
|
+
const idx = sortedEpList.indexOf(show.latestDiscoveredEpisode);
|
|
725
|
+
if (idx !== -1) {
|
|
726
|
+
relativeEpisodeNumber = String(idx + 1);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
_id: show.id,
|
|
731
|
+
id: show.id,
|
|
732
|
+
name: show.name,
|
|
733
|
+
thumbnail: this.deobfuscateUrl(show.thumbnail ?? '', show.id),
|
|
734
|
+
nativeName: show.nativeName,
|
|
735
|
+
englishName: show.englishName,
|
|
736
|
+
episodeNumber: show.latestDiscoveredEpisode,
|
|
737
|
+
relativeEpisodeNumber: show.latestDiscoveredEpisode,
|
|
738
|
+
currentTime: 0,
|
|
739
|
+
duration: 0,
|
|
740
|
+
episodeCount: show.episodeCount,
|
|
741
|
+
nextEpisodeToWatch: show.latestDiscoveredEpisode,
|
|
742
|
+
newEpisodesCount: 1,
|
|
743
|
+
type: show.type || show.smType,
|
|
744
|
+
};
|
|
745
|
+
});
|
|
746
|
+
res.json(enrichedRows);
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
exports.WatchlistController = WatchlistController;
|