@serifpersia/dango 3.6.7 → 3.6.9

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 (156) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
  11. package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
  18. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
  19. package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
  20. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
  21. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
  22. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
  23. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
  24. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
  25. package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
  26. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
  27. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
  28. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
  29. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
  30. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
  31. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
  32. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
  33. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
  34. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
  35. package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
  36. package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
  37. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
  38. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
  39. package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
  40. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
  41. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
  42. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
  43. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
  44. package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
  45. package/client/dist/index.html +1 -1
  46. package/package.json +3 -5
  47. package/server/dist/config.js +1 -174
  48. package/server/dist/server.js +1068 -289
  49. package/server/dist/shipped-defaults.js +1 -6
  50. package/server/package.json +4 -5
  51. package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
  52. package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
  53. package/client/dist/assets/Music-BGH_0voi.css +0 -1
  54. package/client/dist/assets/Music-CQpyXTLH.js +0 -1
  55. package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
  56. package/client/dist/assets/Radio-DF6g6odd.js +0 -1
  57. package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
  58. package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
  59. package/server/dist/app-auth.js +0 -157
  60. package/server/dist/app-hono.js +0 -598
  61. package/server/dist/db/drizzle.js +0 -16
  62. package/server/dist/db/schema-anime.js +0 -61
  63. package/server/dist/db/schema-asmr.js +0 -13
  64. package/server/dist/db/schema-manga.js +0 -31
  65. package/server/dist/db/schema-tv.js +0 -18
  66. package/server/dist/db.js +0 -217
  67. package/server/dist/discord-gateway.js +0 -385
  68. package/server/dist/discord-rpc.js +0 -413
  69. package/server/dist/github-sync.js +0 -508
  70. package/server/dist/google.js +0 -741
  71. package/server/dist/hono/asmr.js +0 -10
  72. package/server/dist/hono/auth.js +0 -49
  73. package/server/dist/hono/music.js +0 -209
  74. package/server/dist/hono/proxy.js +0 -827
  75. package/server/dist/hono/tv.js +0 -200
  76. package/server/dist/hono/watchlist.js +0 -341
  77. package/server/dist/lib/anilist.js +0 -1583
  78. package/server/dist/lib/anischedule.js +0 -191
  79. package/server/dist/lib/insights.js +0 -185
  80. package/server/dist/lib/ipc.js +0 -23
  81. package/server/dist/lib/kitsu.js +0 -517
  82. package/server/dist/lib/mal-import.js +0 -291
  83. package/server/dist/lib/mal.js +0 -957
  84. package/server/dist/lib/migration.js +0 -310
  85. package/server/dist/lib/offline-db.js +0 -395
  86. package/server/dist/lib/potoken.js +0 -135
  87. package/server/dist/lib/recommendations.service.js +0 -216
  88. package/server/dist/lib/temp-ids.js +0 -3
  89. package/server/dist/lib/tmdb.js +0 -72
  90. package/server/dist/lib/tracker/anilist-tracker.js +0 -308
  91. package/server/dist/lib/tracker/manga-sync.service.js +0 -773
  92. package/server/dist/lib/tracker/sync-state.js +0 -21
  93. package/server/dist/lib/tracker/sync.service.js +0 -476
  94. package/server/dist/lib/translate.js +0 -79
  95. package/server/dist/lib/ump.js +0 -106
  96. package/server/dist/lib/ytmusic.js +0 -162
  97. package/server/dist/logger.js +0 -16
  98. package/server/dist/providers/manga/manga.types.js +0 -1
  99. package/server/dist/providers/provider.interface.js +0 -1
  100. package/server/dist/providers/remote-loader.js +0 -614
  101. package/server/dist/providers/remote-types.js +0 -159
  102. package/server/dist/providers/title-matching.js +0 -226
  103. package/server/dist/providers/tv.types.js +0 -1
  104. package/server/dist/rclone.js +0 -120
  105. package/server/dist/repositories/asmr.repository.js +0 -77
  106. package/server/dist/repositories/insights.repository.js +0 -106
  107. package/server/dist/repositories/library.repository.js +0 -94
  108. package/server/dist/repositories/mal-cache.repository.js +0 -52
  109. package/server/dist/repositories/manga.repository.js +0 -95
  110. package/server/dist/repositories/notifications.repository.js +0 -60
  111. package/server/dist/repositories/queue.repository.js +0 -72
  112. package/server/dist/repositories/settings.repository.js +0 -30
  113. package/server/dist/repositories/shows-meta.repository.js +0 -58
  114. package/server/dist/repositories/temp-show-ids.repository.js +0 -63
  115. package/server/dist/repositories/tv.repository.js +0 -93
  116. package/server/dist/repositories/watched-episodes.repository.js +0 -82
  117. package/server/dist/repositories/watchlist.repository.js +0 -60
  118. package/server/dist/scripts/backfill-episode-duration.js +0 -42
  119. package/server/dist/sync-payload.js +0 -91
  120. package/server/dist/sync.js +0 -657
  121. package/server/dist/trpc/context.js +0 -7
  122. package/server/dist/trpc/errors.js +0 -15
  123. package/server/dist/trpc/index.js +0 -18
  124. package/server/dist/trpc/router.js +0 -49
  125. package/server/dist/trpc/routers/asmr-library.js +0 -112
  126. package/server/dist/trpc/routers/asmr-progress.js +0 -121
  127. package/server/dist/trpc/routers/asmr.js +0 -87
  128. package/server/dist/trpc/routers/auth.js +0 -194
  129. package/server/dist/trpc/routers/continue-watching.js +0 -81
  130. package/server/dist/trpc/routers/data.js +0 -1026
  131. package/server/dist/trpc/routers/discord.js +0 -285
  132. package/server/dist/trpc/routers/discovery.js +0 -11
  133. package/server/dist/trpc/routers/insights.js +0 -14
  134. package/server/dist/trpc/routers/manga-library.js +0 -263
  135. package/server/dist/trpc/routers/manga-progress.js +0 -169
  136. package/server/dist/trpc/routers/manga.js +0 -201
  137. package/server/dist/trpc/routers/music.js +0 -655
  138. package/server/dist/trpc/routers/notifications.js +0 -80
  139. package/server/dist/trpc/routers/progress.js +0 -109
  140. package/server/dist/trpc/routers/queue.js +0 -93
  141. package/server/dist/trpc/routers/radio.js +0 -145
  142. package/server/dist/trpc/routers/settings.js +0 -96
  143. package/server/dist/trpc/routers/tracker.js +0 -213
  144. package/server/dist/trpc/routers/tv-library.js +0 -135
  145. package/server/dist/trpc/routers/tv-progress.js +0 -159
  146. package/server/dist/trpc/routers/tv.js +0 -551
  147. package/server/dist/trpc/routers/watchlist.js +0 -550
  148. package/server/dist/trpc/validation.js +0 -258
  149. package/server/dist/utils/cache.utils.js +0 -25
  150. package/server/dist/utils/cookie.utils.js +0 -13
  151. package/server/dist/utils/db-utils.js +0 -17
  152. package/server/dist/utils/env.utils.js +0 -77
  153. package/server/dist/utils/http.utils.js +0 -82
  154. package/server/dist/utils/request-context.js +0 -2
  155. package/server/dist/utils/security.utils.js +0 -76
  156. /package/client/dist/assets/{Asmr-D1KuQwQy.css → useAsmrPopup-D1KuQwQy.css} +0 -0
