@serifpersia/dango 3.3.9 → 3.4.0
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/README.md +1 -1
- package/client/dist/assets/{AnimeCard-URCiiy5W.js → AnimeCard-CJy1SXH3.js} +1 -1
- package/client/dist/assets/{AnimeInfo-e3z3er9t.js → AnimeInfo-BoXlVvZ-.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-B7Djlmy_.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
- package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
- package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
- package/client/dist/assets/Home-rrhOGQcg.js +1 -0
- package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
- package/client/dist/assets/{LibraryListPage-B0qWqhb_.js → LibraryListPage-HzuDavbb.js} +1 -1
- package/client/dist/assets/{ListeningList-CSqD8j1C.js → ListeningList-DYcQnnsG.js} +1 -1
- package/client/dist/assets/{Manga-Cg2YuObF.js → Manga-D8CT9aO0.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-DcArhEsy.js → MangaInfoPage-DGK6CYp1.js} +1 -1
- package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-D_yT3d1v.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
- package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
- package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
- package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
- package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
- package/client/dist/assets/Player-BYtEax_z.js +56 -0
- package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
- package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
- package/client/dist/assets/{QueueRail-B7f1-e6i.js → QueueRail-O7f4RqjB.js} +1 -1
- package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
- package/client/dist/assets/{ReadingList-BaIem8H1.js → ReadingList-n6rr8ygy.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-DES5Y-Fl.js → ResetProgressModal-CcStlbhP.js} +1 -1
- package/client/dist/assets/Search-C2wrWOdy.js +1 -0
- package/client/dist/assets/Settings-CluUYs8U.js +21 -0
- package/client/dist/assets/{SkeletonGrid-BxfgjP4i.js → SkeletonGrid-CLEPY9nP.js} +1 -1
- package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
- package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
- package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
- package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
- package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
- package/client/dist/assets/{TvSearch-BWDwWnHe.js → TvSearch-DuqR-z3v.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D3n5_4MI.js → TvWatchlist-CBvgFrA0.js} +1 -1
- package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
- package/client/dist/assets/{hls-BO_L1WoN.js → hls-Boje3IOE.js} +1 -1
- package/client/dist/assets/index-BXwhFWB_.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
- package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
- package/client/dist/assets/{useAsmrPopup-D_o9-QlR.js → useAsmrPopup-e4zNN9ig.js} +1 -1
- package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
- package/client/dist/assets/{useMangaPopup-TFzkC-tG.js → useMangaPopup-B8nnpvyi.js} +1 -1
- package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
- package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
- package/client/dist/assets/useTvLibrary-B7jEk2Q7.js +1 -0
- package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
- package/client/dist/index.html +2 -2
- package/orchestrator.js +1 -2
- package/package.json +2 -6
- package/server/dist/app-auth.js +9 -29
- package/server/dist/app-hono.js +621 -0
- package/server/dist/config.js +0 -1
- package/server/dist/db/drizzle.js +32 -0
- package/server/dist/db/schema-anime.js +64 -0
- package/server/dist/db/schema-asmr.js +25 -0
- package/server/dist/db/schema-manga.js +31 -0
- package/server/dist/db/schema-tv.js +33 -0
- package/server/dist/db.js +75 -4
- package/server/dist/discord-gateway.js +1 -1
- package/server/dist/google.js +0 -7
- package/server/dist/hono/asmr.js +1 -0
- package/server/dist/hono/auth.js +56 -0
- package/server/dist/hono/music.js +419 -0
- package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
- package/server/dist/hono/tv.js +197 -0
- package/server/dist/hono/watchlist.js +733 -0
- package/server/dist/lib/anilist.js +3 -3
- package/server/dist/lib/insights.js +185 -0
- package/server/dist/lib/ipc.js +1 -2
- package/server/dist/lib/kitsu.js +1 -28
- package/server/dist/lib/mal-import.js +291 -0
- package/server/dist/lib/migration.js +20 -34
- package/server/dist/lib/offline-db.js +36 -36
- package/server/dist/lib/tracker/manga-sync.service.js +21 -21
- package/server/dist/lib/tracker/sync.service.js +20 -20
- package/server/dist/lib/translate.js +79 -0
- package/server/dist/lib/ytmusic.js +0 -2
- package/server/dist/providers/title-matching.js +0 -1
- package/server/dist/repositories/asmr.repository.js +90 -87
- package/server/dist/repositories/insights.repository.js +24 -13
- package/server/dist/repositories/library.repository.js +33 -93
- package/server/dist/repositories/manga.repository.js +108 -116
- package/server/dist/repositories/notifications.repository.js +50 -31
- package/server/dist/repositories/queue.repository.js +36 -25
- package/server/dist/repositories/settings.repository.js +18 -8
- package/server/dist/repositories/shows-meta.repository.js +54 -41
- package/server/dist/repositories/temp-show-ids.repository.js +43 -31
- package/server/dist/repositories/tv.repository.js +98 -115
- package/server/dist/repositories/watched-episodes.repository.js +52 -20
- package/server/dist/repositories/watchlist.repository.js +36 -48
- package/server/dist/server.js +59 -151
- package/server/dist/sync.js +41 -4
- package/server/dist/trpc/context.js +7 -0
- package/server/dist/trpc/errors.js +21 -0
- package/server/dist/trpc/index.js +19 -0
- package/server/dist/trpc/router.js +49 -0
- package/server/dist/trpc/routers/asmr-library.js +122 -0
- package/server/dist/trpc/routers/asmr-progress.js +133 -0
- package/server/dist/trpc/routers/asmr.js +87 -0
- package/server/dist/trpc/routers/auth.js +247 -0
- package/server/dist/trpc/routers/continue-watching.js +83 -0
- package/server/dist/trpc/routers/data.js +1039 -0
- package/server/dist/trpc/routers/discord.js +288 -0
- package/server/dist/trpc/routers/discovery.js +15 -0
- package/server/dist/trpc/routers/insights.js +45 -0
- package/server/dist/trpc/routers/manga-library.js +280 -0
- package/server/dist/trpc/routers/manga-progress.js +181 -0
- package/server/dist/trpc/routers/manga.js +201 -0
- package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +155 -288
- package/server/dist/trpc/routers/notifications.js +80 -0
- package/server/dist/trpc/routers/progress.js +15 -0
- package/server/dist/trpc/routers/queue.js +95 -0
- package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
- package/server/dist/trpc/routers/settings.js +105 -0
- package/server/dist/trpc/routers/tracker.js +219 -0
- package/server/dist/trpc/routers/tv-library.js +145 -0
- package/server/dist/trpc/routers/tv-progress.js +171 -0
- package/server/dist/trpc/routers/tv.js +565 -0
- package/server/dist/trpc/routers/watchlist.js +564 -0
- package/server/dist/trpc/validation.js +159 -0
- package/server/dist/utils/security.utils.js +0 -18
- package/server/package.json +5 -10
- package/client/dist/assets/Asmr-C28TfSvX.js +0 -2
- package/client/dist/assets/AsmrResetProgressModal-6Xh71AgR.js +0 -1
- package/client/dist/assets/AvSyncCalibrator-BwCtRW0P.js +0 -10
- package/client/dist/assets/Home-BJdbHYja.js +0 -1
- package/client/dist/assets/Insights-CjRy42YE.js +0 -1
- package/client/dist/assets/MangaReadPage-YCETZRte.js +0 -1
- package/client/dist/assets/Mature-BGxRWkV5.js +0 -1
- package/client/dist/assets/MediaCard-CAzoxsRS.js +0 -1
- package/client/dist/assets/Music-Cgo-yQ7E.js +0 -1
- package/client/dist/assets/OptionListMenu--Owe9Jle.js +0 -1
- package/client/dist/assets/Player-yRXG2BrP.js +0 -56
- package/client/dist/assets/PlayerSettings-DrZLa0CC.js +0 -1
- package/client/dist/assets/QueueOptionsButton-BVZyPyKf.js +0 -1
- package/client/dist/assets/Radio-BFjxlzuF.js +0 -1
- package/client/dist/assets/Search-BOBo0Ogh.js +0 -1
- package/client/dist/assets/Settings-DOvNQ0gX.js +0 -21
- package/client/dist/assets/Trackers-DZ9KqOWv.js +0 -2
- package/client/dist/assets/Tv-5mATEU4p.js +0 -4
- package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
- package/client/dist/assets/TvInfo-CF-5C5Fs.js +0 -1
- package/client/dist/assets/TvPopup-Dfqifj3s.js +0 -1
- package/client/dist/assets/Watchlist-B8MHNsAU.js +0 -1
- package/client/dist/assets/index-D2RkUJIN.js +0 -178
- package/client/dist/assets/useAnimeInfoData-BWShDHqE.js +0 -1
- package/client/dist/assets/useAsmr-FyfD9G7b.js +0 -1
- package/client/dist/assets/useMangaLibrary-BJU5x8Vz.js +0 -1
- package/client/dist/assets/useProviders-DDRxa8vl.js +0 -1
- package/client/dist/assets/useTv-Bh6WmNnZ.js +0 -1
- package/client/dist/assets/useTvLibrary-_Pqm8re1.js +0 -1
- package/server/dist/controllers/asmr-library.controller.js +0 -288
- package/server/dist/controllers/auth.controller.js +0 -225
- package/server/dist/controllers/data.controller.js +0 -767
- package/server/dist/controllers/insights.controller.js +0 -203
- package/server/dist/controllers/manga-library.controller.js +0 -350
- package/server/dist/controllers/settings.controller.js +0 -609
- package/server/dist/controllers/tv-library.controller.js +0 -314
- package/server/dist/controllers/watchlist.controller.js +0 -1069
- package/server/dist/request-logger.js +0 -16
- package/server/dist/routes/asmr-library.routes.js +0 -23
- package/server/dist/routes/asmr.routes.js +0 -81
- package/server/dist/routes/auth.routes.js +0 -25
- package/server/dist/routes/data.routes.js +0 -47
- package/server/dist/routes/discord-gateway.routes.js +0 -51
- package/server/dist/routes/insights.routes.js +0 -35
- package/server/dist/routes/lan-auth.routes.js +0 -46
- package/server/dist/routes/manga-library.routes.js +0 -25
- package/server/dist/routes/manga.routes.js +0 -141
- package/server/dist/routes/music.routes.js +0 -20
- package/server/dist/routes/providers.routes.js +0 -17
- package/server/dist/routes/proxy.routes.js +0 -12
- package/server/dist/routes/settings.routes.js +0 -24
- package/server/dist/routes/tracker.routes.js +0 -287
- package/server/dist/routes/translate.routes.js +0 -88
- package/server/dist/routes/tv-library.routes.js +0 -23
- package/server/dist/routes/tv.routes.js +0 -629
- package/server/dist/routes/watchlist.routes.js +0 -341
|
@@ -1,767 +0,0 @@
|
|
|
1
|
-
import { parseJsonBody } from '../utils/http.utils.js';
|
|
2
|
-
import { pickBestMatch } from '../providers/title-matching.js';
|
|
3
|
-
import { getTrending, getLatestReleases, getSeasonal, getShowMetaById, getAnilistEpisodes, getSchedule, searchAnilist, searchAnilistByTitle, parseMalId, getShowMetaByMalId, getSpotlightBanners, getBatchedHomeData, getGenreTagLists, anilistUnavailable, wasAnilistDownAtBoot, checkAnilistStatus, fromAnilistMedia, } from '../lib/anilist.js';
|
|
4
|
-
import { malSearchMedia, malSearchTitle, malAnimeDetail } from '../lib/mal.js';
|
|
5
|
-
import { malCacheStore } from '../repositories/mal-cache.repository.js';
|
|
6
|
-
import { getMigratedId } from '../lib/migration.js';
|
|
7
|
-
import { isTempShowId } from '../lib/temp-ids.js';
|
|
8
|
-
import { TempShowIdsRepository } from '../repositories/temp-show-ids.repository.js';
|
|
9
|
-
import { ShowsMetaRepository } from '../repositories/shows-meta.repository.js';
|
|
10
|
-
import { WatchlistRepository } from '../repositories/watchlist.repository.js';
|
|
11
|
-
import { dbRun } from '../utils/db-utils.js';
|
|
12
|
-
import logger from '../logger.js';
|
|
13
|
-
function parseListParam(value) {
|
|
14
|
-
if (typeof value !== 'string' || !value.trim())
|
|
15
|
-
return undefined;
|
|
16
|
-
const list = value
|
|
17
|
-
.split(',')
|
|
18
|
-
.map((v) => v.trim())
|
|
19
|
-
.filter(Boolean);
|
|
20
|
-
return list.length > 0 ? list : undefined;
|
|
21
|
-
}
|
|
22
|
-
export class DataController {
|
|
23
|
-
providers;
|
|
24
|
-
getCatalog;
|
|
25
|
-
constructor(providers, getCatalog) {
|
|
26
|
-
this.providers = providers;
|
|
27
|
-
this.getCatalog = getCatalog;
|
|
28
|
-
}
|
|
29
|
-
setDataCache(res, maxAgeSeconds, body) {
|
|
30
|
-
const isEmpty = Array.isArray(body)
|
|
31
|
-
? body.length === 0
|
|
32
|
-
: body == null || (typeof body === 'object' && Object.keys(body).length === 0);
|
|
33
|
-
res.set('Cache-Control', isEmpty ? 'no-store' : `public, max-age=${maxAgeSeconds}`);
|
|
34
|
-
}
|
|
35
|
-
getProvider(req) {
|
|
36
|
-
const providerName = req.query.provider?.toLowerCase();
|
|
37
|
-
if (!providerName)
|
|
38
|
-
return null;
|
|
39
|
-
return this.providers[providerName] || null;
|
|
40
|
-
}
|
|
41
|
-
matureStreamingIds() {
|
|
42
|
-
const catalog = this.getCatalog?.();
|
|
43
|
-
if (!catalog || catalog.length === 0)
|
|
44
|
-
return null;
|
|
45
|
-
return new Set(catalog
|
|
46
|
-
.filter((c) => c.mature && (c.kind ?? 'anime') === 'anime' && c.loaded)
|
|
47
|
-
.map((c) => c.id.toLowerCase()));
|
|
48
|
-
}
|
|
49
|
-
isMatureStreamingProvider(provider) {
|
|
50
|
-
const key = (provider || '').toLowerCase();
|
|
51
|
-
if (!key || !this.providers[key] || key === 'mal' || key === 'anilist')
|
|
52
|
-
return false;
|
|
53
|
-
const mature = this.matureStreamingIds();
|
|
54
|
-
return mature ? mature.has(key) : true;
|
|
55
|
-
}
|
|
56
|
-
async resolveMatureStreamingId(title, wanted) {
|
|
57
|
-
const mature = this.matureStreamingIds();
|
|
58
|
-
const pool = mature ? [...mature] : Object.keys(this.providers);
|
|
59
|
-
const names = [wanted, ...pool].filter((p) => !!p && p !== 'mal' && p !== 'anilist' && !!this.providers[p]);
|
|
60
|
-
const seen = new Set();
|
|
61
|
-
for (const name of names) {
|
|
62
|
-
if (seen.has(name))
|
|
63
|
-
continue;
|
|
64
|
-
seen.add(name);
|
|
65
|
-
try {
|
|
66
|
-
const resolved = await this.providers[name].resolveShowId?.(title);
|
|
67
|
-
if (resolved)
|
|
68
|
-
return { provider: this.providers[name], nativeId: resolved };
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
// ignore
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
getTrending = async (_req, res) => {
|
|
77
|
-
try {
|
|
78
|
-
const data = await getTrending(1, 20, 'TRENDING_DESC', 'RELEASING');
|
|
79
|
-
this.setDataCache(res, 300, data);
|
|
80
|
-
res.json(data);
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
logger.error({ err: e }, 'Trending fetch failed');
|
|
84
|
-
res.set('Cache-Control', 'no-store').json([]);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
getSpotlight = async (_req, res) => {
|
|
88
|
-
try {
|
|
89
|
-
const data = await getSpotlightBanners(1, 20);
|
|
90
|
-
this.setDataCache(res, 300, data);
|
|
91
|
-
res.json(data);
|
|
92
|
-
}
|
|
93
|
-
catch (e) {
|
|
94
|
-
logger.error({ err: e }, 'Spotlight fetch failed');
|
|
95
|
-
res.set('Cache-Control', 'no-store').json([]);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
getPopularList = async (req, res) => {
|
|
99
|
-
const sort = req.query.sort === 'POPULARITY_DESC' ? 'POPULARITY_DESC' : 'TRENDING_DESC';
|
|
100
|
-
const page = parseInt(req.query.page) || 1;
|
|
101
|
-
const size = parseInt(req.query.size) || 20;
|
|
102
|
-
try {
|
|
103
|
-
const data = await getTrending(page, size, sort);
|
|
104
|
-
this.setDataCache(res, 300, data);
|
|
105
|
-
res.json(data);
|
|
106
|
-
}
|
|
107
|
-
catch (e) {
|
|
108
|
-
logger.error({ err: e }, 'Popular list fetch failed');
|
|
109
|
-
res.set('Cache-Control', 'no-store').json([]);
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
getSchedule = async (req, res) => {
|
|
113
|
-
try {
|
|
114
|
-
const date = new Date(req.params.date + 'T00:00:00.000Z');
|
|
115
|
-
const format = req.query.format || undefined;
|
|
116
|
-
const adult = format === 'ADULT';
|
|
117
|
-
const data = await getSchedule(date, adult ? undefined : format, adult);
|
|
118
|
-
this.setDataCache(res, 300, data);
|
|
119
|
-
res.json(data);
|
|
120
|
-
}
|
|
121
|
-
catch (e) {
|
|
122
|
-
logger.error({ err: e, date: req.params.date }, 'Schedule fetch failed');
|
|
123
|
-
res.set('Cache-Control', 'no-store').json([]);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
getSkipTimes = async (req, res) => {
|
|
127
|
-
try {
|
|
128
|
-
const showId = req.params.showId;
|
|
129
|
-
const episodeNumber = req.params.episodeNumber;
|
|
130
|
-
if (/^\d+$/.test(showId)) {
|
|
131
|
-
const skipRes = await fetch(`https://api.aniskip.com/v1/skip-times/${showId}/${episodeNumber}?types=op&types=ed`);
|
|
132
|
-
if (skipRes.ok) {
|
|
133
|
-
const data = await parseJsonBody(skipRes);
|
|
134
|
-
return res.json(data);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
res.json({ found: false, results: [] });
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
res.json({ found: false, results: [] });
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
getVideo = async (req, res) => {
|
|
144
|
-
try {
|
|
145
|
-
let showId = req.query.showId;
|
|
146
|
-
const videoMalId = parseMalId(showId);
|
|
147
|
-
if (videoMalId) {
|
|
148
|
-
try {
|
|
149
|
-
const malMeta = await getShowMetaByMalId(videoMalId);
|
|
150
|
-
if (malMeta?.anilistId && malMeta.anilistId > 0)
|
|
151
|
-
showId = String(malMeta.anilistId);
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// ignore
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if (isTempShowId(showId)) {
|
|
158
|
-
try {
|
|
159
|
-
const row = TempShowIdsRepository.getById(req.db, showId);
|
|
160
|
-
if (!row || !this.isMatureStreamingProvider(row.provider))
|
|
161
|
-
return res.json([]);
|
|
162
|
-
const wanted = String(req.query.provider || '').toLowerCase();
|
|
163
|
-
const own = this.providers[row.provider];
|
|
164
|
-
if (own && (!wanted || wanted === row.provider)) {
|
|
165
|
-
const urls = await own.getStreamUrls(row.nativeId, req.query.episodeNumber, req.query.mode);
|
|
166
|
-
return res.json(urls || []);
|
|
167
|
-
}
|
|
168
|
-
const hit = await this.resolveMatureStreamingId(row.title, wanted);
|
|
169
|
-
if (hit) {
|
|
170
|
-
const urls = await hit.provider.getStreamUrls(hit.nativeId, req.query.episodeNumber, req.query.mode);
|
|
171
|
-
return res.json(urls || []);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
catch (e) {
|
|
175
|
-
logger.error({ err: e, showId }, 'Temp show video fetch failed');
|
|
176
|
-
}
|
|
177
|
-
return res.json([]);
|
|
178
|
-
}
|
|
179
|
-
const providerName = req.query.provider;
|
|
180
|
-
const providerKey = providerName?.toLowerCase();
|
|
181
|
-
const stillMal = parseMalId(showId) !== null;
|
|
182
|
-
const isNativeId = !!(providerKey && this.providers[providerKey]?.isDirectId?.(showId));
|
|
183
|
-
const needsResolution = !isNativeId && (/^\d+$/.test(showId) || stillMal);
|
|
184
|
-
if (providerKey && needsResolution) {
|
|
185
|
-
const meta = (await ShowsMetaRepository.getById(req.db, showId));
|
|
186
|
-
let targetTitle = meta?.englishName || meta?.name;
|
|
187
|
-
let anilistShow = null;
|
|
188
|
-
if (!targetTitle) {
|
|
189
|
-
try {
|
|
190
|
-
anilistShow = await getShowMetaById(showId);
|
|
191
|
-
targetTitle = anilistShow?.englishName || anilistShow?.name;
|
|
192
|
-
if (anilistShow && targetTitle) {
|
|
193
|
-
await ShowsMetaRepository.upsert(req.db, {
|
|
194
|
-
id: showId,
|
|
195
|
-
name: anilistShow.name,
|
|
196
|
-
thumbnail: anilistShow.thumbnail,
|
|
197
|
-
nativeName: anilistShow.nativeName,
|
|
198
|
-
englishName: anilistShow.englishName,
|
|
199
|
-
genres: anilistShow.genres
|
|
200
|
-
? JSON.stringify(anilistShow.genres.map((genre) => genre.name))
|
|
201
|
-
: undefined,
|
|
202
|
-
status: anilistShow.status,
|
|
203
|
-
episodeCount: anilistShow.episodeCount != null ? Number(anilistShow.episodeCount) : undefined,
|
|
204
|
-
type: anilistShow.type,
|
|
205
|
-
anilistId: anilistShow.anilistId,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
catch (err) {
|
|
210
|
-
logger.warn({ err, provider: providerKey, showId }, '[Video] AniList metadata lookup failed while resolving numeric showId');
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (targetTitle) {
|
|
214
|
-
let romaji = anilistShow?.names?.romaji;
|
|
215
|
-
let nativeName = anilistShow?.names?.native;
|
|
216
|
-
let synonyms = anilistShow?.names?.synonyms;
|
|
217
|
-
if (!romaji || !nativeName) {
|
|
218
|
-
try {
|
|
219
|
-
const show = await getShowMetaById(showId);
|
|
220
|
-
romaji = romaji || show?.names?.romaji;
|
|
221
|
-
nativeName = nativeName || show?.names?.native;
|
|
222
|
-
synonyms = synonyms || show?.names?.synonyms;
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
// ignore
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const titleTargets = [
|
|
229
|
-
targetTitle,
|
|
230
|
-
romaji,
|
|
231
|
-
nativeName,
|
|
232
|
-
...(synonyms ?? []).slice(0, 2),
|
|
233
|
-
].filter((t) => !!t && t.trim().length > 0);
|
|
234
|
-
const distinctTargets = [...new Set(titleTargets.map((t) => t.trim()))].filter((t, i, a) => a.findIndex((x) => x.toLowerCase() === t.toLowerCase()) === i);
|
|
235
|
-
let resolved = null;
|
|
236
|
-
for (const variant of distinctTargets) {
|
|
237
|
-
try {
|
|
238
|
-
resolved =
|
|
239
|
-
(await this.providers[providerKey]?.resolveShowId?.(variant, romaji, req.query.mode)) ?? null;
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
resolved = null;
|
|
243
|
-
}
|
|
244
|
-
if (resolved)
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
if (resolved) {
|
|
248
|
-
showId = resolved;
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
logger.warn({ provider: providerKey, showId, title: targetTitle, romaji }, '[Video] resolveShowId failed, attempting fallback provider search');
|
|
252
|
-
try {
|
|
253
|
-
const fallbackResults = await this.providers[providerKey]?.search?.({
|
|
254
|
-
query: targetTitle,
|
|
255
|
-
});
|
|
256
|
-
const fallbackMatch = pickBestMatch((fallbackResults || []).map((r) => ({
|
|
257
|
-
title: r.name || r.englishName || '',
|
|
258
|
-
id: r.id || r._id || '',
|
|
259
|
-
})), distinctTargets);
|
|
260
|
-
if (fallbackMatch) {
|
|
261
|
-
showId = fallbackMatch.item.id;
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
logger.warn({ provider: providerKey, showId, title: targetTitle }, '[Video] fallback provider search returned no results');
|
|
265
|
-
return res.json([]);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
catch (fallbackErr) {
|
|
269
|
-
if (fallbackErr.message === 'AUTH_REQUIRED') {
|
|
270
|
-
throw fallbackErr;
|
|
271
|
-
}
|
|
272
|
-
logger.error({ err: fallbackErr, provider: providerKey, showId, title: targetTitle }, '[Video] fallback provider search failed');
|
|
273
|
-
return res.json([]);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
278
|
-
logger.warn({ provider: providerKey, showId }, '[Video] numeric showId passed to provider but local meta missing title');
|
|
279
|
-
return res.json([]);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
const provider = this.getProvider(req);
|
|
283
|
-
if (!provider)
|
|
284
|
-
return res.json([]);
|
|
285
|
-
const urls = await provider.getStreamUrls(showId, req.query.episodeNumber, req.query.mode);
|
|
286
|
-
res.json(urls || []);
|
|
287
|
-
}
|
|
288
|
-
catch (e) {
|
|
289
|
-
if (e.message === 'AUTH_REQUIRED') {
|
|
290
|
-
return res.status(403).json({ error: 'AUTH_REQUIRED', provider: 'animepahe' });
|
|
291
|
-
}
|
|
292
|
-
logger.error({ err: e, provider: req.query.provider }, 'Provider video fetch failed');
|
|
293
|
-
res.json([]);
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
getEpisodes = async (req, res) => {
|
|
297
|
-
const showIdRaw = req.query.showId;
|
|
298
|
-
if (!showIdRaw) {
|
|
299
|
-
return res.json({ episodes: [] });
|
|
300
|
-
}
|
|
301
|
-
let showId = await getMigratedId(req.db, showIdRaw);
|
|
302
|
-
const epsMalId = parseMalId(showId);
|
|
303
|
-
if (epsMalId) {
|
|
304
|
-
try {
|
|
305
|
-
const malMeta = await getShowMetaByMalId(epsMalId);
|
|
306
|
-
if (malMeta?.anilistId && malMeta.anilistId > 0)
|
|
307
|
-
showId = String(malMeta.anilistId);
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
// ignore
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (isTempShowId(showId)) {
|
|
314
|
-
try {
|
|
315
|
-
const row = TempShowIdsRepository.getById(req.db, showId);
|
|
316
|
-
if (!row || !this.isMatureStreamingProvider(row.provider))
|
|
317
|
-
return res.json({ episodes: [] });
|
|
318
|
-
const wanted = String(req.query.provider || '').toLowerCase();
|
|
319
|
-
const own = this.providers[row.provider];
|
|
320
|
-
if (own && (!wanted || wanted === row.provider)) {
|
|
321
|
-
const data = await own.getEpisodes(row.nativeId, req.query.mode);
|
|
322
|
-
if (data?.episodes?.length)
|
|
323
|
-
return res.json(data);
|
|
324
|
-
return res.json({ episodes: [] });
|
|
325
|
-
}
|
|
326
|
-
const hit = await this.resolveMatureStreamingId(row.title, wanted);
|
|
327
|
-
if (hit) {
|
|
328
|
-
const data = await hit.provider.getEpisodes(hit.nativeId, req.query.mode);
|
|
329
|
-
if (data?.episodes?.length)
|
|
330
|
-
return res.json(data);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
catch {
|
|
334
|
-
// ignore
|
|
335
|
-
}
|
|
336
|
-
return res.json({ episodes: [] });
|
|
337
|
-
}
|
|
338
|
-
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(showId)) {
|
|
339
|
-
try {
|
|
340
|
-
if (this.providers['animepahe']) {
|
|
341
|
-
const data = await this.providers['animepahe'].getEpisodes(showId, req.query.mode);
|
|
342
|
-
return res.json(data || { episodes: [] });
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
return res.json({ episodes: [] });
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
const isNumeric = /^(mal-\d+|-?\d+)$/i.test(showId);
|
|
350
|
-
if (isNumeric) {
|
|
351
|
-
let episodes = [];
|
|
352
|
-
try {
|
|
353
|
-
episodes = await getAnilistEpisodes(showId);
|
|
354
|
-
}
|
|
355
|
-
catch (e) {
|
|
356
|
-
logger.error({ err: e, showId }, 'Episodes fetch failed');
|
|
357
|
-
}
|
|
358
|
-
if (episodes.length === 0) {
|
|
359
|
-
const absId = Math.abs(parseInt(showId, 10));
|
|
360
|
-
if (!Number.isFinite(absId)) {
|
|
361
|
-
res.set('Cache-Control', 'public, max-age=3600').json({ episodes });
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
try {
|
|
365
|
-
const d = await malAnimeDetail(malCacheStore(), absId);
|
|
366
|
-
const total = d.detail?.episodes;
|
|
367
|
-
if (total && total > 0 && total <= 500) {
|
|
368
|
-
episodes = Array.from({ length: total }, (_, i) => (i + 1).toString());
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
catch {
|
|
372
|
-
// ignore
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
if (episodes.length === 0) {
|
|
376
|
-
const providerName = req.query.provider?.toLowerCase();
|
|
377
|
-
const provider = providerName ? this.providers[providerName] : undefined;
|
|
378
|
-
if (provider?.isDirectId?.(showId)) {
|
|
379
|
-
try {
|
|
380
|
-
const data = await provider.getEpisodes(showId, req.query.mode);
|
|
381
|
-
if (data?.episodes?.length) {
|
|
382
|
-
res.set('Cache-Control', 'no-store').json(data);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
// ignore
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
res.set('Cache-Control', 'public, max-age=3600').json({ episodes });
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
const providerName = req.query.provider?.toLowerCase();
|
|
395
|
-
const provider = providerName ? this.providers[providerName] : undefined;
|
|
396
|
-
if (provider) {
|
|
397
|
-
try {
|
|
398
|
-
const data = await provider.getEpisodes(showId, req.query.mode);
|
|
399
|
-
if (data?.episodes?.length) {
|
|
400
|
-
return res.json(data);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
catch {
|
|
404
|
-
// ignore
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
res.json({ episodes: [] });
|
|
408
|
-
};
|
|
409
|
-
search = async (req, res) => {
|
|
410
|
-
try {
|
|
411
|
-
const query = req.query.query || '';
|
|
412
|
-
const page = parseInt(req.query.page) || 1;
|
|
413
|
-
const perPage = parseInt(req.query.limit) || 14;
|
|
414
|
-
const sort = req.query.sortBy || undefined;
|
|
415
|
-
const result = await searchAnilist({
|
|
416
|
-
query,
|
|
417
|
-
page,
|
|
418
|
-
perPage,
|
|
419
|
-
format: req.query.type,
|
|
420
|
-
status: req.query.status,
|
|
421
|
-
season: req.query.season,
|
|
422
|
-
seasonYear: req.query.year ? parseInt(req.query.year) : undefined,
|
|
423
|
-
countryOfOrigin: req.query.country,
|
|
424
|
-
genre: req.query.genres,
|
|
425
|
-
genre_not_in: parseListParam(req.query.excludeGenres),
|
|
426
|
-
tag_not_in: parseListParam(req.query.excludeTags),
|
|
427
|
-
averageScore_greater: req.query.minScore
|
|
428
|
-
? parseInt(req.query.minScore)
|
|
429
|
-
: undefined,
|
|
430
|
-
episodes_greater: req.query.minEpisodes
|
|
431
|
-
? parseInt(req.query.minEpisodes)
|
|
432
|
-
: undefined,
|
|
433
|
-
isAdult: req.query.adult === 'true' ? true : req.query.adult === 'false' ? false : undefined,
|
|
434
|
-
sort,
|
|
435
|
-
});
|
|
436
|
-
return res.json(result);
|
|
437
|
-
}
|
|
438
|
-
catch (e) {
|
|
439
|
-
logger.error({ err: e }, 'search failed');
|
|
440
|
-
res.json([]);
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
matureSearch = async (req, res) => {
|
|
444
|
-
try {
|
|
445
|
-
const provider = (req.query.provider || 'anilist').toLowerCase();
|
|
446
|
-
const query = req.query.query || '';
|
|
447
|
-
const page = parseInt(req.query.page) || 1;
|
|
448
|
-
const limit = parseInt(req.query.limit) || 14;
|
|
449
|
-
if (provider === 'anilist') {
|
|
450
|
-
const result = await searchAnilist({
|
|
451
|
-
query,
|
|
452
|
-
page,
|
|
453
|
-
perPage: limit,
|
|
454
|
-
format: req.query.type,
|
|
455
|
-
status: req.query.status,
|
|
456
|
-
season: req.query.season,
|
|
457
|
-
seasonYear: req.query.year ? parseInt(req.query.year) : undefined,
|
|
458
|
-
countryOfOrigin: req.query.country,
|
|
459
|
-
genre: req.query.genres,
|
|
460
|
-
genre_not_in: parseListParam(req.query.excludeGenres),
|
|
461
|
-
isAdult: true,
|
|
462
|
-
sort: req.query.sortBy || undefined,
|
|
463
|
-
});
|
|
464
|
-
return res.json({ data: result, hasMore: result.length >= limit });
|
|
465
|
-
}
|
|
466
|
-
if (this.isMatureStreamingProvider(provider)) {
|
|
467
|
-
const streaming = this.providers[provider];
|
|
468
|
-
if (streaming?.browse) {
|
|
469
|
-
const genre = req.query.genre || req.query.genres || undefined;
|
|
470
|
-
const result = await streaming.browse({
|
|
471
|
-
query,
|
|
472
|
-
page,
|
|
473
|
-
limit,
|
|
474
|
-
pageSize: limit,
|
|
475
|
-
genre,
|
|
476
|
-
genres: req.query.genres || genre,
|
|
477
|
-
order: req.query.order || req.query.sortBy || undefined,
|
|
478
|
-
sort: req.query.sortBy || req.query.order || undefined,
|
|
479
|
-
studio: req.query.studio || undefined,
|
|
480
|
-
blacklist: req.query.blacklist || undefined,
|
|
481
|
-
});
|
|
482
|
-
return res.json({
|
|
483
|
-
data: result.shows,
|
|
484
|
-
hasMore: result.hasMore,
|
|
485
|
-
...(result.total !== undefined ? { total: result.total } : {}),
|
|
486
|
-
...(result.genres ? { genres: result.genres } : {}),
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
const shows = await streaming.search({ query });
|
|
490
|
-
return res.json({
|
|
491
|
-
data: shows.map((s) => ({ ...s, isAdult: true })),
|
|
492
|
-
hasMore: false,
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
if (provider === 'mal') {
|
|
496
|
-
const malPage = page;
|
|
497
|
-
const malLimit = 50;
|
|
498
|
-
let result = [];
|
|
499
|
-
try {
|
|
500
|
-
result = await malSearchMedia(malCacheStore(), {
|
|
501
|
-
query,
|
|
502
|
-
page: malPage,
|
|
503
|
-
perPage: malLimit,
|
|
504
|
-
format: req.query.type,
|
|
505
|
-
status: req.query.status,
|
|
506
|
-
genre: req.query.genres || undefined,
|
|
507
|
-
genre_not_in: parseListParam(req.query.excludeGenres),
|
|
508
|
-
isAdult: true,
|
|
509
|
-
sort: req.query.sortBy || undefined,
|
|
510
|
-
averageScore_greater: req.query.minScore
|
|
511
|
-
? parseInt(req.query.minScore)
|
|
512
|
-
: undefined,
|
|
513
|
-
episodes_greater: req.query.minEpisodes
|
|
514
|
-
? parseInt(req.query.minEpisodes)
|
|
515
|
-
: undefined,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
catch (e) {
|
|
519
|
-
logger.error({ err: e }, 'mal mature search failed');
|
|
520
|
-
}
|
|
521
|
-
const shows = result.map((m) => {
|
|
522
|
-
const show = fromAnilistMedia(m);
|
|
523
|
-
return { ...show, isAdult: true };
|
|
524
|
-
});
|
|
525
|
-
const sliced = shows.slice(0, limit);
|
|
526
|
-
return res.json({ data: sliced, hasMore: shows.length >= limit });
|
|
527
|
-
}
|
|
528
|
-
return res.status(400).json({ error: 'Unknown mature provider' });
|
|
529
|
-
}
|
|
530
|
-
catch (e) {
|
|
531
|
-
logger.error({ err: e }, 'mature search failed');
|
|
532
|
-
res.json({ data: [], hasMore: false });
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
resolveMature = async (req, res) => {
|
|
536
|
-
try {
|
|
537
|
-
const title = (req.query.title || '').trim();
|
|
538
|
-
if (!title)
|
|
539
|
-
return res.status(400).json({ error: 'title is required' });
|
|
540
|
-
const anilistResult = await searchAnilistByTitle(title);
|
|
541
|
-
if (anilistResult) {
|
|
542
|
-
const id = anilistResult.id;
|
|
543
|
-
return res.json({ id: typeof id === 'number' && id < 0 ? `mal-${Math.abs(id)}` : id });
|
|
544
|
-
}
|
|
545
|
-
const malResult = await malSearchTitle(malCacheStore(), title);
|
|
546
|
-
if (malResult)
|
|
547
|
-
return res.json({ id: malResult, provider: 'mal' });
|
|
548
|
-
return res.status(404).json({ error: 'No match found' });
|
|
549
|
-
}
|
|
550
|
-
catch (e) {
|
|
551
|
-
logger.error({ err: e }, 'mature resolve failed');
|
|
552
|
-
res.status(500).json({ error: 'Resolve failed' });
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
getMatureFilters = async (_req, res) => {
|
|
556
|
-
try {
|
|
557
|
-
const catalog = this.getCatalog?.() ?? [];
|
|
558
|
-
const providers = {};
|
|
559
|
-
for (const item of catalog) {
|
|
560
|
-
if (!item.mature || (item.kind ?? 'anime') !== 'anime' || !item.loaded)
|
|
561
|
-
continue;
|
|
562
|
-
providers[item.id] = {
|
|
563
|
-
label: item.label,
|
|
564
|
-
...(item.browse ? { browse: item.browse } : {}),
|
|
565
|
-
...(item.facets?.genres?.length ? { genres: item.facets.genres } : {}),
|
|
566
|
-
...(item.facets?.orders?.length ? { orders: item.facets.orders } : {}),
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
return res.json({ providers });
|
|
570
|
-
}
|
|
571
|
-
catch (e) {
|
|
572
|
-
logger.error({ err: e }, 'mature filters failed');
|
|
573
|
-
res.status(500).json({ error: 'Failed to load filters' });
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
getSeasonal = async (req, res) => {
|
|
577
|
-
const page = parseInt(req.query.page) || 1;
|
|
578
|
-
const size = parseInt(req.query.size) || 14;
|
|
579
|
-
const format = req.query.format;
|
|
580
|
-
try {
|
|
581
|
-
const data = await getSeasonal(page, size, format);
|
|
582
|
-
this.setDataCache(res, 300, data);
|
|
583
|
-
res.json(data);
|
|
584
|
-
}
|
|
585
|
-
catch (e) {
|
|
586
|
-
logger.error({ err: e }, 'Seasonal fetch failed');
|
|
587
|
-
res.set('Cache-Control', 'no-store').json([]);
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
getLatestReleases = async (req, res) => {
|
|
591
|
-
const format = req.query.format || 'TV';
|
|
592
|
-
const page = parseInt(req.query.page) || 1;
|
|
593
|
-
const size = parseInt(req.query.size) || 12;
|
|
594
|
-
try {
|
|
595
|
-
const data = await getLatestReleases(format, page, size);
|
|
596
|
-
this.setDataCache(res, 300, data);
|
|
597
|
-
res.json(data);
|
|
598
|
-
}
|
|
599
|
-
catch (e) {
|
|
600
|
-
logger.error({ err: e }, 'Latest releases fetch failed');
|
|
601
|
-
res.set('Cache-Control', 'no-store').json([]);
|
|
602
|
-
}
|
|
603
|
-
};
|
|
604
|
-
getShowMeta = async (req, res) => {
|
|
605
|
-
const showIdRaw = req.params.id;
|
|
606
|
-
const id = await getMigratedId(req.db, showIdRaw);
|
|
607
|
-
if (isTempShowId(id)) {
|
|
608
|
-
const row = TempShowIdsRepository.getById(req.db, id);
|
|
609
|
-
if (!row || !this.isMatureStreamingProvider(row.provider)) {
|
|
610
|
-
res.json({});
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
res.set('Cache-Control', 'public, max-age=300').json({
|
|
614
|
-
_id: row.id,
|
|
615
|
-
id: row.id,
|
|
616
|
-
name: row.title,
|
|
617
|
-
englishName: row.title,
|
|
618
|
-
thumbnail: row.thumbnail || '',
|
|
619
|
-
type: 'TV',
|
|
620
|
-
isAdult: true,
|
|
621
|
-
status: 'UNKNOWN',
|
|
622
|
-
provider: row.provider,
|
|
623
|
-
nativeId: row.nativeId,
|
|
624
|
-
});
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
const isNumeric = /^(mal-\d+|-?\d+)$/i.test(id);
|
|
628
|
-
if (isNumeric) {
|
|
629
|
-
let meta = null;
|
|
630
|
-
try {
|
|
631
|
-
meta = await getShowMetaById(id);
|
|
632
|
-
}
|
|
633
|
-
catch (e) {
|
|
634
|
-
logger.warn({ err: e, id }, 'AniList show-meta fetch failed, trying local cache');
|
|
635
|
-
const localMeta = (await ShowsMetaRepository.getById(req.db, id));
|
|
636
|
-
if (localMeta) {
|
|
637
|
-
if (typeof localMeta.genres === 'string') {
|
|
638
|
-
try {
|
|
639
|
-
localMeta.genres = JSON.parse(localMeta.genres);
|
|
640
|
-
}
|
|
641
|
-
catch {
|
|
642
|
-
localMeta.genres = [];
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
res.set('Cache-Control', 'public, max-age=3600').json(localMeta);
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
if (meta) {
|
|
650
|
-
const poster = meta.thumbnail?.trim() ? meta.thumbnail : undefined;
|
|
651
|
-
const hasPoster = !!poster;
|
|
652
|
-
try {
|
|
653
|
-
ShowsMetaRepository.upsert(req.db, {
|
|
654
|
-
id,
|
|
655
|
-
name: meta.name,
|
|
656
|
-
thumbnail: poster,
|
|
657
|
-
nativeName: meta.nativeName,
|
|
658
|
-
englishName: meta.englishName,
|
|
659
|
-
popularityScore: meta.score ?? (meta.averageScore != null ? meta.averageScore / 10 : undefined),
|
|
660
|
-
genres: meta.genres
|
|
661
|
-
? JSON.stringify(meta.genres.map((g) => (typeof g === 'string' ? g : g?.name)).filter(Boolean))
|
|
662
|
-
: undefined,
|
|
663
|
-
status: meta.status,
|
|
664
|
-
episodeCount: meta.episodeCount != null ? Number(meta.episodeCount) : undefined,
|
|
665
|
-
type: meta.type,
|
|
666
|
-
anilistId: meta.anilistId,
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
catch {
|
|
670
|
-
// ignore
|
|
671
|
-
}
|
|
672
|
-
if (hasPoster && poster) {
|
|
673
|
-
const existingWatchlist = (await WatchlistRepository.getById(req.db, id));
|
|
674
|
-
if (existingWatchlist && existingWatchlist.thumbnail !== poster) {
|
|
675
|
-
try {
|
|
676
|
-
await WatchlistRepository.updateThumbnail(req.db, id, poster);
|
|
677
|
-
}
|
|
678
|
-
catch {
|
|
679
|
-
// ignore
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
req.db.scheduleSave();
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
if (/^(mal-\d+|-\d+)$/i.test(id) && meta?.anilistId && meta.anilistId > 0) {
|
|
686
|
-
try {
|
|
687
|
-
dbRun(req.db, 'INSERT OR REPLACE INTO legacy_id_mapping (legacyId, numericId) VALUES (?, ?)', [id, String(meta.anilistId)]);
|
|
688
|
-
}
|
|
689
|
-
catch {
|
|
690
|
-
// ignore
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
this.setDataCache(res, 3600, meta || {});
|
|
694
|
-
res.json(meta || {});
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
res.json({});
|
|
698
|
-
};
|
|
699
|
-
allocateTempShow = async (req, res) => {
|
|
700
|
-
try {
|
|
701
|
-
const provider = String(req.body?.provider || '').toLowerCase();
|
|
702
|
-
const nativeId = String(req.body?.nativeId || '').trim();
|
|
703
|
-
const title = String(req.body?.title || '').trim();
|
|
704
|
-
const thumbnail = String(req.body?.thumbnail || '');
|
|
705
|
-
if (!provider || !this.isMatureStreamingProvider(provider)) {
|
|
706
|
-
return res.status(400).json({ error: 'Unknown provider' });
|
|
707
|
-
}
|
|
708
|
-
if (!nativeId || !title) {
|
|
709
|
-
return res.status(400).json({ error: 'nativeId and title are required' });
|
|
710
|
-
}
|
|
711
|
-
const row = TempShowIdsRepository.allocate(req.db, {
|
|
712
|
-
provider,
|
|
713
|
-
nativeId,
|
|
714
|
-
title,
|
|
715
|
-
thumbnail,
|
|
716
|
-
});
|
|
717
|
-
return res.json({ id: row.id });
|
|
718
|
-
}
|
|
719
|
-
catch (e) {
|
|
720
|
-
logger.error({ err: e }, 'temp show allocate failed');
|
|
721
|
-
res.status(500).json({ error: 'Allocate failed' });
|
|
722
|
-
}
|
|
723
|
-
};
|
|
724
|
-
getAnilistStatus = async (_req, res) => {
|
|
725
|
-
const available = !anilistUnavailable();
|
|
726
|
-
if (!available) {
|
|
727
|
-
checkAnilistStatus().catch(() => { });
|
|
728
|
-
}
|
|
729
|
-
res.json({ available, wasDownAtBoot: wasAnilistDownAtBoot() });
|
|
730
|
-
};
|
|
731
|
-
getGenresAndTags = async (_req, res) => {
|
|
732
|
-
try {
|
|
733
|
-
res.json(await getGenreTagLists());
|
|
734
|
-
}
|
|
735
|
-
catch (e) {
|
|
736
|
-
logger.error({ err: e }, 'genres-and-tags failed');
|
|
737
|
-
res.status(500).json({ error: 'Failed to load genres' });
|
|
738
|
-
}
|
|
739
|
-
};
|
|
740
|
-
getSystemNotifications = async (_req, res) => {
|
|
741
|
-
const notifications = [];
|
|
742
|
-
if (wasAnilistDownAtBoot() && anilistUnavailable()) {
|
|
743
|
-
const fb = 'MAL + Kitsu';
|
|
744
|
-
notifications.push({
|
|
745
|
-
id: 'system-anilist-down',
|
|
746
|
-
type: 'system',
|
|
747
|
-
title: 'AniList API',
|
|
748
|
-
message: `AniList metadata API is currently down. dango is experiencing degraded performance. ${fb} is being used as a fallback in the meantime. Some features may be limited until service is restored.`,
|
|
749
|
-
icon: 'warning',
|
|
750
|
-
createdAt: Date.now(),
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
res.json(notifications);
|
|
754
|
-
};
|
|
755
|
-
getBatchedHome = async (req, res) => {
|
|
756
|
-
try {
|
|
757
|
-
const format = req.query.format || undefined;
|
|
758
|
-
const data = await getBatchedHomeData(format);
|
|
759
|
-
const isEmpty = data.trending.length === 0 && data.seasonal.length === 0 && data.spotlight.length === 0;
|
|
760
|
-
res.set('Cache-Control', isEmpty ? 'no-store' : 'public, max-age=300').json(data);
|
|
761
|
-
}
|
|
762
|
-
catch (e) {
|
|
763
|
-
logger.error({ err: e }, 'Batched home fetch failed');
|
|
764
|
-
res.set('Cache-Control', 'no-store').json({ trending: [], seasonal: [], spotlight: [] });
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
}
|