@serifpersia/dango 3.6.7 → 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 (149) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DraA_TF9.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-BJ7pzs-P.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_esUr5C.js} +2 -2
  5. package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-Ckuh2xfW.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-BS6UFJN4.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-DwHCd9yB.js} +1 -1
  8. package/client/dist/assets/{Home-DUkUTTTy.js → Home-DuM1EuvN.js} +1 -1
  9. package/client/dist/assets/{Insights-QCh4xW12.js → Insights-D3Pd9c9V.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-Dm2yVn0-.js} +1 -1
  11. package/client/dist/assets/{ListeningList-DguKjI_t.js → ListeningList-D-zt6EXZ.js} +1 -1
  12. package/client/dist/assets/{Manga-HydR-L7o.js → Manga-JwprMKBk.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DaeyJl2p.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-COg0fHI9.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-kguXv0_M.js} +1 -1
  16. package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-IHDaasrs.js} +1 -1
  17. package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CpwsrXo5.js} +1 -1
  18. package/client/dist/assets/{Music-CQpyXTLH.js → Music-Dks4tPeK.js} +1 -1
  19. package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-11CcAsy8.js} +1 -1
  20. package/client/dist/assets/{Player-CsH4x_I_.js → Player-DQoEQkHL.js} +2 -2
  21. package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-D4Ud4kv4.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-B8PlPKjC.js} +1 -1
  23. package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-B4Dok7U3.js} +1 -1
  24. package/client/dist/assets/{Radio-DF6g6odd.js → Radio-BHK6LSk4.js} +1 -1
  25. package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-BCsxIz57.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BXHxSfy-.js} +1 -1
  27. package/client/dist/assets/{Search-DFkhuk-x.js → Search-PK1ds07O.js} +1 -1
  28. package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-Bxk_x55l.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CpJCZSRN.js} +1 -1
  30. package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-RieCh_84.js} +1 -1
  31. package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-DLytPJUj.js} +1 -1
  32. package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-C8djKebR.js} +1 -1
  33. package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-40JGGJDk.js} +1 -1
  34. package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-A3hbospf.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-DuXjqmZM.js} +1 -1
  36. package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-B9wGBh0u.js} +1 -1
  37. package/client/dist/assets/{hls-BGCX7mJy.js → hls-CZ5X3Ya-.js} +1 -1
  38. package/client/dist/assets/{index-CnfZwsds.js → index-dRhc9-ol.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-ZzDtLfvD.js} +1 -1
  40. package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CKOdHgol.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-SymV1J-b.js → useAsmrPopup-SFHPhpcl.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary-CKATe31P.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-C75QuYgY.js} +1 -1
  44. package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-CMtvZbEi.js} +1 -1
  45. package/client/dist/assets/{useTv-DGflCsSI.js → useTv-DUX4zYMr.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-BqeSmyc4.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 -783
  66. package/server/dist/hono/tv.js +5 -200
  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 -614
  92. package/server/dist/providers/remote-types.js +1 -159
  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 -310
  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
@@ -1,655 +1 @@
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
- });
1
+ import{TRPCError as N}from"@trpc/server";import{protectedProcedure as y,router as $}from"../index.js";import{badRequest as I,defineSchema as A,failed as x,optStr as R,reqObj as U}from"../validation.js";import u from"../../logger.js";import{getInnertube as M,getAuthedInnertube as m,refreshAuthedInnertube as T,saveMusicCookie as j,getMusicAuthStatus as F,signOutMusic as H,isParserVariantError as Y,verifyMusicSession as V}from"../../lib/ytmusic.js";const Q=[/\s*(- topic)$/i,/\s*vevo$/i,/\s*[(|[]official(.*?)[)|\]]/i,/\s*[(|[]((lyrics?|visualizer|audio)\s*(video)?)[)|\]]/i,/\s*[(|[](performance video)[)|\]]/i,/\s*[(|[](clip official)[)|\]]/i,/\s*[(|[](video version)[)|\]]/i,/\s*[(|[](HD|HQ)\s*?(?:audio)?[)|\]]$/i,/\s*[(|[](live)[)|\]]$/i,/\s*[(|[]4K\s*?(?:upgrade)?[)|\]]$/i];function h(i){if(!i)return i;let n=i;for(const t of Q)n=n.replace(t,"");return n.trim()}function O(i){const n=i,t=n.video_id||n.videoId||n.id||"";if(!t||typeof t!="string")return null;const r=n.title,a=typeof r=="string"?r:r?.text??"Unknown title",s=n.artists??n.authors??n.subtitle;let c="";Array.isArray(s)?c=s.map(d=>{const f=d;return f.name||f.text||f.title||""}).filter(Boolean).join(", "):typeof s=="string"?c=s:s&&typeof s=="object"&&(c=s.text??"");const e=n.thumbnails??n.thumbnail,o=Array.isArray(e)?e:Array.isArray(e?.thumbnails)?e.thumbnails:[],l=E(n.album),k=E(n.duration);return{id:t,title:h(String(a)),artists:h(c),album:l?h(l):void 0,duration:k||void 0,thumbnails:o}}function E(i){if(!i)return"";if(typeof i=="string")return i;const n=i;return typeof n.text=="string"?n.text:Array.isArray(n.runs)?n.runs.map(t=>t.text||"").join(""):""}function _(i){if(!i)return[];if(Array.isArray(i))return i.filter(t=>t&&typeof t.url=="string").map(t=>({url:t.url,width:t.width,height:t.height}));const n=i;return Array.isArray(n.thumbnails)?_(n.thumbnails):[]}function P(i){const n=i;return typeof n?.type=="string"?n.type:n?.constructor?.name??""}function K(i,n=!1){const t=P(i);if(t==="AutomixPreviewVideo")return null;const r=i,a=t==="PlaylistPanelVideoWrapper"?r.primary??null:r;if(!a||typeof a!="object")return null;const s=a.video_id;if(typeof s!="string"||!s)return null;const c=a.artists;let e="";Array.isArray(c)&&(e=c.map(f=>f?.name||"").filter(Boolean).join(", ")),!e&&typeof a.author=="string"&&(e=a.author);const o=a.album,l=a.duration,k=typeof l=="string"?l:l?.text??"";let d=null;if(n)for(const f of[a.menu,i.menu]){let w=null;for(const b of D(f)){if(P(b)!=="ToggleMenuServiceItem")continue;const g=b.icon_type;if(!(g!=="FAVORITE"&&g!=="UNFAVORITE")){w=g==="UNFAVORITE";break}}if(w!==null){d=w;break}}return{id:s,title:h(E(a.title)||"Unknown title"),artists:h(e||"Unknown artist"),album:o?.name?h(o.name):void 0,duration:k||void 0,thumbnails:_(a.thumbnail??a.thumbnails),...d!==null?{liked:d}:{}}}function B(i){return!!i?.payload&&"signInEndpoint"in i.payload}function C(i,n){const t=[],r=i,a=r.contents??[];for(const c of a){const e=K(c,n);if(e&&t.push(e),t.length>=50)break}const s=r.playlist_id??null;return{tracks:t,playlistId:s}}function D(i){if(!i||typeof i!="object")return[];const n=i.items;return Array.isArray(n)?n:[]}function X(i,n){const t={like:null,unlike:null},r=P(i),a=i,s=r==="PlaylistPanelVideoWrapper"?a.primary??null:a;if(!s||s.video_id!==n)return t;const c=[s.menu,a.menu];for(const e of c)for(const o of D(e)){if(P(o)!=="ToggleMenuServiceItem")continue;const l=o.icon_type;if(!(l!=="FAVORITE"&&l!=="UNFAVORITE")){for(const k of["default_endpoint","toggled_endpoint"]){const d=o[k];if(typeof d?.call!="function")continue;const f=d.payload?.status;f==="LIKE"&&!t.like?t.like=d:f==="INDIFFERENT"&&!t.unlike&&(t.unlike=d)}if(t.like&&t.unlike)return t}}return t}async function S(i,n,t=500){const r=[];let a=await i.music.getPlaylist(n);for(let s=0;s<6;s++){for(const c of(a.items??a.contents??[]).slice(0,t-r.length)){const e=O(c);if(e&&r.push(e),r.length>=t)break}if(r.length>=t||!a.has_continuation||typeof a.getContinuation!="function")break;try{a=await a.getContinuation()}catch(c){u.warn({err:c},"[music] playlist continuation failed");break}}return r}async function v(i){let n;try{n=await i.music.getLibrary()}catch(e){if(!Y(e))throw e;u.warn("[music] library landing hit a layout variant, falling back to Liked playlist");try{return{tracks:await S(i,"LM"),playlists:[]}}catch(o){return u.warn({err:o},"[music] liked playlist fallback failed"),{tracks:[],playlists:[]}}}const t=[];for(const e of n.contents??[]){const o=e.items??e.contents??[];t.push(...o)}const r=[],a=new Set;for(const e of t){const o=e,l=typeof o.playlist_id=="string"&&o.playlist_id||typeof o.playlistId=="string"&&o.playlistId||typeof o.id=="string"&&o.id||"",k=E(o.title),d=E(o.subtitle);!l||!k||a.has(l)||!/playlist|mix/i.test(d)&&k!=="Liked Music"||(a.add(l),r.push({id:l,title:k,subtitle:d||void 0,thumbnails:_(o.thumbnails??o.thumbnail)}))}const s=[],c=r.find(e=>e.title==="Liked Music");if(c)try{s.push(...await S(i,c.id))}catch(e){u.warn({err:e},"[music] liked playlist fetch failed")}return u.info(`[music] library parsed tracks=${s.length} playlists=${r.length} gridItems=${t.length}`),{tracks:s,playlists:r}}function p(i){return new N({code:"UNAUTHORIZED",message:i})}const z=()=>A(i=>{const n=U(i),t={},r=R(n,"q");return r!==void 0&&(t.q=r),t}),q=()=>A(i=>{const n=U(i),t={},r=R(n,"id");return r!==void 0&&(t.id=r),t}),J=()=>A(i=>{const n=U(i),t={},r=R(n,"cookie");return r!==void 0&&(t.cookie=r),t}),W=()=>A(i=>{const n=U(i),t={},r=R(n,"id");return r!==void 0&&(t.id=r),(typeof n.like=="boolean"||typeof n.like=="string")&&(t.like=n.like),t});function L(i){const n=String(i.id||"").trim();if(!n)throw I("id is required");return n}const et=$({authStatus:y.query(()=>F()),authStart:y.input(J()).mutation(async({input:i})=>{let n=String(i.cookie||"").trim();if(!n)throw I("Cookie is required");if(n=n.replace(/^cookie\s*:\s*/i,"").replace(/^["']+|["']+$/g,"").trim(),n.includes("\u2026")||n.includes("..."))throw I(`Cookie looks truncated (${n.length} chars). In DevTools, right-click the Cookie header \u2192 Copy value (do not copy from the wrapped preview).`);if(!n.includes("SID")&&!n.includes("SAPISID"))throw I('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 \u2192 Network.');try{await j(n);const t=await m();if(t)try{const r=await v(t);return{success:!0,tracks:r.tracks.length,playlists:r.playlists.length}}catch{}return{success:!0,tracks:0,playlists:0}}catch(t){throw t instanceof N?t:(u.error({err:t},"[music] cookie sign-in failed"),p("Cookie rejected by YouTube Music. Copy a fresh Cookie header from music.youtube.com."))}}),signOut:y.mutation(async()=>{try{return await H(),{success:!0}}catch(i){throw u.error({err:i},"[music] sign-out failed"),x("Sign-out failed")}}),search:y.input(z()).query(async({ctx:i,input:n})=>{const t=String(n.q||"").trim();if(!t)return{tracks:[]};const r=`music-search-${t.toLowerCase()}`,a=i.apiCache.get(r);if(a)return{tracks:a};try{const c=await(await M()).music.search(t,{type:"song"}),e=[],o=c.contents??[];for(const l of o){const k=l.contents??[l];for(const d of k){const f=O(d);if(f&&e.push(f),e.length>=25)break}if(e.length>=25)break}return i.apiCache.set(r,e,300),{tracks:e}}catch(s){return u.error({err:s},"[music] search failed"),{tracks:[]}}}),track:y.input(q()).query(async({ctx:i,input:n})=>{const t=L(n),r=`music-track-${t.toLowerCase()}`,a=i.apiCache.get(r);if(a)return{track:a};try{const e=(await(await M()).music.getInfo(t)).basic_info;if(!e?.title)return{track:null};const o=typeof e.duration=="number"&&Number.isFinite(e.duration)?Math.max(Math.round(e.duration),0):0,l={id:t,title:h(String(e.title)),artists:h(String(e.author||"Unknown artist")),duration:o?`${Math.floor(o/60)}:${String(o%60).padStart(2,"0")}`:void 0,thumbnails:Array.isArray(e.thumbnail)?e.thumbnail:[]};return i.apiCache.set(r,l,300),{track:l}}catch(s){return u.error({err:s},"[music] track lookup failed"),{track:null}}}),playlist:y.input(q()).query(async({input:i})=>{const n=L(i),t=await m().catch(()=>null);if(!t)throw p("MUSIC_AUTH_REQUIRED");try{return{tracks:await S(t,n)}}catch(r){u.warn({err:r},"[music] playlist failed, retrying with a fresh saved-cookie session");const a=await T();if(!a)return{tracks:[]};try{return{tracks:await S(a,n)}}catch(s){return u.warn({err:s},"[music] playlist retry failed, returning empty"),{tracks:[]}}}}),library:y.query(async()=>{const i=await m().catch(()=>null);if(!i)throw p("MUSIC_AUTH_REQUIRED");try{const t=await v(i);if(t.tracks.length>0||t.playlists.length>0)return t;u.warn("[music] library came back empty, retrying with a fresh saved-cookie session")}catch(t){u.warn({err:t},"[music] library failed, retrying with a fresh saved-cookie session")}const n=await T();if(!n)return{tracks:[],playlists:[]};try{return await v(n)}catch(t){return u.warn({err:t},"[music] library retry failed, returning empty"),{tracks:[],playlists:[]}}}),home:y.query(async()=>{try{const n=await(await M()).music.getHomeFeed();return JSON.parse(JSON.stringify(n))}catch(i){return u.error({err:i},"[music] home feed failed"),{contents:[]}}}),upnext:y.input(q()).query(async({ctx:i,input:n})=>{const t=L(n),r=await m().catch(()=>null);if(!r){const s=`music-upnext-${t.toLowerCase()}`,c=i.apiCache.get(s);if(c)return c;try{const e=await M(),o=C(await e.music.getUpNext(t),!1);return i.apiCache.set(s,o,300),o}catch(e){return u.error({err:e},"[music] upnext failed"),{tracks:[],playlistId:null}}}try{return C(await r.music.getUpNext(t),!0)}catch(s){u.warn({err:s},"[music] authed upnext failed, retrying with a fresh saved-cookie session")}const a=await T();if(a)try{return C(await a.music.getUpNext(t),!0)}catch(s){u.warn({err:s},"[music] upnext retry failed, falling back to public")}try{const s=await M();return C(await s.music.getUpNext(t),!1)}catch(s){return u.error({err:s},"[music] upnext failed"),{tracks:[],playlistId:null}}}),likes:y.query(async({ctx:i})=>{const n=await m().catch(()=>null);if(!n)throw p("MUSIC_AUTH_REQUIRED");const t="music-liked-ids",r=i.apiCache.get(t);if(r)return{likedIds:r};const a=async s=>(await S(s,"LM",500)).map(e=>e.id);try{const s=await a(n);return i.apiCache.set(t,s,120),{likedIds:s}}catch(s){u.warn({err:s},"[music] liked ids failed, retrying with a fresh saved-cookie session");const c=await T();if(!c)return{likedIds:[]};try{const e=await a(c);return i.apiCache.set(t,e,120),{likedIds:e}}catch(e){return u.warn({err:e},"[music] liked ids retry failed, returning empty"),{likedIds:[]}}}}),like:y.input(W()).mutation(async({ctx:i,input:n})=>{const t=String(n.id||"").trim(),r=n.like===!0||n.like==="true";if(!t)throw I("id is required");const a=await m().catch(()=>null);if(!a||!await V())throw p("MUSIC_AUTH_REQUIRED");const s=async c=>{const e=c,o=e.actions??e.session?.actions;if(!o)throw new Error("Actions unavailable");const k=(await c.music.getUpNext(t)).contents??[];let d=null,f=!1;for(const w of k){const b=X(w,t),g=r?b.like:b.unlike;if(B(g)){f=!0;continue}if(g){d=g;break}}if(f&&!d)throw new Error("MUSIC_SESSION_EXPIRED");if(!d)throw new Error("Like toggle not found for track");await d.call(o)};try{await s(a)}catch(c){if(c.message==="MUSIC_SESSION_EXPIRED")throw u.warn("[music] like rejected, session expired"),p("MUSIC_AUTH_REQUIRED");u.warn({err:c},"[music] rate failed, retrying with a fresh saved-cookie session");const e=await T();if(!e)throw x("Like action failed");try{await s(e)}catch(o){throw o.message==="MUSIC_SESSION_EXPIRED"?p("MUSIC_AUTH_REQUIRED"):(u.error({err:o},"[music] rate retry failed"),x("Like action failed"))}}return i.apiCache.delete("music-liked-ids"),u.info(`[music] rate id=${t} liked=${r} via=toggle`),{success:!0,liked:r}})});export{et as musicRouter};