@serifpersia/dango 3.3.8 → 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.
Files changed (180) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-D4qo4289.js → AnimeCard-CJy1SXH3.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-BdRRm4So.js → AnimeInfo-BoXlVvZ-.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DeJNAKMR.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
  5. package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
  6. package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
  7. package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
  8. package/client/dist/assets/Home-rrhOGQcg.js +1 -0
  9. package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
  10. package/client/dist/assets/{LibraryListPage-D3hTqTRU.js → LibraryListPage-HzuDavbb.js} +1 -1
  11. package/client/dist/assets/{ListeningList-BgWsfu40.js → ListeningList-DYcQnnsG.js} +1 -1
  12. package/client/dist/assets/{Manga-Dn-CYins.js → Manga-D8CT9aO0.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-EmQoK7Bv.js → MangaInfoPage-DGK6CYp1.js} +1 -1
  14. package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
  15. package/client/dist/assets/{MangaResetProgressModal-Dian37PQ.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
  16. package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
  17. package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
  18. package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
  19. package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
  20. package/client/dist/assets/Player-BYtEax_z.js +56 -0
  21. package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
  22. package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
  23. package/client/dist/assets/{QueueRail-B7DeSFoA.js → QueueRail-O7f4RqjB.js} +1 -1
  24. package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
  25. package/client/dist/assets/{ReadingList-BT8kUjCB.js → ReadingList-n6rr8ygy.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-DWeiO-hs.js → ResetProgressModal-CcStlbhP.js} +1 -1
  27. package/client/dist/assets/Search-C2wrWOdy.js +1 -0
  28. package/client/dist/assets/Settings-CluUYs8U.js +21 -0
  29. package/client/dist/assets/{SkeletonGrid-B5loz7ii.js → SkeletonGrid-CLEPY9nP.js} +1 -1
  30. package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
  31. package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
  32. package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
  33. package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
  34. package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
  35. package/client/dist/assets/{TvSearch-BYwCf5kO.js → TvSearch-DuqR-z3v.js} +1 -1
  36. package/client/dist/assets/{TvWatchlist-BiJU6pKT.js → TvWatchlist-CBvgFrA0.js} +1 -1
  37. package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
  38. package/client/dist/assets/{hls-C11FOWHj.js → hls-Boje3IOE.js} +1 -1
  39. package/client/dist/assets/index-BXwhFWB_.js +178 -0
  40. package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
  41. package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
  42. package/client/dist/assets/{useAsmrPopup-cT_Uu41e.js → useAsmrPopup-e4zNN9ig.js} +1 -1
  43. package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
  44. package/client/dist/assets/{useMangaPopup-PnmSctHu.js → useMangaPopup-B8nnpvyi.js} +1 -1
  45. package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
  46. package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
  47. package/client/dist/assets/useTvLibrary-B7jEk2Q7.js +1 -0
  48. package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
  49. package/client/dist/index.html +2 -2
  50. package/orchestrator.js +1 -2
  51. package/package.json +2 -6
  52. package/server/dist/app-auth.js +9 -29
  53. package/server/dist/app-hono.js +621 -0
  54. package/server/dist/config.js +0 -1
  55. package/server/dist/db/drizzle.js +32 -0
  56. package/server/dist/db/schema-anime.js +64 -0
  57. package/server/dist/db/schema-asmr.js +25 -0
  58. package/server/dist/db/schema-manga.js +31 -0
  59. package/server/dist/db/schema-tv.js +33 -0
  60. package/server/dist/db.js +75 -4
  61. package/server/dist/discord-gateway.js +1 -1
  62. package/server/dist/google.js +0 -7
  63. package/server/dist/hono/asmr.js +1 -0
  64. package/server/dist/hono/auth.js +56 -0
  65. package/server/dist/hono/music.js +419 -0
  66. package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
  67. package/server/dist/hono/tv.js +197 -0
  68. package/server/dist/hono/watchlist.js +733 -0
  69. package/server/dist/lib/anilist.js +3 -3
  70. package/server/dist/lib/insights.js +185 -0
  71. package/server/dist/lib/ipc.js +1 -2
  72. package/server/dist/lib/kitsu.js +1 -28
  73. package/server/dist/lib/mal-import.js +291 -0
  74. package/server/dist/lib/migration.js +20 -34
  75. package/server/dist/lib/offline-db.js +36 -36
  76. package/server/dist/lib/tracker/manga-sync.service.js +21 -21
  77. package/server/dist/lib/tracker/sync.service.js +20 -20
  78. package/server/dist/lib/translate.js +79 -0
  79. package/server/dist/lib/ytmusic.js +15 -4
  80. package/server/dist/providers/title-matching.js +0 -1
  81. package/server/dist/repositories/asmr.repository.js +90 -87
  82. package/server/dist/repositories/insights.repository.js +24 -13
  83. package/server/dist/repositories/library.repository.js +33 -93
  84. package/server/dist/repositories/manga.repository.js +108 -116
  85. package/server/dist/repositories/notifications.repository.js +50 -31
  86. package/server/dist/repositories/queue.repository.js +36 -25
  87. package/server/dist/repositories/settings.repository.js +18 -8
  88. package/server/dist/repositories/shows-meta.repository.js +54 -41
  89. package/server/dist/repositories/temp-show-ids.repository.js +43 -31
  90. package/server/dist/repositories/tv.repository.js +98 -115
  91. package/server/dist/repositories/watched-episodes.repository.js +52 -20
  92. package/server/dist/repositories/watchlist.repository.js +36 -48
  93. package/server/dist/server.js +59 -151
  94. package/server/dist/sync.js +41 -4
  95. package/server/dist/trpc/context.js +7 -0
  96. package/server/dist/trpc/errors.js +21 -0
  97. package/server/dist/trpc/index.js +19 -0
  98. package/server/dist/trpc/router.js +49 -0
  99. package/server/dist/trpc/routers/asmr-library.js +122 -0
  100. package/server/dist/trpc/routers/asmr-progress.js +133 -0
  101. package/server/dist/trpc/routers/asmr.js +87 -0
  102. package/server/dist/trpc/routers/auth.js +247 -0
  103. package/server/dist/trpc/routers/continue-watching.js +83 -0
  104. package/server/dist/trpc/routers/data.js +1039 -0
  105. package/server/dist/trpc/routers/discord.js +288 -0
  106. package/server/dist/trpc/routers/discovery.js +15 -0
  107. package/server/dist/trpc/routers/insights.js +45 -0
  108. package/server/dist/trpc/routers/manga-library.js +280 -0
  109. package/server/dist/trpc/routers/manga-progress.js +181 -0
  110. package/server/dist/trpc/routers/manga.js +201 -0
  111. package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +235 -295
  112. package/server/dist/trpc/routers/notifications.js +80 -0
  113. package/server/dist/trpc/routers/progress.js +15 -0
  114. package/server/dist/trpc/routers/queue.js +95 -0
  115. package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
  116. package/server/dist/trpc/routers/settings.js +105 -0
  117. package/server/dist/trpc/routers/tracker.js +219 -0
  118. package/server/dist/trpc/routers/tv-library.js +145 -0
  119. package/server/dist/trpc/routers/tv-progress.js +171 -0
  120. package/server/dist/trpc/routers/tv.js +565 -0
  121. package/server/dist/trpc/routers/watchlist.js +564 -0
  122. package/server/dist/trpc/validation.js +159 -0
  123. package/server/dist/utils/security.utils.js +0 -18
  124. package/server/package.json +5 -10
  125. package/client/dist/assets/Asmr-D0EGRYsv.js +0 -2
  126. package/client/dist/assets/AsmrResetProgressModal-Gs0GmVaT.js +0 -1
  127. package/client/dist/assets/AvSyncCalibrator-D8hqarVB.js +0 -10
  128. package/client/dist/assets/Home-BnBzjFkW.js +0 -1
  129. package/client/dist/assets/Insights-i9QgY3nE.js +0 -1
  130. package/client/dist/assets/MangaReadPage-xJB8K2YU.js +0 -1
  131. package/client/dist/assets/Mature-DQtXKOXc.js +0 -1
  132. package/client/dist/assets/MediaCard-DpKoY-1j.js +0 -1
  133. package/client/dist/assets/Music-BP43Zqmq.js +0 -1
  134. package/client/dist/assets/OptionListMenu-Bj9ebsW3.js +0 -1
  135. package/client/dist/assets/Player-O2Cy2GF1.js +0 -56
  136. package/client/dist/assets/PlayerSettings-DYZ272Yp.js +0 -1
  137. package/client/dist/assets/QueueOptionsButton-lv26-6jp.js +0 -1
  138. package/client/dist/assets/Radio-qQBYnVfY.js +0 -1
  139. package/client/dist/assets/Search-BkvPYN33.js +0 -1
  140. package/client/dist/assets/Settings-CyZEBF6S.js +0 -21
  141. package/client/dist/assets/Trackers-JK-aXBYC.js +0 -2
  142. package/client/dist/assets/Tv-BwXWJxMy.js +0 -4
  143. package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
  144. package/client/dist/assets/TvInfo-Dzyq6ucH.js +0 -1
  145. package/client/dist/assets/TvPopup-PlCU30FO.js +0 -1
  146. package/client/dist/assets/Watchlist-BOEeGXH9.js +0 -1
  147. package/client/dist/assets/index-CtoKRNLc.js +0 -178
  148. package/client/dist/assets/useAnimeInfoData-kr3WOIWH.js +0 -1
  149. package/client/dist/assets/useAsmr-CB9THf0m.js +0 -1
  150. package/client/dist/assets/useMangaLibrary-rM6z3_0E.js +0 -1
  151. package/client/dist/assets/useProviders-B3VB7A9M.js +0 -1
  152. package/client/dist/assets/useTv-BTUKMH7Q.js +0 -1
  153. package/client/dist/assets/useTvLibrary-C0sX-ilZ.js +0 -1
  154. package/server/dist/controllers/asmr-library.controller.js +0 -288
  155. package/server/dist/controllers/auth.controller.js +0 -225
  156. package/server/dist/controllers/data.controller.js +0 -767
  157. package/server/dist/controllers/insights.controller.js +0 -203
  158. package/server/dist/controllers/manga-library.controller.js +0 -350
  159. package/server/dist/controllers/settings.controller.js +0 -609
  160. package/server/dist/controllers/tv-library.controller.js +0 -314
  161. package/server/dist/controllers/watchlist.controller.js +0 -1069
  162. package/server/dist/request-logger.js +0 -16
  163. package/server/dist/routes/asmr-library.routes.js +0 -23
  164. package/server/dist/routes/asmr.routes.js +0 -81
  165. package/server/dist/routes/auth.routes.js +0 -25
  166. package/server/dist/routes/data.routes.js +0 -47
  167. package/server/dist/routes/discord-gateway.routes.js +0 -51
  168. package/server/dist/routes/insights.routes.js +0 -35
  169. package/server/dist/routes/lan-auth.routes.js +0 -46
  170. package/server/dist/routes/manga-library.routes.js +0 -25
  171. package/server/dist/routes/manga.routes.js +0 -141
  172. package/server/dist/routes/music.routes.js +0 -20
  173. package/server/dist/routes/providers.routes.js +0 -17
  174. package/server/dist/routes/proxy.routes.js +0 -12
  175. package/server/dist/routes/settings.routes.js +0 -24
  176. package/server/dist/routes/tracker.routes.js +0 -287
  177. package/server/dist/routes/translate.routes.js +0 -88
  178. package/server/dist/routes/tv-library.routes.js +0 -23
  179. package/server/dist/routes/tv.routes.js +0 -629
  180. package/server/dist/routes/watchlist.routes.js +0 -341
@@ -1,629 +0,0 @@
1
- import { Router } from 'express';
2
- import { parseJsonBody } from '../utils/http.utils.js';
3
- import { getTmdbKey, TMDB_BASE, TMDB_IMAGE } from '../lib/tmdb.js';
4
- import { URL } from 'url';
5
- import { isSafeExternalUrl } from '../utils/security.utils.js';
6
- async function resolveTvMeta(mediaType, numericTmdbId, partial) {
7
- const meta = { ...partial };
8
- if (meta.title && meta.imdbId && meta.year)
9
- return meta;
10
- try {
11
- const tmdbKey = await getTmdbKey();
12
- if (tmdbKey) {
13
- const tmdbRes = await fetch(`${TMDB_BASE}/${mediaType}/${numericTmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`, { headers: { 'User-Agent': 'Mozilla/5.0' }, signal: AbortSignal.timeout(4000) });
14
- if (tmdbRes.ok) {
15
- const d = await parseJsonBody(tmdbRes);
16
- if (!meta.title)
17
- meta.title = d.title || d.name || '';
18
- if (!meta.year)
19
- meta.year = (d.release_date || d.first_air_date || '').split('-')[0] || '';
20
- if (!meta.imdbId)
21
- meta.imdbId = d.external_ids?.imdb_id || d.imdb_id || '';
22
- if (mediaType === 'tv' && d.number_of_seasons)
23
- meta.totalSeasons = String(d.number_of_seasons);
24
- }
25
- }
26
- }
27
- catch {
28
- // ignore
29
- }
30
- return meta;
31
- }
32
- function proxiedMediaUrl(targetUrl, refererStr) {
33
- return `/api/tv/stream-proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}`;
34
- }
35
- export function rewriteTvPlaylist(body, baseUrl, refererStr) {
36
- return body
37
- .split('\n')
38
- .map((line) => {
39
- const trimmed = line.trim();
40
- if (!trimmed)
41
- return line;
42
- if (trimmed.startsWith('#')) {
43
- return trimmed.replace(/URI="([^"]+)"/g, (_, uri) => {
44
- const absolute = new URL(uri, baseUrl).href;
45
- return `URI="${proxiedMediaUrl(absolute, refererStr)}"`;
46
- });
47
- }
48
- const absolute = new URL(trimmed, baseUrl).href;
49
- return proxiedMediaUrl(absolute, refererStr);
50
- })
51
- .join('\n');
52
- }
53
- export function isPlaylistBody(body) {
54
- return (body.length >= 7 &&
55
- body
56
- .subarray(0, 32)
57
- .toString('utf8')
58
- .replace(/^\uFEFF/, '')
59
- .trimStart()
60
- .startsWith('#EXTM3U'));
61
- }
62
- function sendRewrittenPlaylist(res, rewritten) {
63
- const bodyBuffer = Buffer.from(rewritten, 'utf8');
64
- res.set('Content-Type', 'application/vnd.apple.mpegurl');
65
- res.set('Content-Length', String(bodyBuffer.length));
66
- if (!res.headersSent) {
67
- res.send(bodyBuffer);
68
- }
69
- }
70
- export function createTvRouter(apiCache, getTvProvider) {
71
- const router = Router();
72
- router.get('/tv/search', async (req, res) => {
73
- const query = req.query.q || '';
74
- const page = parseInt(req.query.page) || 1;
75
- const type = req.query.type || 'multi';
76
- const genre = req.query.genre || '';
77
- const year = req.query.year || '';
78
- const sortBy = req.query.sort_by || 'popularity.desc';
79
- const hasTypeFilter = type === 'tv' || type === 'movie';
80
- const hasGenre = !!genre;
81
- const hasYear = !!year && year !== 'ALL';
82
- const shouldFallbackToTrending = !query && !hasTypeFilter && !hasGenre && !hasYear;
83
- const cacheKey = `tv-search-${query.toLowerCase()}-${page}-${type}-${genre}-${year}-${sortBy}`;
84
- const cached = apiCache.get(cacheKey);
85
- if (cached)
86
- return res.json(cached);
87
- const key = await getTmdbKey();
88
- if (!key)
89
- return res.status(500).json({ error: 'No TMDB API key available' });
90
- try {
91
- let url;
92
- if (type === 'tv' || type === 'movie') {
93
- if (!query.trim()) {
94
- const params = new URLSearchParams({
95
- api_key: key,
96
- page: String(page),
97
- sort_by: sortBy,
98
- });
99
- if (genre)
100
- params.set('with_genres', genre);
101
- if (year) {
102
- if (type === 'tv')
103
- params.set('first_air_date_year', year);
104
- else
105
- params.set('primary_release_year', year);
106
- }
107
- url = `${TMDB_BASE}/discover/${type}?${params.toString()}`;
108
- }
109
- else {
110
- const params = new URLSearchParams({
111
- api_key: key,
112
- query: encodeURIComponent(query.trim()),
113
- page: String(page),
114
- include_adult: 'true',
115
- });
116
- url = `${TMDB_BASE}/search/${type}?${params.toString()}`;
117
- }
118
- }
119
- else if (!query.trim()) {
120
- const params = new URLSearchParams({
121
- api_key: key,
122
- page: String(page),
123
- sort_by: sortBy,
124
- });
125
- if (genre)
126
- params.set('with_genres', genre);
127
- if (year)
128
- params.set('first_air_date_year', year);
129
- url = `${TMDB_BASE}/discover/tv?${params.toString()}`;
130
- }
131
- else {
132
- const params = new URLSearchParams({
133
- api_key: key,
134
- query: encodeURIComponent(query),
135
- page: String(page),
136
- include_adult: 'true',
137
- });
138
- url = `${TMDB_BASE}/search/multi?${params.toString()}`;
139
- }
140
- const r = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
141
- if (!r.ok)
142
- return res.status(500).json({ error: 'TMDB search failed' });
143
- const d = await parseJsonBody(r);
144
- let results;
145
- if (type === 'tv' || type === 'movie') {
146
- results = (d.results || []).map((item) => ({
147
- id: item.id,
148
- title: item.title || item.name || '',
149
- year: (item.release_date || item.first_air_date || '').split('-')[0],
150
- type,
151
- image: item.poster_path ? `${TMDB_IMAGE}/w500${item.poster_path}` : '',
152
- backdrop: item.backdrop_path ? `${TMDB_IMAGE}/w780${item.backdrop_path}` : '',
153
- overview: item.overview || '',
154
- vote_average: item.vote_average || 0,
155
- adult: item.adult === true,
156
- genre_ids: item.genre_ids || [],
157
- }));
158
- }
159
- else {
160
- results = (d.results || [])
161
- .filter((item) => item.media_type !== 'person')
162
- .map((item) => ({
163
- id: item.id,
164
- title: item.title || item.name || '',
165
- year: (item.release_date || item.first_air_date || '').split('-')[0],
166
- type: item.media_type || 'tv',
167
- image: item.poster_path ? `${TMDB_IMAGE}/w500${item.poster_path}` : '',
168
- backdrop: item.backdrop_path ? `${TMDB_IMAGE}/w780${item.backdrop_path}` : '',
169
- overview: item.overview || '',
170
- vote_average: item.vote_average || 0,
171
- adult: item.adult === true,
172
- genre_ids: item.genre_ids || [],
173
- }));
174
- }
175
- const payload = {
176
- results,
177
- total_results: d.total_results || 0,
178
- total_pages: d.total_pages || 0,
179
- page: d.page || page,
180
- };
181
- apiCache.set(cacheKey, payload, 1800);
182
- res.json(payload);
183
- }
184
- catch (e) {
185
- res.status(500).json({ error: e.message });
186
- }
187
- });
188
- router.get('/tv/trending', async (req, res) => {
189
- const mediaType = req.query.media_type || 'all';
190
- const timeWindow = req.query.time_window || 'week';
191
- const page = parseInt(req.query.page) || 1;
192
- const cacheKey = `tv-trending-${mediaType}-${timeWindow}-${page}`;
193
- const cached = apiCache.get(cacheKey);
194
- if (cached)
195
- return res.json(cached);
196
- const key = await getTmdbKey();
197
- if (!key)
198
- return res.status(500).json({ error: 'No TMDB API key available' });
199
- try {
200
- const r = await fetch(`${TMDB_BASE}/trending/${mediaType}/${timeWindow}?api_key=${key}&page=${page}`, { headers: { 'User-Agent': 'Mozilla/5.0' } });
201
- if (!r.ok)
202
- return res.status(500).json({ error: 'TMDB trending failed' });
203
- const d = await parseJsonBody(r);
204
- const results = (d.results || [])
205
- .filter((item) => item.media_type !== 'person')
206
- .map((item) => ({
207
- id: item.id,
208
- title: item.title || item.name || '',
209
- year: (item.release_date || item.first_air_date || '').split('-')[0],
210
- type: item.media_type || 'tv',
211
- image: item.poster_path ? `${TMDB_IMAGE}/w500${item.poster_path}` : '',
212
- backdrop: item.backdrop_path ? `${TMDB_IMAGE}/w780${item.backdrop_path}` : '',
213
- overview: item.overview || '',
214
- vote_average: item.vote_average || 0,
215
- adult: item.adult === true,
216
- genre_ids: item.genre_ids || [],
217
- }));
218
- const payload = {
219
- results,
220
- total_results: d.total_results || 0,
221
- total_pages: d.total_pages || 0,
222
- page: d.page || page,
223
- };
224
- apiCache.set(cacheKey, payload, 3600);
225
- res.json(payload);
226
- }
227
- catch (e) {
228
- res.status(500).json({ error: e.message });
229
- }
230
- });
231
- router.get('/tv/genres', async (_req, res) => {
232
- const cacheKey = 'tv-genres-list';
233
- const cached = apiCache.get(cacheKey);
234
- if (cached)
235
- return res.json(cached);
236
- const key = await getTmdbKey();
237
- if (!key)
238
- return res.status(500).json({ error: 'No TMDB API key available' });
239
- try {
240
- const [tvRes, movieRes] = await Promise.all([
241
- fetch(`${TMDB_BASE}/genre/tv/list?api_key=${key}`, {
242
- headers: { 'User-Agent': 'Mozilla/5.0' },
243
- }),
244
- fetch(`${TMDB_BASE}/genre/movie/list?api_key=${key}`, {
245
- headers: { 'User-Agent': 'Mozilla/5.0' },
246
- }),
247
- ]);
248
- const tvData = tvRes.ok
249
- ? await parseJsonBody(tvRes)
250
- : { genres: [] };
251
- const movieData = movieRes.ok
252
- ? await parseJsonBody(movieRes)
253
- : { genres: [] };
254
- const payload = {
255
- tv: (tvData.genres || []).map((g) => ({
256
- id: g.id,
257
- name: g.name,
258
- })),
259
- movie: (movieData.genres || []).map((g) => ({
260
- id: g.id,
261
- name: g.name,
262
- })),
263
- };
264
- apiCache.set(cacheKey, payload, 86400);
265
- res.json(payload);
266
- }
267
- catch (e) {
268
- res.status(500).json({ error: e.message });
269
- }
270
- });
271
- router.get('/tv/details/:type/:id', async (req, res) => {
272
- const { type, id } = req.params;
273
- const key = await getTmdbKey();
274
- if (!key)
275
- return res.status(500).json({ error: 'No TMDB API key available' });
276
- try {
277
- const r = await fetch(`${TMDB_BASE}/${type}/${id}?api_key=${key}&append_to_response=external_ids`, { headers: { 'User-Agent': 'Mozilla/5.0' } });
278
- if (!r.ok)
279
- return res.status(500).json({ error: 'TMDB details failed' });
280
- const d = await parseJsonBody(r);
281
- const result = {
282
- id: d.id,
283
- imdb_id: d.external_ids?.imdb_id || d.imdb_id || null,
284
- title: d.title || d.name,
285
- overview: d.overview,
286
- vote_average: d.vote_average,
287
- year: (d.release_date || d.first_air_date || '').split('-')[0],
288
- poster: d.poster_path ? `${TMDB_IMAGE}/w500${d.poster_path}` : '',
289
- backdrop: d.backdrop_path ? `${TMDB_IMAGE}/original${d.backdrop_path}` : '',
290
- adult: d.adult === true,
291
- };
292
- if (type === 'tv') {
293
- result.seasons = (d.seasons || [])
294
- .filter((s) => s.season_number > 0)
295
- .map((s) => ({
296
- season_number: s.season_number,
297
- episode_count: s.episode_count,
298
- }));
299
- result.number_of_seasons = d.number_of_seasons;
300
- }
301
- res.json(result);
302
- }
303
- catch (e) {
304
- res.status(500).json({ error: e.message });
305
- }
306
- });
307
- router.get('/tv/episodes/:id/:season', async (req, res) => {
308
- const { id, season } = req.params;
309
- const key = await getTmdbKey();
310
- if (!key)
311
- return res.status(500).json({ error: 'No TMDB API key available' });
312
- try {
313
- const r = await fetch(`${TMDB_BASE}/tv/${id}/season/${season}?api_key=${key}`, {
314
- headers: { 'User-Agent': 'Mozilla/5.0' },
315
- });
316
- if (!r.ok)
317
- return res.status(500).json({ error: 'TMDB episodes failed' });
318
- const d = await parseJsonBody(r);
319
- const episodes = (d.episodes || []).map((ep) => ({
320
- episode_number: ep.episode_number,
321
- name: ep.name,
322
- vote_average: ep.vote_average,
323
- overview: ep.overview,
324
- still_path: ep.still_path,
325
- }));
326
- res.json({ episodes });
327
- }
328
- catch (e) {
329
- res.status(500).json({ error: e.message });
330
- }
331
- });
332
- router.get('/tv/lookup/imdb-to-tmdb/:imdbId', async (req, res) => {
333
- const key = await getTmdbKey();
334
- if (!key)
335
- return res.json({ tmdbId: null, error: 'No TMDB API key available' });
336
- try {
337
- const r = await fetch(`${TMDB_BASE}/find/${req.params.imdbId}?api_key=${key}&external_source=imdb_id`, { headers: { 'User-Agent': 'Mozilla/5.0' } });
338
- if (!r.ok)
339
- return res.json({ tmdbId: null, error: `TMDB error ${r.status}` });
340
- const d = await parseJsonBody(r);
341
- const movie = d.movie_results?.[0];
342
- const tv = d.tv_results?.[0];
343
- const result = movie || tv;
344
- res.json({
345
- tmdbId: result?.id || null,
346
- type: movie ? 'movie' : tv ? 'tv' : null,
347
- title: result?.title || result?.name || null,
348
- year: (result?.release_date || result?.first_air_date || '').substring(0, 4) || null,
349
- });
350
- }
351
- catch (e) {
352
- res.json({ tmdbId: null, error: e.message });
353
- }
354
- });
355
- router.get('/tv/search/imdb', async (req, res) => {
356
- const query = req.query.q || '';
357
- if (!query)
358
- return res.json([]);
359
- const cacheKey = `tv-imdb-search-${query.toLowerCase()}`;
360
- const cached = apiCache.get(cacheKey);
361
- if (cached)
362
- return res.json(cached);
363
- try {
364
- const response = await fetch(`https://v3.sg.media-imdb.com/suggestion/x/${encodeURIComponent(query)}.json`, { headers: { 'User-Agent': 'Mozilla/5.0' } });
365
- if (!response.ok)
366
- return res.status(500).json({ error: 'IMDB search failed' });
367
- const data = await parseJsonBody(response);
368
- const TV_TYPES = new Set(['tvSeries', 'tvMiniSeries', 'movie']);
369
- const matches = (data.d || [])
370
- .filter((entry) => Boolean(entry.id && entry.l) && TV_TYPES.has(entry.qid || ''))
371
- .slice(0, 10)
372
- .map((entry) => ({
373
- id: entry.id,
374
- title: entry.l,
375
- year: entry.y,
376
- type: entry.qid,
377
- image: entry.i?.imageUrl || '',
378
- }));
379
- apiCache.set(cacheKey, matches, 3600);
380
- res.json(matches);
381
- }
382
- catch (e) {
383
- res.status(500).json({ error: e.message });
384
- }
385
- });
386
- router.get('/tv/subtitles/:type/:tmdbId', async (req, res) => {
387
- const { type, tmdbId } = req.params;
388
- const numericTmdbId = parseInt(tmdbId, 10);
389
- if (!numericTmdbId)
390
- return res.json({ subtitles: [] });
391
- const mediaType = type === 'movie' ? 'movie' : 'tv';
392
- const season = String(req.query.season || '1');
393
- const episode = String(req.query.episode || '1');
394
- const cacheKey = mediaType === 'tv'
395
- ? `tv-subs-${numericTmdbId}-${season}-${episode}`
396
- : `tv-subs-${numericTmdbId}-movie`;
397
- const cached = apiCache.get(cacheKey);
398
- if (cached)
399
- return res.json(cached);
400
- try {
401
- const searchUrl = mediaType === 'tv'
402
- ? `https://subtitles.vidy.st/search?id=${numericTmdbId}&season=${encodeURIComponent(season)}&episode=${encodeURIComponent(episode)}`
403
- : `https://subtitles.vidy.st/search?id=${numericTmdbId}`;
404
- const r = await fetch(searchUrl, {
405
- headers: { 'User-Agent': 'Mozilla/5.0' },
406
- signal: AbortSignal.timeout(10000),
407
- });
408
- if (!r.ok)
409
- return res.json({ subtitles: [] });
410
- const items = (await parseJsonBody(r));
411
- const subtitles = (Array.isArray(items) ? items : [])
412
- .filter((s) => typeof s.url === 'string' && s.url.startsWith('https://'))
413
- .map((s) => ({
414
- language: String(s.language || 'en'),
415
- label: String(s.display || s.language || 'Unknown'),
416
- url: String(s.url),
417
- trusted: s.isTrusted === true,
418
- hi: s.isHearingImpaired === true,
419
- downloads: Number(s.downloadCount) || 0,
420
- media: String(s.media || s.release || ''),
421
- }))
422
- .sort((a, b) => {
423
- const aEn = a.language.toLowerCase().startsWith('en') ? 0 : 1;
424
- const bEn = b.language.toLowerCase().startsWith('en') ? 0 : 1;
425
- if (aEn !== bEn)
426
- return aEn - bEn;
427
- if (a.trusted !== b.trusted)
428
- return a.trusted ? -1 : 1;
429
- return b.downloads - a.downloads;
430
- })
431
- .slice(0, 50);
432
- const payload = { subtitles };
433
- apiCache.set(cacheKey, payload, 3600);
434
- res.json(payload);
435
- }
436
- catch {
437
- res.json({ subtitles: [] });
438
- }
439
- });
440
- router.get('/tv/sources/:provider/:type/:tmdbId', async (req, res) => {
441
- const { provider, type, tmdbId } = req.params;
442
- const mediaType = type === 'movie' ? 'movie' : 'tv';
443
- const numericTmdbId = parseInt(tmdbId, 10);
444
- if (!numericTmdbId)
445
- return res.json({ sources: [], audioTracks: [], error: 'Bad tmdb id' });
446
- const mod = getTvProvider(String(provider).toLowerCase());
447
- if (!mod || typeof mod.getSources !== 'function') {
448
- return res.json({ sources: [], audioTracks: [], error: `Unknown TV provider ${provider}` });
449
- }
450
- const season = parseInt(String(req.query.season || '1'), 10) || 1;
451
- const episode = parseInt(String(req.query.episode || '1'), 10) || 1;
452
- const server = typeof req.query.server === 'string' ? req.query.server : undefined;
453
- const meta = await resolveTvMeta(mediaType, numericTmdbId, {
454
- title: String(req.query.title || ''),
455
- year: String(req.query.year || ''),
456
- imdbId: String(req.query.imdbId || ''),
457
- totalSeasons: String(req.query.totalSeasons || '1'),
458
- });
459
- const media = {
460
- tmdbId: numericTmdbId,
461
- type: mediaType,
462
- season,
463
- episode,
464
- title: meta.title,
465
- year: meta.year,
466
- imdbId: meta.imdbId,
467
- totalSeasons: parseInt(meta.totalSeasons, 10) || 1,
468
- };
469
- try {
470
- const result = await mod.getSources(media, server);
471
- if (!result || !Array.isArray(result.sources) || result.sources.length === 0) {
472
- return res.json({
473
- provider: mod.name,
474
- server: result?.server || server,
475
- sources: [],
476
- audioTracks: [],
477
- valid: false,
478
- error: 'No sources found',
479
- });
480
- }
481
- res.json({
482
- provider: mod.name,
483
- server: result.server || server,
484
- sources: result.sources,
485
- audioTracks: result.audioTracks || [],
486
- subtitles: result.subtitles || [],
487
- referer: result.referer || '',
488
- valid: true,
489
- });
490
- }
491
- catch (e) {
492
- res.json({
493
- provider: mod.name,
494
- sources: [],
495
- audioTracks: [],
496
- valid: false,
497
- error: e.message,
498
- });
499
- }
500
- });
501
- router.get('/tv/embed/:provider/:type/:tmdbId', async (req, res) => {
502
- const { provider, type, tmdbId } = req.params;
503
- const mediaType = type === 'movie' ? 'movie' : 'tv';
504
- const numericTmdbId = parseInt(tmdbId, 10);
505
- if (!numericTmdbId)
506
- return res.json({ url: null, error: 'Bad tmdb id' });
507
- const mod = getTvProvider(String(provider).toLowerCase());
508
- if (!mod || typeof mod.getEmbedUrl !== 'function') {
509
- return res.json({ url: null, error: `Unknown TV provider ${provider}` });
510
- }
511
- const media = {
512
- tmdbId: numericTmdbId,
513
- type: mediaType,
514
- season: parseInt(String(req.query.season || '1'), 10) || 1,
515
- episode: parseInt(String(req.query.episode || '1'), 10) || 1,
516
- title: '',
517
- year: '',
518
- imdbId: '',
519
- totalSeasons: 1,
520
- };
521
- try {
522
- const url = await mod.getEmbedUrl(media);
523
- if (!url)
524
- return res.json({ url: null, error: 'No embed url' });
525
- res.json({ url });
526
- }
527
- catch (e) {
528
- res.json({ url: null, error: e.message });
529
- }
530
- });
531
- router.get('/tv/stream-proxy', async (req, res) => {
532
- const { url, referer } = req.query;
533
- const urlStr = url;
534
- const refererStr = referer || '';
535
- if (!urlStr)
536
- return res.status(400).send('URL required');
537
- const safeCheck = isSafeExternalUrl(urlStr);
538
- if (!safeCheck.safe) {
539
- return res.status(400).send(safeCheck.error || 'Invalid URL');
540
- }
541
- const abortController = new AbortController();
542
- const timeout = setTimeout(() => abortController.abort(), 30000);
543
- res.on('close', () => {
544
- clearTimeout(timeout);
545
- abortController.abort();
546
- });
547
- try {
548
- const headers = {
549
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
550
- };
551
- if (refererStr)
552
- headers['Referer'] = refererStr;
553
- const fetchResp = await fetch(urlStr, {
554
- headers,
555
- signal: abortController.signal,
556
- redirect: 'follow',
557
- });
558
- const status = fetchResp.status;
559
- if (status !== 200 && status !== 206) {
560
- return res.status(status ?? 502).send('Upstream error');
561
- }
562
- const contentType = fetchResp.headers.get('content-type') || 'application/octet-stream';
563
- const contentLength = fetchResp.headers.get('content-length');
564
- const contentRange = fetchResp.headers.get('content-range');
565
- const acceptRanges = fetchResp.headers.get('accept-ranges');
566
- res.status(status);
567
- res.set('Content-Type', contentType);
568
- if (contentLength)
569
- res.set('Content-Length', contentLength);
570
- if (contentRange)
571
- res.set('Content-Range', contentRange);
572
- if (acceptRanges)
573
- res.set('Accept-Ranges', acceptRanges);
574
- res.set('Access-Control-Allow-Origin', '*');
575
- res.set('Connection', 'keep-alive');
576
- if (urlStr.includes('.m3u8') || /mpegurl|m3u8/i.test(contentType)) {
577
- const body = await fetchResp.text();
578
- const baseUrl = new URL(fetchResp.url || urlStr);
579
- sendRewrittenPlaylist(res, rewriteTvPlaylist(body, baseUrl, refererStr));
580
- }
581
- else {
582
- const chunks = [];
583
- const reader = fetchResp.body?.getReader();
584
- if (!reader) {
585
- return res.status(500).send('No response body');
586
- }
587
- try {
588
- while (true) {
589
- const { done, value } = await reader.read();
590
- if (done)
591
- break;
592
- chunks.push(Buffer.from(value));
593
- }
594
- const body = Buffer.concat(chunks);
595
- if (isPlaylistBody(body)) {
596
- const baseUrl = new URL(fetchResp.url || urlStr);
597
- sendRewrittenPlaylist(res, rewriteTvPlaylist(body.toString('utf8'), baseUrl, refererStr));
598
- return;
599
- }
600
- res.set('Content-Type', contentType);
601
- if (contentLength)
602
- res.set('Content-Length', contentLength);
603
- if (contentRange)
604
- res.set('Content-Range', contentRange);
605
- if (acceptRanges)
606
- res.set('Accept-Ranges', acceptRanges);
607
- res.set('Access-Control-Allow-Origin', '*');
608
- res.set('Connection', 'keep-alive');
609
- if (!res.headersSent) {
610
- res.send(body);
611
- }
612
- }
613
- catch (e) {
614
- if (!res.headersSent) {
615
- res.status(500).send('Proxy error');
616
- }
617
- }
618
- }
619
- }
620
- catch (e) {
621
- if (abortController.signal.aborted)
622
- return;
623
- if (!res.headersSent) {
624
- res.status(500).send('Proxy error');
625
- }
626
- }
627
- });
628
- return router;
629
- }