@serifpersia/dango 3.6.5 → 3.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-JkXG40Es.js → AnimeCard-DraA_TF9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-2L0UJkbB.js → AnimeInfo-BJ7pzs-P.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-BDs03vKk.js → AnimeInfoPage-B_esUr5C.js} +2 -2
  5. package/client/dist/assets/{Asmr-D2mVKEl9.js → Asmr-Ckuh2xfW.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-MQA_bNQM.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
  7. package/client/dist/assets/AvSyncCalibrator-DwHCd9yB.js +10 -0
  8. package/client/dist/assets/{Home-DxMBHFmc.js → Home-DuM1EuvN.js} +1 -1
  9. package/client/dist/assets/{Insights-hmLWsSXY.js → Insights-D3Pd9c9V.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-CWI7hgUh.js → LibraryListPage-Dm2yVn0-.js} +1 -1
  11. package/client/dist/assets/{ListeningList-NOc28KAV.js → ListeningList-D-zt6EXZ.js} +1 -1
  12. package/client/dist/assets/{Manga-B5qNhI7v.js → Manga-JwprMKBk.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-D4wIyb-V.js → MangaInfoPage-DaeyJl2p.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-Ce1VX7-0.js → MangaReadPage-COg0fHI9.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-P3oY4_14.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
  16. package/client/dist/assets/{Mature-D8EwGYQq.js → Mature-IHDaasrs.js} +1 -1
  17. package/client/dist/assets/{MediaCard-Dp8DjyXB.js → MediaCard-CpwsrXo5.js} +1 -1
  18. package/client/dist/assets/{Music-DULtVsAM.js → Music-Dks4tPeK.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-fIHdE-nq.js → OptionListMenu-11CcAsy8.js} +1 -1
  20. package/client/dist/assets/{Player-B8XeOmso.js → Player-DQoEQkHL.js} +6 -6
  21. package/client/dist/assets/{PlayerSettings-BycNCiCs.js → PlayerSettings-D4Ud4kv4.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-T1J37IZT.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
  23. package/client/dist/assets/{QueueRail-BAwI-apz.js → QueueRail-B4Dok7U3.js} +1 -1
  24. package/client/dist/assets/{Radio-Bv9DyKtZ.js → Radio-BHK6LSk4.js} +1 -1
  25. package/client/dist/assets/{ReadingList-Cl0CcVp3.js → ReadingList-BCsxIz57.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal--N60WWCB.js → ResetProgressModal-BXHxSfy-.js} +1 -1
  27. package/client/dist/assets/{Search-DvgVDlUR.js → Search-PK1ds07O.js} +1 -1
  28. package/client/dist/assets/{Settings-dCz_-bJn.js → Settings-Bxk_x55l.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-BZHKk1I6.js → SkeletonGrid-CpJCZSRN.js} +1 -1
  30. package/client/dist/assets/{Trackers-C1T5VcPu.js → Trackers-RieCh_84.js} +1 -1
  31. package/client/dist/assets/{Tv-Dhv_cg4y.js → Tv-DLytPJUj.js} +4 -4
  32. package/client/dist/assets/{TvInfo-DgaLK2H6.js → TvInfo-C8djKebR.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DyX7f8kC.js → TvPopup-40JGGJDk.js} +1 -1
  34. package/client/dist/assets/{TvSearch-Bp0H6kue.js → TvSearch-A3hbospf.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-ohMtMeKa.js → TvWatchlist-DuXjqmZM.js} +1 -1
  36. package/client/dist/assets/{Watchlist-BbQtzsER.js → Watchlist-B9wGBh0u.js} +1 -1
  37. package/client/dist/assets/{hls-Bi1cjpLZ.js → hls-CZ5X3Ya-.js} +1 -1
  38. package/client/dist/assets/{index-BTnhzofk.js → index-dRhc9-ol.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-Cvmso3IK.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
  40. package/client/dist/assets/{useAsmr-CbzGCzmv.js → useAsmr-CKOdHgol.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-DaREZ9Ie.js → useAsmrPopup-SFHPhpcl.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-BVQRhsFh.js → useMangaLibrary-CKATe31P.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-CzsCbndG.js → useMangaPopup-C75QuYgY.js} +1 -1
  44. package/client/dist/assets/{useProviders-UuUPzft-.js → useProviders-CMtvZbEi.js} +1 -1
  45. package/client/dist/assets/{useTv-CSsbWKH9.js → useTv-DUX4zYMr.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-a_Y0c2FS.js → useTvLibrary-By4Gt-R_.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/app-auth.js +1 -157
  50. package/server/dist/app-hono.js +7 -557
  51. package/server/dist/config.js +1 -174
  52. package/server/dist/db/drizzle.js +1 -16
  53. package/server/dist/db/schema-anime.js +1 -61
  54. package/server/dist/db/schema-asmr.js +1 -13
  55. package/server/dist/db/schema-manga.js +1 -31
  56. package/server/dist/db/schema-tv.js +1 -18
  57. package/server/dist/db.js +1 -217
  58. package/server/dist/discord-gateway.js +1 -385
  59. package/server/dist/discord-rpc.js +1 -413
  60. package/server/dist/github-sync.js +1 -508
  61. package/server/dist/google.js +1 -741
  62. package/server/dist/hono/asmr.js +1 -10
  63. package/server/dist/hono/auth.js +3 -26
  64. package/server/dist/hono/music.js +1 -209
  65. package/server/dist/hono/proxy.js +36 -905
  66. package/server/dist/hono/tv.js +5 -169
  67. package/server/dist/hono/watchlist.js +2 -338
  68. package/server/dist/lib/anilist.js +26 -1458
  69. package/server/dist/lib/anischedule.js +1 -191
  70. package/server/dist/lib/insights.js +1 -185
  71. package/server/dist/lib/ipc.js +1 -23
  72. package/server/dist/lib/kitsu.js +1 -517
  73. package/server/dist/lib/mal-import.js +1 -291
  74. package/server/dist/lib/mal.js +1 -957
  75. package/server/dist/lib/migration.js +1 -310
  76. package/server/dist/lib/offline-db.js +4 -384
  77. package/server/dist/lib/potoken.js +1 -135
  78. package/server/dist/lib/recommendations.service.js +1 -216
  79. package/server/dist/lib/temp-ids.js +1 -3
  80. package/server/dist/lib/tmdb.js +1 -72
  81. package/server/dist/lib/tracker/anilist-tracker.js +8 -245
  82. package/server/dist/lib/tracker/manga-sync.service.js +1 -773
  83. package/server/dist/lib/tracker/sync-state.js +1 -21
  84. package/server/dist/lib/tracker/sync.service.js +1 -476
  85. package/server/dist/lib/translate.js +3 -79
  86. package/server/dist/lib/ump.js +1 -106
  87. package/server/dist/lib/ytmusic.js +1 -162
  88. package/server/dist/logger.js +1 -16
  89. package/server/dist/providers/manga/manga.types.js +0 -1
  90. package/server/dist/providers/provider.interface.js +0 -1
  91. package/server/dist/providers/remote-loader.js +2 -605
  92. package/server/dist/providers/remote-types.js +1 -124
  93. package/server/dist/providers/title-matching.js +1 -226
  94. package/server/dist/providers/tv.types.js +0 -1
  95. package/server/dist/rclone.js +2 -120
  96. package/server/dist/repositories/asmr.repository.js +11 -46
  97. package/server/dist/repositories/insights.repository.js +11 -25
  98. package/server/dist/repositories/library.repository.js +3 -94
  99. package/server/dist/repositories/mal-cache.repository.js +1 -52
  100. package/server/dist/repositories/manga.repository.js +14 -58
  101. package/server/dist/repositories/notifications.repository.js +6 -52
  102. package/server/dist/repositories/queue.repository.js +5 -58
  103. package/server/dist/repositories/settings.repository.js +2 -29
  104. package/server/dist/repositories/shows-meta.repository.js +3 -56
  105. package/server/dist/repositories/temp-show-ids.repository.js +3 -63
  106. package/server/dist/repositories/tv.repository.js +11 -54
  107. package/server/dist/repositories/watched-episodes.repository.js +6 -55
  108. package/server/dist/repositories/watchlist.repository.js +3 -54
  109. package/server/dist/scripts/backfill-episode-duration.js +1 -42
  110. package/server/dist/server.js +1 -307
  111. package/server/dist/shipped-defaults.js +1 -6
  112. package/server/dist/sync-payload.js +1 -91
  113. package/server/dist/sync.js +3 -647
  114. package/server/dist/trpc/context.js +1 -7
  115. package/server/dist/trpc/errors.js +1 -15
  116. package/server/dist/trpc/index.js +1 -18
  117. package/server/dist/trpc/router.js +1 -49
  118. package/server/dist/trpc/routers/asmr-library.js +1 -112
  119. package/server/dist/trpc/routers/asmr-progress.js +1 -121
  120. package/server/dist/trpc/routers/asmr.js +1 -87
  121. package/server/dist/trpc/routers/auth.js +1 -194
  122. package/server/dist/trpc/routers/continue-watching.js +2 -58
  123. package/server/dist/trpc/routers/data.js +1 -1026
  124. package/server/dist/trpc/routers/discord.js +1 -285
  125. package/server/dist/trpc/routers/discovery.js +1 -11
  126. package/server/dist/trpc/routers/insights.js +1 -14
  127. package/server/dist/trpc/routers/manga-library.js +1 -263
  128. package/server/dist/trpc/routers/manga-progress.js +1 -169
  129. package/server/dist/trpc/routers/manga.js +1 -201
  130. package/server/dist/trpc/routers/music.js +1 -655
  131. package/server/dist/trpc/routers/notifications.js +1 -80
  132. package/server/dist/trpc/routers/progress.js +1 -109
  133. package/server/dist/trpc/routers/queue.js +1 -93
  134. package/server/dist/trpc/routers/radio.js +1 -145
  135. package/server/dist/trpc/routers/settings.js +1 -96
  136. package/server/dist/trpc/routers/tracker.js +1 -213
  137. package/server/dist/trpc/routers/tv-library.js +1 -135
  138. package/server/dist/trpc/routers/tv-progress.js +1 -159
  139. package/server/dist/trpc/routers/tv.js +1 -551
  140. package/server/dist/trpc/routers/watchlist.js +2 -549
  141. package/server/dist/trpc/validation.js +1 -258
  142. package/server/dist/utils/cache.utils.js +1 -25
  143. package/server/dist/utils/cookie.utils.js +1 -13
  144. package/server/dist/utils/db-utils.js +5 -17
  145. package/server/dist/utils/env.utils.js +5 -77
  146. package/server/dist/utils/http.utils.js +1 -82
  147. package/server/dist/utils/request-context.js +1 -2
  148. package/server/dist/utils/security.utils.js +1 -76
  149. package/server/package.json +3 -2
  150. package/client/dist/assets/AvSyncCalibrator-DmoH5eRA.js +0 -10
  151. package/server/dist/utils/megaplay.utils.js +0 -40
@@ -1,169 +1,5 @@
1
- import logger from '../logger.js';
2
- import { fetchWithRetry, linkAbort, setProxyHeaders } from '../utils/http.utils.js';
3
- import { URL } from 'url';
4
- import { isSafeExternalUrl } from '../utils/security.utils.js';
5
- function proxiedMediaUrl(targetUrl, refererStr, providerStr) {
6
- const provider = providerStr ? `&provider=${encodeURIComponent(providerStr)}` : '';
7
- return `/api/tv/stream-proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}${provider}`;
8
- }
9
- function rewriteTvPlaylist(body, baseUrl, refererStr, providerStr) {
10
- return body
11
- .split('\n')
12
- .map((line) => {
13
- const trimmed = line.trim();
14
- if (!trimmed)
15
- return line;
16
- if (trimmed.startsWith('#')) {
17
- return trimmed.replace(/URI="([^"]+)"/g, (_, uri) => {
18
- const absolute = new URL(uri, baseUrl).href;
19
- return `URI="${proxiedMediaUrl(absolute, refererStr, providerStr)}"`;
20
- });
21
- }
22
- const absolute = new URL(trimmed, baseUrl).href;
23
- return proxiedMediaUrl(absolute, refererStr, providerStr);
24
- })
25
- .join('\n');
26
- }
27
- function isPlaylistBody(body) {
28
- return (body.length >= 7 &&
29
- body
30
- .subarray(0, 32)
31
- .toString('utf8')
32
- .replace(/^\uFEFF/, '')
33
- .trimStart()
34
- .startsWith('#EXTM3U'));
35
- }
36
- export function registerTv(app, getTvProvider) {
37
- app.get('/api/tv/embed/:provider/:type/:tmdbId', async (c) => {
38
- const provider = c.req.param('provider');
39
- const type = c.req.param('type');
40
- const tmdbId = c.req.param('tmdbId');
41
- const mediaType = type === 'movie' ? 'movie' : 'tv';
42
- const numericTmdbId = parseInt(tmdbId, 10);
43
- if (!numericTmdbId)
44
- return c.json({ url: null, error: 'Bad tmdb id' });
45
- const mod = getTvProvider(String(provider).toLowerCase());
46
- if (!mod || typeof mod.getEmbedUrl !== 'function') {
47
- return c.json({ url: null, error: `Unknown TV provider ${provider}` });
48
- }
49
- const media = {
50
- tmdbId: numericTmdbId,
51
- type: mediaType,
52
- season: parseInt(String(c.req.query('season') || '1'), 10) || 1,
53
- episode: parseInt(String(c.req.query('episode') || '1'), 10) || 1,
54
- title: '',
55
- year: '',
56
- imdbId: '',
57
- totalSeasons: 1,
58
- };
59
- try {
60
- const url = await mod.getEmbedUrl(media);
61
- if (!url)
62
- return c.json({ url: null, error: 'No embed url' });
63
- return c.json({ url });
64
- }
65
- catch (e) {
66
- return c.json({ url: null, error: e.message });
67
- }
68
- });
69
- app.get('/api/tv/stream-proxy', async (c) => {
70
- const urlStr = c.req.query('url') ?? '';
71
- const refererStr = c.req.query('referer') || '';
72
- const providerStr = (c.req.query('provider') || '').toLowerCase();
73
- if (!urlStr)
74
- return c.text('URL required', 400);
75
- const providerMod = providerStr ? getTvProvider(providerStr) : undefined;
76
- const rewrite = (body, baseUrl) => rewriteTvPlaylist(providerMod?.rewritePlaylist?.(body, baseUrl.href) ?? body, baseUrl, refererStr, providerStr);
77
- const safeCheck = isSafeExternalUrl(urlStr);
78
- if (!safeCheck.safe) {
79
- return c.text(safeCheck.error || 'Invalid URL', 400);
80
- }
81
- const abort = new AbortController();
82
- const timeout = setTimeout(() => abort.abort(), 30000);
83
- linkAbort(c.req.raw.signal, abort);
84
- try {
85
- const headers = {
86
- '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',
87
- };
88
- if (refererStr)
89
- headers['Referer'] = refererStr;
90
- const fetchResp = await fetchWithRetry(urlStr, {
91
- headers,
92
- redirect: 'follow',
93
- }, { retries: 3, baseDelayMs: 750, signal: abort.signal });
94
- const status = fetchResp.status;
95
- if (status !== 200 && status !== 206) {
96
- return c.text('Upstream error', (status || 502));
97
- }
98
- const contentType = fetchResp.headers.get('content-type') || 'application/octet-stream';
99
- const outHeaders = new Headers();
100
- outHeaders.set('Content-Type', contentType);
101
- setProxyHeaders(outHeaders, (name) => fetchResp.headers.get(name));
102
- outHeaders.set('Connection', 'keep-alive');
103
- if (urlStr.includes('.m3u8') || /mpegurl|m3u8/i.test(contentType)) {
104
- const body = await fetchResp.text();
105
- const baseUrl = new URL(fetchResp.url || urlStr);
106
- const rewritten = rewrite(body, baseUrl);
107
- const bodyBuffer = Buffer.from(rewritten, 'utf8');
108
- outHeaders.set('Content-Type', 'application/vnd.apple.mpegurl');
109
- outHeaders.set('Content-Length', String(bodyBuffer.length));
110
- return new Response(bodyBuffer, { status, headers: outHeaders });
111
- }
112
- const reader = fetchResp.body?.getReader();
113
- if (!reader) {
114
- logger.warn({ host: safeCheck.url?.hostname }, 'tv stream-proxy: upstream sent no body');
115
- return c.text('Upstream sent no body', 502);
116
- }
117
- try {
118
- const first = await reader.read();
119
- const head = first.done ? Buffer.alloc(0) : Buffer.from(first.value);
120
- if (isPlaylistBody(head)) {
121
- const chunks = [head];
122
- while (true) {
123
- const { done, value } = await reader.read();
124
- if (done)
125
- break;
126
- chunks.push(Buffer.from(value));
127
- }
128
- const body = Buffer.concat(chunks);
129
- const baseUrl = new URL(fetchResp.url || urlStr);
130
- const rewritten = rewrite(body.toString('utf8'), baseUrl);
131
- const bodyBuffer = Buffer.from(rewritten, 'utf8');
132
- outHeaders.set('Content-Type', 'application/vnd.apple.mpegurl');
133
- outHeaders.set('Content-Length', String(bodyBuffer.length));
134
- return new Response(bodyBuffer, { status, headers: outHeaders });
135
- }
136
- const stream = new ReadableStream({
137
- start(controller) {
138
- if (head.length)
139
- controller.enqueue(head);
140
- },
141
- async pull(controller) {
142
- const { done, value } = await reader.read();
143
- if (done)
144
- controller.close();
145
- else
146
- controller.enqueue(value);
147
- },
148
- cancel(reason) {
149
- return reader.cancel(reason);
150
- },
151
- });
152
- return new Response(stream, { status, headers: outHeaders });
153
- }
154
- catch (err) {
155
- logger.warn({ err, host: safeCheck.url?.hostname }, 'tv stream-proxy: upstream body stream failed');
156
- return c.text('Upstream stream failed', 502);
157
- }
158
- }
159
- catch (err) {
160
- if (abort.signal.aborted)
161
- return new Response(null, { status: 499 });
162
- logger.warn({ err, host: safeCheck.url?.hostname }, 'tv stream-proxy: upstream fetch failed');
163
- return c.text('Upstream fetch failed', 502);
164
- }
165
- finally {
166
- clearTimeout(timeout);
167
- }
168
- });
169
- }
1
+ import x from"../logger.js";import{fetchWithRetry as j,linkAbort as E,setProxyHeaders as W}from"../utils/http.utils.js";import{URL as R}from"url";import{isSafeExternalUrl as $}from"../utils/security.utils.js";function S(o,u,e){const t=e?`&provider=${encodeURIComponent(e)}`:"";return`/api/tv/stream-proxy?url=${encodeURIComponent(o)}&referer=${encodeURIComponent(u)}${t}`}function A(o,u,e,t){return o.split(`
2
+ `).map(s=>{const n=s.trim();if(!n)return s;if(n.startsWith("#"))return n.replace(/URI="([^"]+)"/g,(i,p)=>{const h=new R(p,u).href;return`URI="${S(h,e,t)}"`});const r=new R(n,u).href;return S(r,e,t)}).join(`
3
+ `)}function H(o,u){const e=o.split(`
4
+ `);let t=-1,s=!1;const n=[];for(const r of e){const i=r.trim();if(i.startsWith("#EXT-X-STREAM-INF")){t++,s=t!==u,s||n.push(r);continue}if(s&&i&&!i.startsWith("#")){s=!1;continue}n.push(r)}return t>=0&&u<=t?n.join(`
5
+ `):o}function N(o){return o.length>=7&&o.subarray(0,32).toString("utf8").replace(/^\uFEFF/,"").trimStart().startsWith("#EXTM3U")}function V(o,u){o.get("/api/tv/embed/:provider/:type/:tmdbId",async e=>{const t=e.req.param("provider"),s=e.req.param("type"),n=e.req.param("tmdbId"),r=s==="movie"?"movie":"tv",i=parseInt(n,10);if(!i)return e.json({url:null,error:"Bad tmdb id"});const p=u(String(t).toLowerCase());if(!p||typeof p.getEmbedUrl!="function")return e.json({url:null,error:`Unknown TV provider ${t}`});const h={tmdbId:i,type:r,season:parseInt(String(e.req.query("season")||"1"),10)||1,episode:parseInt(String(e.req.query("episode")||"1"),10)||1,title:"",year:"",imdbId:"",totalSeasons:1};try{const m=await p.getEmbedUrl(h);return m?e.json({url:m}):e.json({url:null,error:"No embed url"})}catch(m){return e.json({url:null,error:m.message})}}),o.get("/api/tv/stream-proxy",async e=>{const t=e.req.query("url")??"",s=t.match(/#v=(\d+)$/),n=s?parseInt(s[1],10):NaN,r=t.replace(/#.*$/,""),i=e.req.query("referer")||"",p=(e.req.query("provider")||"").toLowerCase();if(!r)return e.text("URL required",400);const h=p?u(p):void 0,m=(g,l)=>{const f=h?.rewritePlaylist?.(g,l.href)??g,U=Number.isInteger(n)?H(f,n):f;return A(U,l,i,p)},v=$(r);if(!v.safe)return e.text(v.error||"Invalid URL",400);const b=new AbortController,B=setTimeout(()=>b.abort(),3e4);E(e.req.raw.signal,b);try{const g={"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"};i&&(g.Referer=i);const l=await j(r,{headers:g,redirect:"follow"},{retries:3,baseDelayMs:750,signal:b.signal}),f=l.status;if(f!==200&&f!==206)return e.text("Upstream error",f||502);const U=l.headers.get("content-type")||"application/octet-stream",d=new Headers;if(d.set("Content-Type",U),W(d,c=>l.headers.get(c)),d.set("Connection","keep-alive"),r.includes(".m3u8")||/mpegurl|m3u8/i.test(U)){const c=await l.text(),y=new R(l.url||r),q=m(c,y),a=Buffer.from(q,"utf8");return d.set("Content-Type","application/vnd.apple.mpegurl"),d.set("Content-Length",String(a.length)),new Response(a,{status:f,headers:d})}const w=l.body?.getReader();if(!w)return x.warn({host:v.url?.hostname},"tv stream-proxy: upstream sent no body"),e.text("Upstream sent no body",502);try{const c=await w.read(),y=c.done?Buffer.alloc(0):Buffer.from(c.value);if(N(y)){const a=[y];for(;;){const{done:M,value:k}=await w.read();if(M)break;a.push(Buffer.from(k))}const I=Buffer.concat(a),T=new R(l.url||r),L=m(I.toString("utf8"),T),C=Buffer.from(L,"utf8");return d.set("Content-Type","application/vnd.apple.mpegurl"),d.set("Content-Length",String(C.length)),new Response(C,{status:f,headers:d})}const q=new ReadableStream({start(a){y.length&&a.enqueue(y)},async pull(a){const{done:I,value:T}=await w.read();I?a.close():a.enqueue(T)},cancel(a){return w.cancel(a)}});return new Response(q,{status:f,headers:d})}catch(c){return x.warn({err:c,host:v.url?.hostname},"tv stream-proxy: upstream body stream failed"),e.text("Upstream stream failed",502)}}catch(g){return b.signal.aborted?new Response(null,{status:499}):(x.warn({err:g,host:v.url?.hostname},"tv stream-proxy: upstream fetch failed"),e.text("Upstream fetch failed",502))}finally{clearTimeout(B)}})}export{V as registerTv};
@@ -1,341 +1,5 @@
1
- import logger from '../logger.js';
2
- import { WatchlistRepository } from '../repositories/watchlist.repository.js';
3
- import { WatchedEpisodesRepository } from '../repositories/watched-episodes.repository.js';
4
- import { ShowsMetaRepository } from '../repositories/shows-meta.repository.js';
5
- import { NotificationsRepository } from '../repositories/notifications.repository.js';
6
- import { dbAll } from '../utils/db-utils.js';
7
- import { AppCache } from '../utils/cache.utils.js';
8
- import { getAiredEpisodesForShows, isAnilistRateLimited, batchGetShowStatuses, } from '../lib/anilist.js';
9
- import { parseJsonBody } from '../utils/http.utils.js';
10
- import { pickBestMatch } from '../providers/title-matching.js';
11
- const BACKGROUND_DISCOVERY_INTERVAL_MS = 5 * 60 * 1000;
12
- const NUDGE_THROTTLE_MS = 120 * 1000;
13
- const SLOW_MAX_RUN_MS = 5 * 60 * 1000;
14
- let discoveryIntervalId = null;
15
- let lastExternalDiscoveryAt = 0;
16
- let discoveryBusy = false;
17
- let lastDiscoveryRunAt = 0;
18
- let discoveryState = 'idle';
19
- let discoveryTotal = 0;
20
- let discoveryDone = 0;
21
- let discoveryStopped = false;
22
- let triggerImpl = null;
23
- export function triggerWatchlistDiscovery() {
24
- return triggerImpl?.() ?? false;
25
- }
26
- export function stopWatchlistDiscovery() {
27
- discoveryStopped = true;
28
- if (discoveryIntervalId !== null) {
29
- clearInterval(discoveryIntervalId);
30
- discoveryIntervalId = null;
31
- }
32
- }
33
- export function getWatchlistDiscoveryStatus() {
34
- return {
35
- running: discoveryBusy,
36
- state: discoveryState,
37
- total: discoveryTotal,
38
- done: Math.min(discoveryDone, discoveryTotal),
39
- lastRunAt: lastDiscoveryRunAt,
40
- };
41
- }
42
- export function startWatchlistDiscovery(getDb) {
43
- const anilistIdCache = new Map();
44
- const getAnilistId = async (showId) => {
45
- if (anilistIdCache.has(showId)) {
46
- return anilistIdCache.get(showId) || null;
47
- }
48
- const db = getDb();
49
- const meta = (await ShowsMetaRepository.getById(db, showId));
50
- if (meta?.anilistId) {
51
- anilistIdCache.set(showId, meta.anilistId);
52
- return meta.anilistId;
53
- }
54
- if (/^\d+$/.test(showId)) {
55
- const numericId = parseInt(showId);
56
- anilistIdCache.set(showId, numericId);
57
- return numericId;
58
- }
59
- anilistIdCache.set(showId, null);
60
- return null;
61
- };
62
- const runDiscovery = async (fast = false) => {
63
- if (discoveryBusy || discoveryStopped)
64
- return;
65
- discoveryBusy = true;
66
- discoveryState = 'running';
67
- discoveryTotal = 0;
68
- discoveryDone = 0;
69
- const db = getDb();
70
- if (!db || db.isClosedCheck()) {
71
- discoveryBusy = false;
72
- return;
73
- }
74
- const startedAt = Date.now();
75
- const MAX_RUN_MS = fast ? 90000 : SLOW_MAX_RUN_MS;
76
- try {
77
- const watchingShows = await WatchlistRepository.getWatchingShows(db);
78
- discoveryTotal = watchingShows.length;
79
- if (watchingShows.length === 0) {
80
- discoveryBusy = false;
81
- return;
82
- }
83
- const showIdMap = new Map();
84
- const MAP_CONCURRENCY = fast ? 4 : 2;
85
- const anilistResults = [];
86
- let nextIndex = 0;
87
- const worker = async () => {
88
- while (nextIndex < watchingShows.length) {
89
- if (Date.now() - startedAt > MAX_RUN_MS)
90
- return;
91
- const show = watchingShows[nextIndex];
92
- nextIndex += 1;
93
- const id = await getAnilistId(show.id);
94
- discoveryDone += 1;
95
- anilistResults.push({ show, id });
96
- }
97
- };
98
- await Promise.all(Array.from({ length: Math.min(MAP_CONCURRENCY, watchingShows.length) }, () => worker()));
99
- for (const { show, id } of anilistResults) {
100
- if (id) {
101
- showIdMap.set(show.id, id);
102
- }
103
- }
104
- if (Date.now() - startedAt > MAX_RUN_MS || showIdMap.size === 0) {
105
- discoveryBusy = false;
106
- return;
107
- }
108
- const now = new Date();
109
- const weekStart = new Date(now);
110
- weekStart.setDate(now.getDate() - 7);
111
- weekStart.setHours(0, 0, 0, 0);
112
- const weekEnd = new Date(now);
113
- weekEnd.setHours(23, 59, 59, 999);
114
- const schedules = await getAiredEpisodesForShows(Array.from(showIdMap.values()), weekStart, weekEnd);
115
- const nowUnix = Math.floor(Date.now() / 1000);
116
- const reverseMap = new Map();
117
- for (const [watchlistId, anilistId] of showIdMap.entries()) {
118
- reverseMap.set(anilistId, watchlistId);
119
- }
120
- const finishedShowIds = new Set();
121
- const unresolvedStatus = [];
122
- for (const [watchlistId, anilistId] of showIdMap.entries()) {
123
- const localMeta = (await ShowsMetaRepository.getById(db, watchlistId));
124
- if (localMeta?.status === 'FINISHED') {
125
- finishedShowIds.add(anilistId);
126
- }
127
- else if (!localMeta?.status) {
128
- unresolvedStatus.push({ watchlistId, anilistId });
129
- }
130
- }
131
- if (unresolvedStatus.length > 0 && !isAnilistRateLimited()) {
132
- const statuses = await batchGetShowStatuses(unresolvedStatus.map((s) => s.anilistId));
133
- let persisted = 0;
134
- for (const { watchlistId, anilistId } of unresolvedStatus) {
135
- const status = statuses.get(anilistId);
136
- if (status === 'FINISHED') {
137
- finishedShowIds.add(anilistId);
138
- await ShowsMetaRepository.upsert(db, { id: watchlistId, status: 'FINISHED' });
139
- persisted++;
140
- }
141
- else if (status) {
142
- await ShowsMetaRepository.upsert(db, { id: watchlistId, status });
143
- persisted++;
144
- }
145
- }
146
- }
147
- if (finishedShowIds.size > 0) {
148
- const monthStart = new Date(now);
149
- monthStart.setDate(now.getDate() - 30);
150
- monthStart.setHours(0, 0, 0, 0);
151
- const monthEnd = new Date(now);
152
- monthEnd.setHours(23, 59, 59, 999);
153
- const finishedSchedules = await getAiredEpisodesForShows(Array.from(finishedShowIds), monthStart, monthEnd);
154
- const finishedEpisodesByShow = new Map();
155
- const seenFinishedKeys = new Set();
156
- for (const entry of finishedSchedules) {
157
- if (entry.airingAt > nowUnix)
158
- continue;
159
- const watchlistId = reverseMap.get(entry.mediaId);
160
- if (!watchlistId)
161
- continue;
162
- const episodeKey = String(Math.round(entry.episode));
163
- const dedupeKey = `${watchlistId}:${episodeKey}`;
164
- if (seenFinishedKeys.has(dedupeKey))
165
- continue;
166
- seenFinishedKeys.add(dedupeKey);
167
- const list = finishedEpisodesByShow.get(watchlistId);
168
- if (list)
169
- list.push({ episodeKey, airingAt: entry.airingAt });
170
- else
171
- finishedEpisodesByShow.set(watchlistId, [{ episodeKey, airingAt: entry.airingAt }]);
172
- }
173
- for (const [watchlistId, episodes] of finishedEpisodesByShow) {
174
- const [watchedEps, dismissedEps] = await Promise.all([
175
- WatchedEpisodesRepository.getWatchedEpisodeNumbers(db, watchlistId),
176
- NotificationsRepository.getDismissedByShow(db, watchlistId),
177
- ]);
178
- const watchedSet = new Set(watchedEps.map((e) => e.toString()));
179
- const dismissedSet = new Set(dismissedEps.map((e) => e.episodeNumber.toString()));
180
- for (const { episodeKey, airingAt } of episodes) {
181
- if (nowUnix - airingAt > 30 * 24 * 60 * 60)
182
- continue;
183
- if (!watchedSet.has(episodeKey) && !dismissedSet.has(episodeKey)) {
184
- await NotificationsRepository.addDiscovered(db, watchlistId, episodeKey);
185
- }
186
- }
187
- }
188
- }
189
- const episodesByShow = new Map();
190
- const seenEpisodeKeys = new Set();
191
- for (const entry of schedules) {
192
- if (entry.airingAt > nowUnix)
193
- continue;
194
- const watchlistId = reverseMap.get(entry.mediaId);
195
- if (!watchlistId)
196
- continue;
197
- const episodeKey = String(Math.round(entry.episode));
198
- const dedupeKey = `${watchlistId}:${episodeKey}`;
199
- if (seenEpisodeKeys.has(dedupeKey))
200
- continue;
201
- seenEpisodeKeys.add(dedupeKey);
202
- const list = episodesByShow.get(watchlistId);
203
- if (list)
204
- list.push({ episodeKey, airingAt: entry.airingAt });
205
- else
206
- episodesByShow.set(watchlistId, [{ episodeKey, airingAt: entry.airingAt }]);
207
- }
208
- for (const [watchlistId, episodes] of episodesByShow) {
209
- const [watchedEps, dismissedEps] = await Promise.all([
210
- WatchedEpisodesRepository.getWatchedEpisodeNumbers(db, watchlistId),
211
- NotificationsRepository.getDismissedByShow(db, watchlistId),
212
- ]);
213
- const watchedSet = new Set(watchedEps.map((e) => e.toString()));
214
- const dismissedSet = new Set(dismissedEps.map((e) => e.episodeNumber.toString()));
215
- for (const { episodeKey } of episodes) {
216
- if (!watchedSet.has(episodeKey) && !dismissedSet.has(episodeKey)) {
217
- await NotificationsRepository.addDiscovered(db, watchlistId, episodeKey);
218
- }
219
- }
220
- }
221
- await NotificationsRepository.cleanupWatchedNotifications(db);
222
- }
223
- catch (e) {
224
- discoveryState = 'error';
225
- if (e?.message === 'Database is closed') {
226
- logger.info('Notification discovery stopped: database is closed');
227
- }
228
- else {
229
- logger.error({ err: e }, 'AniList notification discovery failed');
230
- }
231
- }
232
- finally {
233
- discoveryBusy = false;
234
- lastDiscoveryRunAt = Date.now();
235
- if (discoveryState === 'running') {
236
- discoveryState = discoveryTotal === 0 ? 'empty' : 'complete';
237
- }
238
- }
239
- };
240
- triggerImpl = () => {
241
- if (discoveryStopped || discoveryBusy)
242
- return false;
243
- if (Date.now() - lastExternalDiscoveryAt < NUDGE_THROTTLE_MS)
244
- return false;
245
- lastExternalDiscoveryAt = Date.now();
246
- runDiscovery(true);
247
- return true;
248
- };
249
- discoveryIntervalId = setInterval(() => {
250
- if (!discoveryStopped)
251
- runDiscovery(false);
252
- }, BACKGROUND_DISCOVERY_INTERVAL_MS);
253
- }
254
- export async function getAdultNonWatchlistShowIds(db) {
255
- const rows = await dbAll(db, `SELECT DISTINCT we.showId as showId
1
+ import H from"../logger.js";import{WatchlistRepository as et}from"../repositories/watchlist.repository.js";import{WatchedEpisodesRepository as j}from"../repositories/watched-episodes.repository.js";import{ShowsMetaRepository as k}from"../repositories/shows-meta.repository.js";import{NotificationsRepository as b}from"../repositories/notifications.repository.js";import{dbAll as st}from"../utils/db-utils.js";import{AppCache as F}from"../utils/cache.utils.js";import{getAiredEpisodesForShows as J,isAnilistRateLimited as nt,batchGetShowStatuses as it}from"../lib/anilist.js";import{parseJsonBody as z}from"../utils/http.utils.js";import{pickBestMatch as ot}from"../providers/title-matching.js";const rt=300*1e3,at=120*1e3,ct=300*1e3;let K=null,G=0,y=!1,V=0,N="idle",R=0,U=0,P=!1,X=null;function St(){return X?.()??!1}function It(){P=!0,K!==null&&(clearInterval(K),K=null)}function At(){return{running:y,state:N,total:R,done:Math.min(U,R),lastRunAt:V}}function Dt(g){const n=new Map,w=async o=>{if(n.has(o))return n.get(o)||null;const s=g(),l=await k.getById(s,o);if(l?.anilistId)return n.set(o,l.anilistId),l.anilistId;if(/^\d+$/.test(o)){const d=parseInt(o);return n.set(o,d),d}return n.set(o,null),null},h=async(o=!1)=>{if(y||P)return;y=!0,N="running",R=0,U=0;const s=g();if(!s||s.isClosedCheck()){y=!1;return}const l=Date.now(),d=o?9e4:ct;try{const i=await et.getWatchingShows(s);if(R=i.length,i.length===0){y=!1;return}const r=new Map,E=o?4:2,D=[];let S=0;const M=async()=>{for(;S<i.length;){if(Date.now()-l>d)return;const t=i[S];S+=1;const e=await w(t.id);U+=1,D.push({show:t,id:e})}};await Promise.all(Array.from({length:Math.min(E,i.length)},()=>M()));for(const{show:t,id:e}of D)e&&r.set(t.id,e);if(Date.now()-l>d||r.size===0){y=!1;return}const m=new Date,A=new Date(m);A.setDate(m.getDate()-7),A.setHours(0,0,0,0);const $=new Date(m);$.setHours(23,59,59,999);const Q=await J(Array.from(r.values()),A,$),T=Math.floor(Date.now()/1e3),O=new Map;for(const[t,e]of r.entries())O.set(e,t);const x=new Set,_=[];for(const[t,e]of r.entries()){const a=await k.getById(s,t);a?.status==="FINISHED"?x.add(e):a?.status||_.push({watchlistId:t,anilistId:e})}if(_.length>0&&!nt()){const t=await it(_.map(a=>a.anilistId));let e=0;for(const{watchlistId:a,anilistId:u}of _){const f=t.get(u);f==="FINISHED"?(x.add(u),await k.upsert(s,{id:a,status:"FINISHED"}),e++):f&&(await k.upsert(s,{id:a,status:f}),e++)}}if(x.size>0){const t=new Date(m);t.setDate(m.getDate()-30),t.setHours(0,0,0,0);const e=new Date(m);e.setHours(23,59,59,999);const a=await J(Array.from(x),t,e),u=new Map,f=new Set;for(const p of a){if(p.airingAt>T)continue;const c=O.get(p.mediaId);if(!c)continue;const v=String(Math.round(p.episode)),C=`${c}:${v}`;if(f.has(C))continue;f.add(C);const W=u.get(c);W?W.push({episodeKey:v,airingAt:p.airingAt}):u.set(c,[{episodeKey:v,airingAt:p.airingAt}])}for(const[p,c]of u){const[v,C]=await Promise.all([j.getWatchedEpisodeNumbers(s,p),b.getDismissedByShow(s,p)]),W=new Set(v.map(I=>I.toString())),Z=new Set(C.map(I=>I.episodeNumber.toString()));for(const{episodeKey:I,airingAt:tt}of c)T-tt>720*60*60||!W.has(I)&&!Z.has(I)&&await b.addDiscovered(s,p,I)}}const L=new Map,B=new Set;for(const t of Q){if(t.airingAt>T)continue;const e=O.get(t.mediaId);if(!e)continue;const a=String(Math.round(t.episode)),u=`${e}:${a}`;if(B.has(u))continue;B.add(u);const f=L.get(e);f?f.push({episodeKey:a,airingAt:t.airingAt}):L.set(e,[{episodeKey:a,airingAt:t.airingAt}])}for(const[t,e]of L){const[a,u]=await Promise.all([j.getWatchedEpisodeNumbers(s,t),b.getDismissedByShow(s,t)]),f=new Set(a.map(c=>c.toString())),p=new Set(u.map(c=>c.episodeNumber.toString()));for(const{episodeKey:c}of e)!f.has(c)&&!p.has(c)&&await b.addDiscovered(s,t,c)}await b.cleanupWatchedNotifications(s)}catch(i){N="error",i?.message==="Database is closed"?H.info("Notification discovery stopped: database is closed"):H.error({err:i},"AniList notification discovery failed")}finally{y=!1,V=Date.now(),N==="running"&&(N=R===0?"empty":"complete")}};X=()=>P||y||Date.now()-G<at?!1:(G=Date.now(),h(!0),!0),K=setInterval(()=>{P||h(!1)},rt)}async function vt(g){return(await st(g,`SELECT DISTINCT we.showId as showId
256
2
  FROM watched_episodes we
257
3
  JOIN shows_meta sm ON sm.id = we.showId AND sm.isAdult = 1
258
4
  LEFT JOIN watchlist w ON w.id = we.showId
259
- WHERE w.id IS NULL`);
260
- return rows.map((r) => r.showId);
261
- }
262
- const dlsitePosterCache = new AppCache({ ttlSeconds: 3600, maxKeys: 2000 });
263
- const mangadexCoverCache = new AppCache({ ttlSeconds: 3600, maxKeys: 2000 });
264
- export async function getMangadexCover(title) {
265
- const key = title.trim().toLowerCase();
266
- if (!key)
267
- return null;
268
- const cached = mangadexCoverCache.get(key);
269
- if (cached !== undefined)
270
- return cached;
271
- try {
272
- const params = new URLSearchParams({
273
- title: title.trim(),
274
- limit: '5',
275
- 'includes[]': 'cover_art',
276
- 'order[relevance]': 'desc',
277
- });
278
- for (const r of ['safe', 'suggestive', 'erotica', 'pornographic']) {
279
- params.append('contentRating[]', r);
280
- }
281
- const res = await fetch(`https://api.mangadex.org/manga?${params.toString()}`, {
282
- headers: { 'User-Agent': 'Dango/3.0', Accept: 'application/json' },
283
- signal: AbortSignal.timeout(10000),
284
- });
285
- if (!res.ok)
286
- return null;
287
- const data = (await parseJsonBody(res));
288
- const candidates = [];
289
- for (const m of data.data || []) {
290
- const fileName = m.relationships?.find((r) => r.type === 'cover_art')?.attributes?.fileName;
291
- if (!fileName)
292
- continue;
293
- const cover = `https://uploads.mangadex.org/covers/${m.id}/${fileName}.256.jpg`;
294
- const main = m.attributes?.title;
295
- const mainTitle = main?.en || main?.['ja-ro'] || Object.values(main || {})[0];
296
- if (mainTitle)
297
- candidates.push({ title: mainTitle, cover });
298
- for (const alt of m.attributes?.altTitles || []) {
299
- const t = Object.values(alt)[0];
300
- if (t)
301
- candidates.push({ title: t, cover });
302
- }
303
- }
304
- const match = pickBestMatch(candidates, [title]);
305
- const url = match ? match.item.cover : null;
306
- mangadexCoverCache.set(key, url);
307
- return url;
308
- }
309
- catch {
310
- return null;
311
- }
312
- }
313
- export async function getDlsitePoster(rjCode) {
314
- const key = String(rjCode).trim().toUpperCase();
315
- if (!/^RJ\d{5,}$/.test(key))
316
- return null;
317
- const cached = dlsitePosterCache.get(key);
318
- if (cached !== undefined)
319
- return cached;
320
- try {
321
- const res = await fetch(`https://www.dlsite.com/maniax/product/info/ajax?product_id=${key}`, {
322
- headers: { 'User-Agent': 'Mozilla/5.0', Accept: 'application/json' },
323
- signal: AbortSignal.timeout(5000),
324
- });
325
- if (!res.ok)
326
- return null;
327
- const raw = (await parseJsonBody(res));
328
- const data = raw;
329
- const entryRaw = (data[key] ?? data);
330
- const entry = entryRaw;
331
- const img = typeof entry?.work_image === 'string' ? entry.work_image : undefined;
332
- if (!img)
333
- return null;
334
- const url = img.startsWith('//') ? `https:${img}` : img;
335
- dlsitePosterCache.set(key, url);
336
- return url;
337
- }
338
- catch {
339
- return null;
340
- }
341
- }
5
+ WHERE w.id IS NULL`)).map(w=>w.showId)}const Y=new F({ttlSeconds:3600,maxKeys:2e3}),q=new F({ttlSeconds:3600,maxKeys:2e3});async function bt(g){const n=g.trim().toLowerCase();if(!n)return null;const w=q.get(n);if(w!==void 0)return w;try{const h=new URLSearchParams({title:g.trim(),limit:"5","includes[]":"cover_art","order[relevance]":"desc"});for(const r of["safe","suggestive","erotica","pornographic"])h.append("contentRating[]",r);const o=await fetch(`https://api.mangadex.org/manga?${h.toString()}`,{headers:{"User-Agent":"Dango/3.0",Accept:"application/json"},signal:AbortSignal.timeout(1e4)});if(!o.ok)return null;const s=await z(o),l=[];for(const r of s.data||[]){const E=r.relationships?.find(m=>m.type==="cover_art")?.attributes?.fileName;if(!E)continue;const D=`https://uploads.mangadex.org/covers/${r.id}/${E}.256.jpg`,S=r.attributes?.title,M=S?.en||S?.["ja-ro"]||Object.values(S||{})[0];M&&l.push({title:M,cover:D});for(const m of r.attributes?.altTitles||[]){const A=Object.values(m)[0];A&&l.push({title:A,cover:D})}}const d=ot(l,[g]),i=d?d.item.cover:null;return q.set(n,i),i}catch{return null}}async function Nt(g){const n=String(g).trim().toUpperCase();if(!/^RJ\d{5,}$/.test(n))return null;const w=Y.get(n);if(w!==void 0)return w;try{const h=await fetch(`https://www.dlsite.com/maniax/product/info/ajax?product_id=${n}`,{headers:{"User-Agent":"Mozilla/5.0",Accept:"application/json"},signal:AbortSignal.timeout(5e3)});if(!h.ok)return null;const s=await z(h),d=s[n]??s,i=typeof d?.work_image=="string"?d.work_image:void 0;if(!i)return null;const r=i.startsWith("//")?`https:${i}`:i;return Y.set(n,r),r}catch{return null}}export{vt as getAdultNonWatchlistShowIds,Nt as getDlsitePoster,bt as getMangadexCover,At as getWatchlistDiscoveryStatus,Dt as startWatchlistDiscovery,It as stopWatchlistDiscovery,St as triggerWatchlistDiscovery};