@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.
- package/README.md +1 -1
- package/client/dist/assets/{AnimeCard-C3P9zWi9.js → AnimeCard-DKDJK8Mp.js} +1 -1
- package/client/dist/assets/{AnimeInfo-Et6687Tl.js → AnimeInfo-CTcvfjva.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DX4eNnlH.js → AnimeInfoPage-B_UiacIN.js} +2 -2
- package/client/dist/assets/{Asmr-CCdEWrFi.js → Asmr-DMSN2vxl.js} +2 -2
- package/client/dist/assets/{AsmrResetProgressModal-DgEWBJOw.js → AsmrResetProgressModal-CRwfOatX.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-CQ2fjf2E.js → AvSyncCalibrator-CRJsi-vz.js} +1 -1
- package/client/dist/assets/{Home-DUkUTTTy.js → Home-pwmK9_Eh.js} +1 -1
- package/client/dist/assets/{Insights-QCh4xW12.js → Insights-m7kFzg2A.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BoTxRqHz.js → LibraryListPage-C1wpPvu6.js} +1 -1
- package/client/dist/assets/ListeningList-OzfEI1Ga.js +1 -0
- package/client/dist/assets/{Manga-HydR-L7o.js → Manga-ChRCExhK.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CBakEI9a.js → MangaInfoPage-DSHJPo2A.js} +1 -1
- package/client/dist/assets/{MangaReadPage-DwAR3iPy.js → MangaReadPage-CzOVY1wA.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DGzl8vsO.js → MangaResetProgressModal-CJfzdUBb.js} +1 -1
- package/client/dist/assets/{Mature-Pi0TxKuF.js → Mature-CifQMfhs.js} +1 -1
- package/client/dist/assets/{MediaCard--cuFxOCS.js → MediaCard-CM10yB9V.js} +1 -1
- package/client/dist/assets/{OptionListMenu-B3D39dGS.js → OptionListMenu-6Kovbsqw.js} +1 -1
- package/client/dist/assets/{Player-CsH4x_I_.js → Player-CK0hT-u4.js} +2 -2
- package/client/dist/assets/{PlayerSettings-DGxBsi3F.js → PlayerSettings-Bb66uqT9.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-Djq4Cpex.js → QueueOptionsButton-C_nTj8_q.js} +1 -1
- package/client/dist/assets/{QueueRail-DOrYgNVX.js → QueueRail-DUK7URAE.js} +1 -1
- package/client/dist/assets/{ReadingList-DwYoBrNk.js → ReadingList-Ck9LJThU.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CMF86mX6.js → ResetProgressModal-BUphIv0k.js} +1 -1
- package/client/dist/assets/{Search-DFkhuk-x.js → Search-zJyP15h6.js} +1 -1
- package/client/dist/assets/{Settings-DBB2r0VQ.js → Settings-BYdoSEUh.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-DJelJgAv.js → SkeletonGrid-CqgbOhjX.js} +1 -1
- package/client/dist/assets/{Trackers-DGYXFhYZ.js → Trackers-BVjyv6CM.js} +1 -1
- package/client/dist/assets/{Tv-C5MDGD2Z.js → Tv-L13ylswR.js} +1 -1
- package/client/dist/assets/{TvInfo-DrKuK13u.js → TvInfo-Cvcm5U2D.js} +1 -1
- package/client/dist/assets/{TvPopup-DtHilRDT.js → TvPopup-DhxfKvYm.js} +1 -1
- package/client/dist/assets/{TvSearch-CzNxdu_w.js → TvSearch-P8bFE6_X.js} +1 -1
- package/client/dist/assets/{TvWatchlist-D7p4br6S.js → TvWatchlist-D5gVPjtb.js} +1 -1
- package/client/dist/assets/{Watchlist-DrOzswl2.js → Watchlist-Cjq8l3rU.js} +1 -1
- package/client/dist/assets/{hls-BGCX7mJy.js → hls-BkzuANAg.js} +1 -1
- package/client/dist/assets/{index-CnfZwsds.js → index-CLVgzKLk.js} +19 -19
- package/client/dist/assets/{useAnimeInfoData-DYXWBPF-.js → useAnimeInfoData-DZJZu-so.js} +1 -1
- package/client/dist/assets/{useAsmr-BbhQ59bk.js → useAsmr-CvrMGHuo.js} +1 -1
- package/client/dist/assets/useAsmrPopup-Bimy8Ouw.js +1 -0
- package/client/dist/assets/{useMangaLibrary-DuesWQaE.js → useMangaLibrary--MiN11uR.js} +1 -1
- package/client/dist/assets/{useMangaPopup-D57OVGUr.js → useMangaPopup-CR5XhKKS.js} +1 -1
- package/client/dist/assets/{useProviders-mpQb_dix.js → useProviders-DqdT4fA5.js} +1 -1
- package/client/dist/assets/{useTv-DGflCsSI.js → useTv-BL0HMDyP.js} +1 -1
- package/client/dist/assets/{useTvLibrary-BqeSmyc4.js → useTvLibrary-D4U4iqN8.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +3 -5
- package/server/dist/config.js +1 -174
- package/server/dist/server.js +1068 -289
- package/server/dist/shipped-defaults.js +1 -6
- package/server/package.json +4 -5
- package/client/dist/assets/Asmr.module-CdWlioTY.js +0 -1
- package/client/dist/assets/ListeningList-DguKjI_t.js +0 -1
- package/client/dist/assets/Music-BGH_0voi.css +0 -1
- package/client/dist/assets/Music-CQpyXTLH.js +0 -1
- package/client/dist/assets/Radio-CCHNFN7R.css +0 -1
- package/client/dist/assets/Radio-DF6g6odd.js +0 -1
- package/client/dist/assets/Radio.module-DGSZLpKM.js +0 -1
- package/client/dist/assets/useAsmrPopup-SymV1J-b.js +0 -1
- package/server/dist/app-auth.js +0 -157
- package/server/dist/app-hono.js +0 -598
- package/server/dist/db/drizzle.js +0 -16
- package/server/dist/db/schema-anime.js +0 -61
- package/server/dist/db/schema-asmr.js +0 -13
- package/server/dist/db/schema-manga.js +0 -31
- package/server/dist/db/schema-tv.js +0 -18
- package/server/dist/db.js +0 -217
- package/server/dist/discord-gateway.js +0 -385
- package/server/dist/discord-rpc.js +0 -413
- package/server/dist/github-sync.js +0 -508
- package/server/dist/google.js +0 -741
- package/server/dist/hono/asmr.js +0 -10
- package/server/dist/hono/auth.js +0 -49
- package/server/dist/hono/music.js +0 -209
- package/server/dist/hono/proxy.js +0 -827
- package/server/dist/hono/tv.js +0 -200
- package/server/dist/hono/watchlist.js +0 -341
- package/server/dist/lib/anilist.js +0 -1583
- package/server/dist/lib/anischedule.js +0 -191
- package/server/dist/lib/insights.js +0 -185
- package/server/dist/lib/ipc.js +0 -23
- package/server/dist/lib/kitsu.js +0 -517
- package/server/dist/lib/mal-import.js +0 -291
- package/server/dist/lib/mal.js +0 -957
- package/server/dist/lib/migration.js +0 -310
- package/server/dist/lib/offline-db.js +0 -395
- package/server/dist/lib/potoken.js +0 -135
- package/server/dist/lib/recommendations.service.js +0 -216
- package/server/dist/lib/temp-ids.js +0 -3
- package/server/dist/lib/tmdb.js +0 -72
- package/server/dist/lib/tracker/anilist-tracker.js +0 -308
- package/server/dist/lib/tracker/manga-sync.service.js +0 -773
- package/server/dist/lib/tracker/sync-state.js +0 -21
- package/server/dist/lib/tracker/sync.service.js +0 -476
- package/server/dist/lib/translate.js +0 -79
- package/server/dist/lib/ump.js +0 -106
- package/server/dist/lib/ytmusic.js +0 -162
- package/server/dist/logger.js +0 -16
- package/server/dist/providers/manga/manga.types.js +0 -1
- package/server/dist/providers/provider.interface.js +0 -1
- package/server/dist/providers/remote-loader.js +0 -614
- package/server/dist/providers/remote-types.js +0 -159
- package/server/dist/providers/title-matching.js +0 -226
- package/server/dist/providers/tv.types.js +0 -1
- package/server/dist/rclone.js +0 -120
- package/server/dist/repositories/asmr.repository.js +0 -77
- package/server/dist/repositories/insights.repository.js +0 -106
- package/server/dist/repositories/library.repository.js +0 -94
- package/server/dist/repositories/mal-cache.repository.js +0 -52
- package/server/dist/repositories/manga.repository.js +0 -95
- package/server/dist/repositories/notifications.repository.js +0 -60
- package/server/dist/repositories/queue.repository.js +0 -72
- package/server/dist/repositories/settings.repository.js +0 -30
- package/server/dist/repositories/shows-meta.repository.js +0 -58
- package/server/dist/repositories/temp-show-ids.repository.js +0 -63
- package/server/dist/repositories/tv.repository.js +0 -93
- package/server/dist/repositories/watched-episodes.repository.js +0 -82
- package/server/dist/repositories/watchlist.repository.js +0 -60
- package/server/dist/scripts/backfill-episode-duration.js +0 -42
- package/server/dist/sync-payload.js +0 -91
- package/server/dist/sync.js +0 -657
- package/server/dist/trpc/context.js +0 -7
- package/server/dist/trpc/errors.js +0 -15
- package/server/dist/trpc/index.js +0 -18
- package/server/dist/trpc/router.js +0 -49
- package/server/dist/trpc/routers/asmr-library.js +0 -112
- package/server/dist/trpc/routers/asmr-progress.js +0 -121
- package/server/dist/trpc/routers/asmr.js +0 -87
- package/server/dist/trpc/routers/auth.js +0 -194
- package/server/dist/trpc/routers/continue-watching.js +0 -81
- package/server/dist/trpc/routers/data.js +0 -1026
- package/server/dist/trpc/routers/discord.js +0 -285
- package/server/dist/trpc/routers/discovery.js +0 -11
- package/server/dist/trpc/routers/insights.js +0 -14
- package/server/dist/trpc/routers/manga-library.js +0 -263
- package/server/dist/trpc/routers/manga-progress.js +0 -169
- package/server/dist/trpc/routers/manga.js +0 -201
- package/server/dist/trpc/routers/music.js +0 -655
- package/server/dist/trpc/routers/notifications.js +0 -80
- package/server/dist/trpc/routers/progress.js +0 -109
- package/server/dist/trpc/routers/queue.js +0 -93
- package/server/dist/trpc/routers/radio.js +0 -145
- package/server/dist/trpc/routers/settings.js +0 -96
- package/server/dist/trpc/routers/tracker.js +0 -213
- package/server/dist/trpc/routers/tv-library.js +0 -135
- package/server/dist/trpc/routers/tv-progress.js +0 -159
- package/server/dist/trpc/routers/tv.js +0 -551
- package/server/dist/trpc/routers/watchlist.js +0 -550
- package/server/dist/trpc/validation.js +0 -258
- package/server/dist/utils/cache.utils.js +0 -25
- package/server/dist/utils/cookie.utils.js +0 -13
- package/server/dist/utils/db-utils.js +0 -17
- package/server/dist/utils/env.utils.js +0 -77
- package/server/dist/utils/http.utils.js +0 -82
- package/server/dist/utils/request-context.js +0 -2
- package/server/dist/utils/security.utils.js +0 -76
- /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
|
-
});
|