@@ -1,655 +0,0 @@
1
- import { TRPCError } from '@trpc/server';
2
- import { protectedProcedure, router } from '../index.js';
3
- import { badRequest, defineSchema, failed, optStr, reqObj } from '../validation.js';
4
- import logger from '../../logger.js';
5
- import { getInnertube, getAuthedInnertube, refreshAuthedInnertube, saveMusicCookie, getMusicAuthStatus, signOutMusic, isParserVariantError, verifyMusicSession, } from '../../lib/ytmusic.js';
6
- const NAME_SUFFIXES = [
7
- /\s*(- topic)$/i,
8
- /\s*vevo$/i,
9
- /\s*[(|[]official(.*?)[)|\]]/i,
10
- /\s*[(|[]((lyrics?|visualizer|audio)\s*(video)?)[)|\]]/i,
11
- /\s*[(|[](performance video)[)|\]]/i,
12
- /\s*[(|[](clip official)[)|\]]/i,
13
- /\s*[(|[](video version)[)|\]]/i,
14
- /\s*[(|[](HD|HQ)\s*?(?:audio)?[)|\]]$/i,
15
- /\s*[(|[](live)[)|\]]$/i,
16
- /\s*[(|[]4K\s*?(?:upgrade)?[)|\]]$/i,
17
- ];
18
- function cleanupMusicName(name) {
19
- if (!name)
20
- return name;
21
- let out = name;
22
- for (const re of NAME_SUFFIXES)
23
- out = out.replace(re, '');
24
- return out.trim();
25
- }
26
- function toTrack(node) {
27
- const n = node;
28
- const id = n['video_id'] || n['videoId'] || n['id'] || '';
29
- if (!id || typeof id !== 'string')
30
- return null;
31
- const titleObj = n['title'];
32
- const title = typeof titleObj === 'string' ? titleObj : (titleObj?.text ?? 'Unknown title');
33
- const artistsRaw = n['artists'] ?? n['authors'] ?? n['subtitle'];
34
- let artists = '';
35
- if (Array.isArray(artistsRaw)) {
36
- artists = artistsRaw
37
- .map((a) => {
38
- const item = a;
39
- return item.name || item.text || item.title || '';
40
- })
41
- .filter(Boolean)
42
- .join(', ');
43
- }
44
- else if (typeof artistsRaw === 'string') {
45
- artists = artistsRaw;
46
- }
47
- else if (artistsRaw && typeof artistsRaw === 'object') {
48
- artists = artistsRaw.text ?? '';
49
- }
50
- const thumbsRaw = (n['thumbnails'] ?? n['thumbnail']);
51
- const thumbnails = Array.isArray(thumbsRaw)
52
- ? thumbsRaw
53
- : Array.isArray(thumbsRaw?.thumbnails)
54
- ? thumbsRaw.thumbnails
55
- : [];
56
- const albumText = textOf(n['album']);
57
- const durationText = textOf(n['duration']);
58
- return {
59
- id,
60
- title: cleanupMusicName(String(title)),
61
- artists: cleanupMusicName(artists),
62
- album: albumText ? cleanupMusicName(albumText) : undefined,
63
- duration: durationText || undefined,
64
- thumbnails,
65
- };
66
- }
67
- function textOf(value) {
68
- if (!value)
69
- return '';
70
- if (typeof value === 'string')
71
- return value;
72
- const obj = value;
73
- if (typeof obj.text === 'string')
74
- return obj.text;
75
- if (Array.isArray(obj.runs))
76
- return obj.runs.map((r) => r.text || '').join('');
77
- return '';
78
- }
79
- function thumbsOf(value) {
80
- if (!value)
81
- return [];
82
- if (Array.isArray(value)) {
83
- return value
84
- .filter((t) => t && typeof t.url === 'string')
85
- .map((t) => ({ url: t.url, width: t.width, height: t.height }));
86
- }
87
- const obj = value;
88
- if (Array.isArray(obj.thumbnails))
89
- return thumbsOf(obj.thumbnails);
90
- return [];
91
- }
92
- function nodeType(node) {
93
- const n = node;
94
- if (typeof n?.type === 'string')
95
- return n.type;
96
- return n?.constructor?.name ?? '';
97
- }
98
- function upnextToTrack(node, trustLike = false) {
99
- const kind = nodeType(node);
100
- if (kind === 'AutomixPreviewVideo')
101
- return null;
102
- const n = node;
103
- const video = kind === 'PlaylistPanelVideoWrapper'
104
- ? (n['primary'] ?? null)
105
- : n;
106
- if (!video || typeof video !== 'object')
107
- return null;
108
- const id = video['video_id'];
109
- if (typeof id !== 'string' || !id)
110
- return null;
111
- const artistsRaw = video['artists'];
112
- let artists = '';
113
- if (Array.isArray(artistsRaw)) {
114
- artists = artistsRaw
115
- .map((a) => a?.name || '')
116
- .filter(Boolean)
117
- .join(', ');
118
- }
119
- if (!artists && typeof video['author'] === 'string')
120
- artists = video['author'];
121
- const albumRaw = video['album'];
122
- const durationRaw = video['duration'];
123
- const durationText = typeof durationRaw === 'string' ? durationRaw : (durationRaw?.text ?? '');
124
- let liked = null;
125
- if (trustLike) {
126
- for (const menu of [video['menu'], node['menu']]) {
127
- let found = null;
128
- for (const entry of asMenuItems(menu)) {
129
- if (nodeType(entry) !== 'ToggleMenuServiceItem')
130
- continue;
131
- const icon = entry['icon_type'];
132
- if (icon !== 'FAVORITE' && icon !== 'UNFAVORITE')
133
- continue;
134
- found = icon === 'UNFAVORITE';
135
- break;
136
- }
137
- if (found !== null) {
138
- liked = found;
139
- break;
140
- }
141
- }
142
- }
143
- return {
144
- id,
145
- title: cleanupMusicName(textOf(video['title']) || 'Unknown title'),
146
- artists: cleanupMusicName(artists || 'Unknown artist'),
147
- album: albumRaw?.name ? cleanupMusicName(albumRaw.name) : undefined,
148
- duration: durationText || undefined,
149
- thumbnails: thumbsOf(video['thumbnail'] ?? video['thumbnails']),
150
- ...(liked !== null ? { liked } : {}),
151
- };
152
- }
153
- function isSignInPrompt(endpoint) {
154
- return !!endpoint?.payload && 'signInEndpoint' in endpoint.payload;
155
- }
156
- function parseUpnext(panel, trustLike) {
157
- const tracks = [];
158
- const shaped = panel;
159
- const contents = shaped.contents ?? [];
160
- for (const item of contents) {
161
- const track = upnextToTrack(item, trustLike);
162
- if (track)
163
- tracks.push(track);
164
- if (tracks.length >= 50)
165
- break;
166
- }
167
- const playlistId = shaped.playlist_id ?? null;
168
- return { tracks, playlistId };
169
- }
170
- function asMenuItems(menu) {
171
- if (!menu || typeof menu !== 'object')
172
- return [];
173
- const items = menu.items;
174
- return Array.isArray(items) ? items : [];
175
- }
176
- function findLikeToggle(item, videoId) {
177
- const result = { like: null, unlike: null };
178
- const kind = nodeType(item);
179
- const n = item;
180
- const video = kind === 'PlaylistPanelVideoWrapper'
181
- ? (n['primary'] ?? null)
182
- : n;
183
- if (!video || video['video_id'] !== videoId)
184
- return result;
185
- const menus = [video['menu'], n['menu']];
186
- for (const menu of menus) {
187
- for (const entry of asMenuItems(menu)) {
188
- if (nodeType(entry) !== 'ToggleMenuServiceItem')
189
- continue;
190
- const icon = entry['icon_type'];
191
- if (icon !== 'FAVORITE' && icon !== 'UNFAVORITE')
192
- continue;
193
- for (const key of ['default_endpoint', 'toggled_endpoint']) {
194
- const ep = entry[key];
195
- if (typeof ep?.call !== 'function')
196
- continue;
197
- const status = ep.payload?.status;
198
- if (status === 'LIKE' && !result.like)
199
- result.like = ep;
200
- else if (status === 'INDIFFERENT' && !result.unlike)
201
- result.unlike = ep;
202
- }
203
- if (result.like && result.unlike)
204
- return result;
205
- }
206
- }
207
- return result;
208
- }
209
- async function fetchPlaylistTracks(yt, id, cap = 500) {
210
- const tracks = [];
211
- let page = (await yt.music.getPlaylist(id));
212
- for (let fetches = 0; fetches < 6; fetches++) {
213
- for (const item of (page.items ?? page.contents ?? []).slice(0, cap - tracks.length)) {
214
- const track = toTrack(item);
215
- if (track)
216
- tracks.push(track);
217
- if (tracks.length >= cap)
218
- break;
219
- }
220
- if (tracks.length >= cap)
221
- break;
222
- if (!page.has_continuation || typeof page.getContinuation !== 'function')
223
- break;
224
- try {
225
- page = (await page.getContinuation());
226
- }
227
- catch (err) {
228
- logger.warn({ err }, '[music] playlist continuation failed');
229
- break;
230
- }
231
- }
232
- return tracks;
233
- }
234
- async function readLibrary(yt) {
235
- let lib;
236
- try {
237
- lib = (await yt.music.getLibrary());
238
- }
239
- catch (err) {
240
- if (!isParserVariantError(err))
241
- throw err;
242
- logger.warn('[music] library landing hit a layout variant, falling back to Liked playlist');
243
- try {
244
- const tracks = await fetchPlaylistTracks(yt, 'LM');
245
- return { tracks, playlists: [] };
246
- }
247
- catch (fallbackErr) {
248
- logger.warn({ err: fallbackErr }, '[music] liked playlist fallback failed');
249
- return { tracks: [], playlists: [] };
250
- }
251
- }
252
- const gridItems = [];
253
- for (const section of lib.contents ?? []) {
254
- const items = section.items ?? section.contents ?? [];
255
- gridItems.push(...items);
256
- }
257
- const playlists = [];
258
- const seen = new Set();
259
- for (const item of gridItems) {
260
- const n = item;
261
- const id = (typeof n['playlist_id'] === 'string' && n['playlist_id']) ||
262
- (typeof n['playlistId'] === 'string' && n['playlistId']) ||
263
- (typeof n['id'] === 'string' && n['id']) ||
264
- '';
265
- const title = textOf(n['title']);
266
- const subtitle = textOf(n['subtitle']);
267
- if (!id || !title || seen.has(id))
268
- continue;
269
- if (!/playlist|mix/i.test(subtitle) && title !== 'Liked Music')
270
- continue;
271
- seen.add(id);
272
- playlists.push({
273
- id,
274
- title,
275
- subtitle: subtitle || undefined,
276
- thumbnails: thumbsOf(n['thumbnails'] ?? n['thumbnail']),
277
- });
278
- }
279
- const tracks = [];
280
- const liked = playlists.find((p) => p.title === 'Liked Music');
281
- if (liked) {
282
- try {
283
- tracks.push(...(await fetchPlaylistTracks(yt, liked.id)));
284
- }
285
- catch (err) {
286
- logger.warn({ err }, '[music] liked playlist fetch failed');
287
- }
288
- }
289
- logger.info(`[music] library parsed tracks=${tracks.length} playlists=${playlists.length} gridItems=${gridItems.length}`);
290
- return { tracks, playlists };
291
- }
292
- function unauthorized(message) {
293
- return new TRPCError({ code: 'UNAUTHORIZED', message });
294
- }
295
- const musicSearchInput = () => defineSchema((value) => {
296
- const obj = reqObj(value);
297
- const out = {};
298
- const q = optStr(obj, 'q');
299
- if (q !== undefined)
300
- out.q = q;
301
- return out;
302
- });
303
- const musicTrackInput = () => defineSchema((value) => {
304
- const obj = reqObj(value);
305
- const out = {};
306
- const id = optStr(obj, 'id');
307
- if (id !== undefined)
308
- out.id = id;
309
- return out;
310
- });
311
- const musicAuthStartInput = () => defineSchema((value) => {
312
- const obj = reqObj(value);
313
- const out = {};
314
- const cookie = optStr(obj, 'cookie');
315
- if (cookie !== undefined)
316
- out.cookie = cookie;
317
- return out;
318
- });
319
- const musicLikeInput = () => defineSchema((value) => {
320
- const obj = reqObj(value);
321
- const out = {};
322
- const id = optStr(obj, 'id');
323
- if (id !== undefined)
324
- out.id = id;
325
- if (typeof obj.like === 'boolean' || typeof obj.like === 'string')
326
- out.like = obj.like;
327
- return out;
328
- });
329
- function requireId(input) {
330
- const id = String(input.id || '').trim();
331
- if (!id)
332
- throw badRequest('id is required');
333
- return id;
334
- }
335
- export const musicRouter = router({
336
- authStatus: protectedProcedure.query(() => {
337
- return getMusicAuthStatus();
338
- }),
339
- authStart: protectedProcedure.input(musicAuthStartInput()).mutation(async ({ input }) => {
340
- let cookie = String(input.cookie || '').trim();
341
- if (!cookie)
342
- throw badRequest('Cookie is required');
343
- cookie = cookie
344
- .replace(/^cookie\s*:\s*/i, '')
345
- .replace(/^["']+|["']+$/g, '')
346
- .trim();
347
- if (cookie.includes('…') || cookie.includes('...')) {
348
- throw badRequest(`Cookie looks truncated (${cookie.length} chars). In DevTools, right-click the Cookie header → Copy value (do not copy from the wrapped preview).`);
349
- }
350
- if (!cookie.includes('SID') && !cookie.includes('SAPISID')) {
351
- throw badRequest('That does not look like a YouTube cookie at all. Copy the full Cookie request header from a "browse" call on music.youtube.com in DevTools → Network.');
352
- }
353
- try {
354
- await saveMusicCookie(cookie);
355
- const yt = await getAuthedInnertube();
356
- if (yt) {
357
- try {
358
- const lib = await readLibrary(yt);
359
- return {
360
- success: true,
361
- tracks: lib.tracks.length,
362
- playlists: lib.playlists.length,
363
- };
364
- }
365
- catch {
366
- // ignore
367
- }
368
- }
369
- return { success: true, tracks: 0, playlists: 0 };
370
- }
371
- catch (err) {
372
- if (err instanceof TRPCError)
373
- throw err;
374
- logger.error({ err }, '[music] cookie sign-in failed');
375
- throw unauthorized('Cookie rejected by YouTube Music. Copy a fresh Cookie header from music.youtube.com.');
376
- }
377
- }),
378
- signOut: protectedProcedure.mutation(async () => {
379
- try {
380
- await signOutMusic();
381
- return { success: true };
382
- }
383
- catch (err) {
384
- logger.error({ err }, '[music] sign-out failed');
385
- throw failed('Sign-out failed');
386
- }
387
- }),
388
- search: protectedProcedure.input(musicSearchInput()).query(async ({ ctx, input }) => {
389
- const q = String(input.q || '').trim();
390
- if (!q)
391
- return { tracks: [] };
392
- const cacheKey = `music-search-${q.toLowerCase()}`;
393
- const cached = ctx.apiCache.get(cacheKey);
394
- if (cached)
395
- return { tracks: cached };
396
- try {
397
- const yt = await getInnertube();
398
- const result = await yt.music.search(q, { type: 'song' });
399
- const tracks = [];
400
- const contents = result.contents ?? [];
401
- for (const section of contents) {
402
- const items = section.contents ?? [section];
403
- for (const item of items) {
404
- const track = toTrack(item);
405
- if (track)
406
- tracks.push(track);
407
- if (tracks.length >= 25)
408
- break;
409
- }
410
- if (tracks.length >= 25)
411
- break;
412
- }
413
- ctx.apiCache.set(cacheKey, tracks, 300);
414
- return { tracks };
415
- }
416
- catch (err) {
417
- logger.error({ err }, '[music] search failed');
418
- return { tracks: [] };
419
- }
420
- }),
421
- track: protectedProcedure.input(musicTrackInput()).query(async ({ ctx, input }) => {
422
- const id = requireId(input);
423
- const cacheKey = `music-track-${id.toLowerCase()}`;
424
- const cached = ctx.apiCache.get(cacheKey);
425
- if (cached)
426
- return { track: cached };
427
- try {
428
- const yt = await getInnertube();
429
- const info = await yt.music.getInfo(id);
430
- const basic = info.basic_info;
431
- if (!basic?.title)
432
- return { track: null };
433
- const durationSec = typeof basic.duration === 'number' && Number.isFinite(basic.duration)
434
- ? Math.max(Math.round(basic.duration), 0)
435
- : 0;
436
- const track = {
437
- id,
438
- title: cleanupMusicName(String(basic.title)),
439
- artists: cleanupMusicName(String(basic.author || 'Unknown artist')),
440
- duration: durationSec
441
- ? `${Math.floor(durationSec / 60)}:${String(durationSec % 60).padStart(2, '0')}`
442
- : undefined,
443
- thumbnails: Array.isArray(basic.thumbnail) ? basic.thumbnail : [],
444
- };
445
- ctx.apiCache.set(cacheKey, track, 300);
446
- return { track: track };
447
- }
448
- catch (err) {
449
- logger.error({ err }, '[music] track lookup failed');
450
- return { track: null };
451
- }
452
- }),
453
- playlist: protectedProcedure.input(musicTrackInput()).query(async ({ input }) => {
454
- const id = requireId(input);
455
- const yt = await getAuthedInnertube().catch(() => null);
456
- if (!yt) {
457
- throw unauthorized('MUSIC_AUTH_REQUIRED');
458
- }
459
- try {
460
- return { tracks: await fetchPlaylistTracks(yt, id) };
461
- }
462
- catch (err) {
463
- logger.warn({ err }, '[music] playlist failed, retrying with a fresh saved-cookie session');
464
- const fresh = await refreshAuthedInnertube();
465
- if (!fresh)
466
- return { tracks: [] };
467
- try {
468
- return { tracks: await fetchPlaylistTracks(fresh, id) };
469
- }
470
- catch (retryErr) {
471
- logger.warn({ err: retryErr }, '[music] playlist retry failed, returning empty');
472
- return { tracks: [] };
473
- }
474
- }
475
- }),
476
- library: protectedProcedure.query(async () => {
477
- const yt = await getAuthedInnertube().catch(() => null);
478
- if (!yt) {
479
- throw unauthorized('MUSIC_AUTH_REQUIRED');
480
- }
481
- try {
482
- const first = await readLibrary(yt);
483
- if (first.tracks.length > 0 || first.playlists.length > 0)
484
- return first;
485
- logger.warn('[music] library came back empty, retrying with a fresh saved-cookie session');
486
- }
487
- catch (err) {
488
- logger.warn({ err }, '[music] library failed, retrying with a fresh saved-cookie session');
489
- }
490
- const fresh = await refreshAuthedInnertube();
491
- if (!fresh)
492
- return { tracks: [], playlists: [] };
493
- try {
494
- return await readLibrary(fresh);
495
- }
496
- catch (err) {
497
- logger.warn({ err }, '[music] library retry failed, returning empty');
498
- return { tracks: [], playlists: [] };
499
- }
500
- }),
501
- home: protectedProcedure.query(async () => {
502
- try {
503
- const yt = await getInnertube();
504
- const feed = await yt.music.getHomeFeed();
505
- return JSON.parse(JSON.stringify(feed));
506
- }
507
- catch (err) {
508
- logger.error({ err }, '[music] home feed failed');
509
- return { contents: [] };
510
- }
511
- }),
512
- upnext: protectedProcedure.input(musicTrackInput()).query(async ({ ctx, input }) => {
513
- const id = requireId(input);
514
- const authed = await getAuthedInnertube().catch(() => null);
515
- if (!authed) {
516
- const cacheKey = `music-upnext-${id.toLowerCase()}`;
517
- const cached = ctx.apiCache.get(cacheKey);
518
- if (cached)
519
- return cached;
520
- try {
521
- const yt = await getInnertube();
522
- const payload = parseUpnext(await yt.music.getUpNext(id), false);
523
- ctx.apiCache.set(cacheKey, payload, 300);
524
- return payload;
525
- }
526
- catch (err) {
527
- logger.error({ err }, '[music] upnext failed');
528
- return { tracks: [], playlistId: null };
529
- }
530
- }
531
- try {
532
- return parseUpnext(await authed.music.getUpNext(id), true);
533
- }
534
- catch (err) {
535
- logger.warn({ err }, '[music] authed upnext failed, retrying with a fresh saved-cookie session');
536
- }
537
- const fresh = await refreshAuthedInnertube();
538
- if (fresh) {
539
- try {
540
- return parseUpnext(await fresh.music.getUpNext(id), true);
541
- }
542
- catch (retryErr) {
543
- logger.warn({ err: retryErr }, '[music] upnext retry failed, falling back to public');
544
- }
545
- }
546
- try {
547
- const yt = await getInnertube();
548
- return parseUpnext(await yt.music.getUpNext(id), false);
549
- }
550
- catch (err) {
551
- logger.error({ err }, '[music] upnext failed');
552
- return { tracks: [], playlistId: null };
553
- }
554
- }),
555
- likes: protectedProcedure.query(async ({ ctx }) => {
556
- const yt = await getAuthedInnertube().catch(() => null);
557
- if (!yt) {
558
- throw unauthorized('MUSIC_AUTH_REQUIRED');
559
- }
560
- const cacheKey = 'music-liked-ids';
561
- const cached = ctx.apiCache.get(cacheKey);
562
- if (cached)
563
- return { likedIds: cached };
564
- const fetchLikedIds = async (session) => {
565
- const tracks = await fetchPlaylistTracks(session, 'LM', 500);
566
- return tracks.map((t) => t.id);
567
- };
568
- try {
569
- const likedIds = await fetchLikedIds(yt);
570
- ctx.apiCache.set(cacheKey, likedIds, 120);
571
- return { likedIds };
572
- }
573
- catch (err) {
574
- logger.warn({ err }, '[music] liked ids failed, retrying with a fresh saved-cookie session');
575
- const fresh = await refreshAuthedInnertube();
576
- if (!fresh)
577
- return { likedIds: [] };
578
- try {
579
- const likedIds = await fetchLikedIds(fresh);
580
- ctx.apiCache.set(cacheKey, likedIds, 120);
581
- return { likedIds };
582
- }
583
- catch (retryErr) {
584
- logger.warn({ err: retryErr }, '[music] liked ids retry failed, returning empty');
585
- return { likedIds: [] };
586
- }
587
- }
588
- }),
589
- like: protectedProcedure.input(musicLikeInput()).mutation(async ({ ctx, input }) => {
590
- const id = String(input.id || '').trim();
591
- const like = input.like === true || input.like === 'true';
592
- if (!id)
593
- throw badRequest('id is required');
594
- const yt = await getAuthedInnertube().catch(() => null);
595
- if (!yt) {
596
- throw unauthorized('MUSIC_AUTH_REQUIRED');
597
- }
598
- if (!(await verifyMusicSession())) {
599
- throw unauthorized('MUSIC_AUTH_REQUIRED');
600
- }
601
- const attempt = async (session) => {
602
- const shaped = session;
603
- const actions = shaped.actions ?? shaped.session?.actions;
604
- if (!actions)
605
- throw new Error('Actions unavailable');
606
- const panel = await session.music.getUpNext(id);
607
- const contents = panel.contents ?? [];
608
- let endpoint = null;
609
- let signedOut = false;
610
- for (const item of contents) {
611
- const toggle = findLikeToggle(item, id);
612
- const picked = like ? toggle.like : toggle.unlike;
613
- if (isSignInPrompt(picked)) {
614
- signedOut = true;
615
- continue;
616
- }
617
- if (picked) {
618
- endpoint = picked;
619
- break;
620
- }
621
- }
622
- if (signedOut && !endpoint)
623
- throw new Error('MUSIC_SESSION_EXPIRED');
624
- if (!endpoint)
625
- throw new Error('Like toggle not found for track');
626
- await endpoint.call(actions);
627
- };
628
- try {
629
- await attempt(yt);
630
- }
631
- catch (err) {
632
- if (err.message === 'MUSIC_SESSION_EXPIRED') {
633
- logger.warn('[music] like rejected, session expired');
634
- throw unauthorized('MUSIC_AUTH_REQUIRED');
635
- }
636
- logger.warn({ err }, '[music] rate failed, retrying with a fresh saved-cookie session');
637
- const fresh = await refreshAuthedInnertube();
638
- if (!fresh)
639
- throw failed('Like action failed');
640
- try {
641
- await attempt(fresh);
642
- }
643
- catch (retryErr) {
644
- if (retryErr.message === 'MUSIC_SESSION_EXPIRED') {
645
- throw unauthorized('MUSIC_AUTH_REQUIRED');
646
- }
647
- logger.error({ err: retryErr }, '[music] rate retry failed');
648
- throw failed('Like action failed');
649
- }
650
- }
651
- ctx.apiCache.delete('music-liked-ids');
652
- logger.info(`[music] rate id=${id} liked=${like} via=toggle`);
653
- return { success: true, liked: like };
654
- }),
655
- });