@serifpersia/dango 3.4.4 → 3.4.6
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-4CEz8Buu.js → AnimeCard-BVdH0EeW.js} +1 -1
- package/client/dist/assets/{AnimeInfo-DyX9qTwg.js → AnimeInfo-Dw2Nop6Q.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-Dvp_c8vK.js → AnimeInfoPage-Cw-Tq3YX.js} +2 -2
- package/client/dist/assets/{Asmr-qg3lMAJn.js → Asmr-BrjzzyOi.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-BpPlg_Vv.js → AsmrResetProgressModal-CYjpcYpb.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-FJllOgOH.js → AvSyncCalibrator-BuHurVgo.js} +3 -3
- package/client/dist/assets/AvSyncCalibrator-D0FYSwiz.css +1 -0
- package/client/dist/assets/{Home-A7gCeEST.js → Home-KdnP7sNM.js} +1 -1
- package/client/dist/assets/{Insights-G9IXepHJ.js → Insights-DDWry-t1.js} +1 -1
- package/client/dist/assets/{LibraryListPage-Bh2qCJPh.js → LibraryListPage-CV8Z9JlO.js} +1 -1
- package/client/dist/assets/{ListeningList-Czv4cTRl.js → ListeningList-DSMbMZT7.js} +1 -1
- package/client/dist/assets/{Manga-qdAAS1z7.js → Manga-J6XoqEKq.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-1rTgZzf2.js → MangaInfoPage-N7TxsN5u.js} +1 -1
- package/client/dist/assets/{MangaReadPage-CeyxPG6w.js → MangaReadPage-D_VzSNWq.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-C_ppotmh.js → MangaResetProgressModal-DyhQYBDy.js} +1 -1
- package/client/dist/assets/{Mature-Bkhxnr3K.js → Mature-Db53R-LR.js} +1 -1
- package/client/dist/assets/{MediaCard-BQZsutEo.js → MediaCard-BcLjlpfR.js} +1 -1
- package/client/dist/assets/{Music-C0rGzSbi.js → Music-DjxQjJOl.js} +1 -1
- package/client/dist/assets/{OptionListMenu-CyfpCBEy.js → OptionListMenu-DngTgHxH.js} +1 -1
- package/client/dist/assets/Player-Be_7Ne5p.js +56 -0
- package/client/dist/assets/{PlayerSettings-icy4SfQa.js → PlayerSettings-D47ItGDR.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-5eloKF5J.js → QueueOptionsButton-ClVTpH8J.js} +1 -1
- package/client/dist/assets/{QueueRail-DaMjlKL4.js → QueueRail-DeJKVRs5.js} +1 -1
- package/client/dist/assets/{Radio-FxGqCxR4.js → Radio-DU0ma3GA.js} +1 -1
- package/client/dist/assets/{ReadingList-CCm3IAbt.js → ReadingList-qXW7Dk5z.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-CeI4V3Y5.js → ResetProgressModal-C6ukct_c.js} +1 -1
- package/client/dist/assets/{Search-qpILaAiQ.js → Search-DjjKA_Wu.js} +1 -1
- package/client/dist/assets/{Settings-Bw9M3NGz.js → Settings-DQt_S5Qw.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-0dY_xMq3.js → SkeletonGrid-nx2HEi4P.js} +1 -1
- package/client/dist/assets/{Trackers-Zy5GOs0P.js → Trackers-BcGzeqj2.js} +1 -1
- package/client/dist/assets/Tv-COvyHdeJ.js +4 -0
- package/client/dist/assets/{TvInfo-CHIsVzZ0.js → TvInfo-CUd1agFy.js} +1 -1
- package/client/dist/assets/{TvPopup-D_tVTyZ2.js → TvPopup-CJeu3tgc.js} +1 -1
- package/client/dist/assets/{TvSearch-BVxedBW-.js → TvSearch-BIypjbU3.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BEIwq9R9.js → TvWatchlist-DZrBdiqZ.js} +1 -1
- package/client/dist/assets/{Watchlist-CSngjqlz.js → Watchlist-Dw8cPe0n.js} +1 -1
- package/client/dist/assets/{hls-D48GVTkk.js → hls-B3_1VuDP.js} +1 -1
- package/client/dist/assets/{index-CVB5dYhT.js → index-gMxawd-E.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-DxdOg_B7.js → useAnimeInfoData-_iOLEAa8.js} +1 -1
- package/client/dist/assets/{useAsmr-ClckxnPe.js → useAsmr-Blk4v41t.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-Bbgxefy_.js → useAsmrPopup-D9SQIgZa.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-B3_BUKVk.js → useMangaLibrary-B60at6ni.js} +1 -1
- package/client/dist/assets/{useMangaPopup-Celk4lzf.js → useMangaPopup-DEfGjWSu.js} +1 -1
- package/client/dist/assets/{useProviders-pwxPZPHh.js → useProviders-Ca0bZWt7.js} +1 -1
- package/client/dist/assets/{useTv-D9sQrDop.js → useTv-WJCDL5_5.js} +1 -1
- package/client/dist/assets/{useTvLibrary-UsQpgBTB.js → useTvLibrary-Fgn5i9rJ.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +4 -2
- package/server/dist/app-hono.js +2 -0
- package/server/dist/hono/asmr.js +10 -1
- package/server/dist/hono/music.js +122 -332
- package/server/dist/hono/proxy.js +105 -99
- package/server/dist/hono/tv.js +5 -58
- package/server/dist/hono/watchlist.js +2 -291
- package/server/dist/lib/anilist.js +0 -3
- package/server/dist/lib/mal.js +4 -41
- package/server/dist/lib/offline-db.js +0 -6
- package/server/dist/lib/potoken.js +135 -0
- package/server/dist/lib/tracker/manga-sync.service.js +3 -22
- package/server/dist/lib/tracker/sync-state.js +21 -0
- package/server/dist/lib/tracker/sync.service.js +3 -22
- package/server/dist/lib/ump.js +106 -0
- package/server/dist/providers/remote-loader.js +17 -27
- package/server/dist/providers/title-matching.js +5 -5
- package/server/dist/repositories/asmr.repository.js +4 -53
- package/server/dist/repositories/library.repository.js +46 -0
- package/server/dist/repositories/manga.repository.js +3 -53
- package/server/dist/repositories/settings.repository.js +8 -0
- package/server/dist/repositories/tv.repository.js +3 -46
- package/server/dist/repositories/watched-episodes.repository.js +0 -2
- package/server/dist/server.js +5 -1
- package/server/dist/sync.js +3 -5
- package/server/dist/trpc/routers/asmr-library.js +2 -12
- package/server/dist/trpc/routers/asmr-progress.js +10 -22
- package/server/dist/trpc/routers/data.js +17 -36
- package/server/dist/trpc/routers/discord.js +1 -8
- package/server/dist/trpc/routers/insights.js +15 -16
- package/server/dist/trpc/routers/manga-library.js +5 -22
- package/server/dist/trpc/routers/manga-progress.js +8 -20
- package/server/dist/trpc/routers/music.js +6 -11
- package/server/dist/trpc/routers/queue.js +1 -1
- package/server/dist/trpc/routers/tracker.js +1 -7
- package/server/dist/trpc/routers/tv-library.js +2 -12
- package/server/dist/trpc/routers/tv-progress.js +16 -28
- package/server/dist/trpc/routers/tv.js +5 -21
- package/server/dist/trpc/routers/watchlist.js +1 -8
- package/server/dist/trpc/validation.js +35 -0
- package/server/dist/utils/db-utils.js +8 -0
- package/server/dist/utils/http.utils.js +12 -0
- package/server/dist/utils/machine-id.js +1 -2
- package/server/dist/utils/megaplay.utils.js +1 -2
- package/server/package.json +4 -1
- package/client/dist/assets/AvSyncCalibrator-C2wu1ydI.css +0 -1
- package/client/dist/assets/Player-yhiTlpYU.js +0 -56
- package/client/dist/assets/Tv-DREwc7BX.js +0 -4
|
@@ -1,292 +1,48 @@
|
|
|
1
|
-
import { AppCache } from '../utils/cache.utils.js';
|
|
2
1
|
import logger from '../logger.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
let out = name;
|
|
21
|
-
for (const re of NAME_SUFFIXES)
|
|
22
|
-
out = out.replace(re, '');
|
|
23
|
-
return out.trim();
|
|
24
|
-
}
|
|
25
|
-
function toTrack(node) {
|
|
26
|
-
const n = node;
|
|
27
|
-
const id = n['video_id'] || n['videoId'] || n['id'] || '';
|
|
28
|
-
if (!id || typeof id !== 'string')
|
|
29
|
-
return null;
|
|
30
|
-
const titleObj = n['title'];
|
|
31
|
-
const title = typeof titleObj === 'string' ? titleObj : (titleObj?.text ?? 'Unknown title');
|
|
32
|
-
const artistsRaw = n['artists'] ?? n['authors'] ?? n['subtitle'];
|
|
33
|
-
let artists = '';
|
|
34
|
-
if (Array.isArray(artistsRaw)) {
|
|
35
|
-
artists = artistsRaw
|
|
36
|
-
.map((a) => {
|
|
37
|
-
const item = a;
|
|
38
|
-
return item.name || item.text || item.title || '';
|
|
39
|
-
})
|
|
40
|
-
.filter(Boolean)
|
|
41
|
-
.join(', ');
|
|
42
|
-
}
|
|
43
|
-
else if (typeof artistsRaw === 'string') {
|
|
44
|
-
artists = artistsRaw;
|
|
45
|
-
}
|
|
46
|
-
else if (artistsRaw && typeof artistsRaw === 'object') {
|
|
47
|
-
artists = artistsRaw.text ?? '';
|
|
48
|
-
}
|
|
49
|
-
const thumbsRaw = (n['thumbnails'] ?? n['thumbnail']);
|
|
50
|
-
const thumbnails = Array.isArray(thumbsRaw)
|
|
51
|
-
? thumbsRaw
|
|
52
|
-
: Array.isArray(thumbsRaw?.thumbnails)
|
|
53
|
-
? thumbsRaw.thumbnails
|
|
54
|
-
: [];
|
|
55
|
-
const albumText = textOf(n['album']);
|
|
56
|
-
const durationText = textOf(n['duration']);
|
|
57
|
-
return {
|
|
58
|
-
id,
|
|
59
|
-
title: cleanupMusicName(String(title)),
|
|
60
|
-
artists: cleanupMusicName(artists),
|
|
61
|
-
album: albumText ? cleanupMusicName(albumText) : undefined,
|
|
62
|
-
duration: durationText || undefined,
|
|
63
|
-
thumbnails,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
function textOf(value) {
|
|
67
|
-
if (!value)
|
|
68
|
-
return '';
|
|
69
|
-
if (typeof value === 'string')
|
|
70
|
-
return value;
|
|
71
|
-
const obj = value;
|
|
72
|
-
if (typeof obj.text === 'string')
|
|
73
|
-
return obj.text;
|
|
74
|
-
if (Array.isArray(obj.runs))
|
|
75
|
-
return obj.runs.map((r) => r.text || '').join('');
|
|
76
|
-
return '';
|
|
2
|
+
import { parseUmpResponse } from '../lib/ump.js';
|
|
3
|
+
import { mintPoToken, resetPoTokens } from '../lib/potoken.js';
|
|
4
|
+
import { getInnertube, getAuthedInnertube, getMusicCookie, refreshAuthedInnertube, } from '../lib/ytmusic.js';
|
|
5
|
+
const UPSTREAM_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:156.0) Gecko/20100101 Firefox/156.0';
|
|
6
|
+
const audioCache = new Map();
|
|
7
|
+
const AUDIO_CACHE_MS = 5 * 60 * 1000;
|
|
8
|
+
const FIRST_CHUNK = 16384;
|
|
9
|
+
const AUDIO_CHUNK = 262144;
|
|
10
|
+
const UMP_CVER = '1.20260920.16.00';
|
|
11
|
+
const audioTotalCache = new Map();
|
|
12
|
+
const umpSessions = new Map();
|
|
13
|
+
function randomCpn() {
|
|
14
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
15
|
+
let out = '';
|
|
16
|
+
for (let i = 0; i < 16; i++)
|
|
17
|
+
out += chars[Math.floor(Math.random() * chars.length)];
|
|
18
|
+
return out;
|
|
77
19
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.filter((t) => t && typeof t.url === 'string')
|
|
84
|
-
.map((t) => ({ url: t.url, width: t.width, height: t.height }));
|
|
20
|
+
function getUmpSession(videoId) {
|
|
21
|
+
let session = umpSessions.get(videoId);
|
|
22
|
+
if (!session) {
|
|
23
|
+
session = { cpn: randomCpn(), rn: 0, carry: null };
|
|
24
|
+
umpSessions.set(videoId, session);
|
|
85
25
|
}
|
|
86
|
-
|
|
87
|
-
if (Array.isArray(obj.thumbnails))
|
|
88
|
-
return thumbsOf(obj.thumbnails);
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
function nodeType(node) {
|
|
92
|
-
const n = node;
|
|
93
|
-
if (typeof n?.type === 'string')
|
|
94
|
-
return n.type;
|
|
95
|
-
return n?.constructor?.name ?? '';
|
|
26
|
+
return session;
|
|
96
27
|
}
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
? (n['primary'] ?? null)
|
|
104
|
-
: n;
|
|
105
|
-
if (!video || typeof video !== 'object')
|
|
106
|
-
return null;
|
|
107
|
-
const id = video['video_id'];
|
|
108
|
-
if (typeof id !== 'string' || !id)
|
|
109
|
-
return null;
|
|
110
|
-
const artistsRaw = video['artists'];
|
|
111
|
-
let artists = '';
|
|
112
|
-
if (Array.isArray(artistsRaw)) {
|
|
113
|
-
artists = artistsRaw
|
|
114
|
-
.map((a) => a?.name || '')
|
|
115
|
-
.filter(Boolean)
|
|
116
|
-
.join(', ');
|
|
117
|
-
}
|
|
118
|
-
if (!artists && typeof video['author'] === 'string')
|
|
119
|
-
artists = video['author'];
|
|
120
|
-
const albumRaw = video['album'];
|
|
121
|
-
const durationRaw = video['duration'];
|
|
122
|
-
const durationText = typeof durationRaw === 'string' ? durationRaw : (durationRaw?.text ?? '');
|
|
123
|
-
let liked = null;
|
|
124
|
-
if (trustLike) {
|
|
125
|
-
for (const menu of [video['menu'], node['menu']]) {
|
|
126
|
-
let found = null;
|
|
127
|
-
for (const entry of asMenuItems(menu)) {
|
|
128
|
-
if (nodeType(entry) !== 'ToggleMenuServiceItem')
|
|
129
|
-
continue;
|
|
130
|
-
const icon = entry['icon_type'];
|
|
131
|
-
if (icon !== 'FAVORITE' && icon !== 'UNFAVORITE')
|
|
132
|
-
continue;
|
|
133
|
-
found = icon === 'UNFAVORITE';
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
if (found !== null) {
|
|
137
|
-
liked = found;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
id,
|
|
144
|
-
title: cleanupMusicName(textOf(video['title']) || 'Unknown title'),
|
|
145
|
-
artists: cleanupMusicName(artists || 'Unknown artist'),
|
|
146
|
-
album: albumRaw?.name ? cleanupMusicName(albumRaw.name) : undefined,
|
|
147
|
-
duration: durationText || undefined,
|
|
148
|
-
thumbnails: thumbsOf(video['thumbnail'] ?? video['thumbnails']),
|
|
149
|
-
...(liked !== null ? { liked } : {}),
|
|
28
|
+
function boundWindow(clientRange, videoId) {
|
|
29
|
+
const total = audioTotalCache.get(videoId);
|
|
30
|
+
const clamp = (start, end) => {
|
|
31
|
+
if (total !== undefined)
|
|
32
|
+
end = Math.min(end, total - 1);
|
|
33
|
+
return [start, Math.max(end, start)];
|
|
150
34
|
};
|
|
35
|
+
const m = /^bytes=(\d+)-(\d*)$/.exec((clientRange ?? '').trim());
|
|
36
|
+
if (m && m[2] !== '') {
|
|
37
|
+
const s = Number(m[1]);
|
|
38
|
+
const e = Number(m[2]);
|
|
39
|
+
if (e - s + 1 <= AUDIO_CHUNK)
|
|
40
|
+
return [s, e];
|
|
41
|
+
return clamp(s, s + AUDIO_CHUNK - 1);
|
|
42
|
+
}
|
|
43
|
+
const start = m ? Number(m[1]) : 0;
|
|
44
|
+
return clamp(start, start + (start === 0 ? FIRST_CHUNK : AUDIO_CHUNK) - 1);
|
|
151
45
|
}
|
|
152
|
-
function parseUpnext(panel, trustLike) {
|
|
153
|
-
const tracks = [];
|
|
154
|
-
const contents = panel.contents ?? [];
|
|
155
|
-
for (const item of contents) {
|
|
156
|
-
const track = upnextToTrack(item, trustLike);
|
|
157
|
-
if (track)
|
|
158
|
-
tracks.push(track);
|
|
159
|
-
if (tracks.length >= 50)
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
const playlistId = panel.playlist_id ?? null;
|
|
163
|
-
return { tracks, playlistId };
|
|
164
|
-
}
|
|
165
|
-
function asMenuItems(menu) {
|
|
166
|
-
if (!menu || typeof menu !== 'object')
|
|
167
|
-
return [];
|
|
168
|
-
const items = menu.items;
|
|
169
|
-
return Array.isArray(items) ? items : [];
|
|
170
|
-
}
|
|
171
|
-
function findLikeToggle(item, videoId) {
|
|
172
|
-
const result = { like: null, unlike: null };
|
|
173
|
-
const kind = nodeType(item);
|
|
174
|
-
const n = item;
|
|
175
|
-
const video = kind === 'PlaylistPanelVideoWrapper'
|
|
176
|
-
? (n['primary'] ?? null)
|
|
177
|
-
: n;
|
|
178
|
-
if (!video || video['video_id'] !== videoId)
|
|
179
|
-
return result;
|
|
180
|
-
const menus = [video['menu'], n['menu']];
|
|
181
|
-
for (const menu of menus) {
|
|
182
|
-
for (const entry of asMenuItems(menu)) {
|
|
183
|
-
if (nodeType(entry) !== 'ToggleMenuServiceItem')
|
|
184
|
-
continue;
|
|
185
|
-
const icon = entry['icon_type'];
|
|
186
|
-
if (icon !== 'FAVORITE' && icon !== 'UNFAVORITE')
|
|
187
|
-
continue;
|
|
188
|
-
for (const key of ['default_endpoint', 'toggled_endpoint']) {
|
|
189
|
-
const ep = entry[key];
|
|
190
|
-
if (typeof ep?.call !== 'function')
|
|
191
|
-
continue;
|
|
192
|
-
const status = ep.payload?.status;
|
|
193
|
-
if (status === 'LIKE' && !result.like)
|
|
194
|
-
result.like = ep;
|
|
195
|
-
else if (status === 'INDIFFERENT' && !result.unlike)
|
|
196
|
-
result.unlike = ep;
|
|
197
|
-
}
|
|
198
|
-
if (result.like && result.unlike)
|
|
199
|
-
return result;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return result;
|
|
203
|
-
}
|
|
204
|
-
async function fetchPlaylistTracks(yt, id, cap = 500) {
|
|
205
|
-
const tracks = [];
|
|
206
|
-
let page = (await yt.music.getPlaylist(id));
|
|
207
|
-
for (let fetches = 0; fetches < 6; fetches++) {
|
|
208
|
-
for (const item of (page.items ?? page.contents ?? []).slice(0, cap - tracks.length)) {
|
|
209
|
-
const track = toTrack(item);
|
|
210
|
-
if (track)
|
|
211
|
-
tracks.push(track);
|
|
212
|
-
if (tracks.length >= cap)
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
if (tracks.length >= cap)
|
|
216
|
-
break;
|
|
217
|
-
if (!page.has_continuation || typeof page.getContinuation !== 'function')
|
|
218
|
-
break;
|
|
219
|
-
try {
|
|
220
|
-
page = (await page.getContinuation());
|
|
221
|
-
}
|
|
222
|
-
catch (err) {
|
|
223
|
-
logger.warn({ err }, '[music] playlist continuation failed');
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return tracks;
|
|
228
|
-
}
|
|
229
|
-
async function readLibrary(yt) {
|
|
230
|
-
let lib;
|
|
231
|
-
try {
|
|
232
|
-
lib = (await yt.music.getLibrary());
|
|
233
|
-
}
|
|
234
|
-
catch (err) {
|
|
235
|
-
if (!isParserVariantError(err))
|
|
236
|
-
throw err;
|
|
237
|
-
logger.warn('[music] library landing hit a layout variant, falling back to Liked playlist');
|
|
238
|
-
try {
|
|
239
|
-
const tracks = await fetchPlaylistTracks(yt, 'LM');
|
|
240
|
-
return { tracks, playlists: [] };
|
|
241
|
-
}
|
|
242
|
-
catch (fallbackErr) {
|
|
243
|
-
logger.warn({ err: fallbackErr }, '[music] liked playlist fallback failed');
|
|
244
|
-
return { tracks: [], playlists: [] };
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const gridItems = [];
|
|
248
|
-
for (const section of lib.contents ?? []) {
|
|
249
|
-
const items = section.items ?? section.contents ?? [];
|
|
250
|
-
gridItems.push(...items);
|
|
251
|
-
}
|
|
252
|
-
const playlists = [];
|
|
253
|
-
const seen = new Set();
|
|
254
|
-
for (const item of gridItems) {
|
|
255
|
-
const n = item;
|
|
256
|
-
const id = (typeof n['playlist_id'] === 'string' && n['playlist_id']) ||
|
|
257
|
-
(typeof n['playlistId'] === 'string' && n['playlistId']) ||
|
|
258
|
-
(typeof n['id'] === 'string' && n['id']) ||
|
|
259
|
-
'';
|
|
260
|
-
const title = textOf(n['title']);
|
|
261
|
-
const subtitle = textOf(n['subtitle']);
|
|
262
|
-
if (!id || !title || seen.has(id))
|
|
263
|
-
continue;
|
|
264
|
-
if (!/playlist|mix/i.test(subtitle) && title !== 'Liked Music')
|
|
265
|
-
continue;
|
|
266
|
-
seen.add(id);
|
|
267
|
-
playlists.push({
|
|
268
|
-
id,
|
|
269
|
-
title,
|
|
270
|
-
subtitle: subtitle || undefined,
|
|
271
|
-
thumbnails: thumbsOf(n['thumbnails'] ?? n['thumbnail']),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
const tracks = [];
|
|
275
|
-
const liked = playlists.find((p) => p.title === 'Liked Music');
|
|
276
|
-
if (liked) {
|
|
277
|
-
try {
|
|
278
|
-
tracks.push(...(await fetchPlaylistTracks(yt, liked.id)));
|
|
279
|
-
}
|
|
280
|
-
catch (err) {
|
|
281
|
-
logger.warn({ err }, '[music] liked playlist fetch failed');
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
logger.info(`[music] library parsed tracks=${tracks.length} playlists=${playlists.length} gridItems=${gridItems.length}`);
|
|
285
|
-
return { tracks, playlists };
|
|
286
|
-
}
|
|
287
|
-
const UPSTREAM_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
|
|
288
|
-
const audioCache = new Map();
|
|
289
|
-
const AUDIO_CACHE_MS = 5 * 60 * 1000;
|
|
290
46
|
async function decipherWith(yt, videoId) {
|
|
291
47
|
const info = await yt.music.getInfo(videoId);
|
|
292
48
|
const streaming = info.streaming_data;
|
|
@@ -334,7 +90,6 @@ async function decipherAudio(videoId) {
|
|
|
334
90
|
}
|
|
335
91
|
return decipherWith(await getInnertube(), videoId);
|
|
336
92
|
}
|
|
337
|
-
const PASSTHROUGH_HEADERS = ['content-type', 'content-length', 'content-range', 'accept-ranges'];
|
|
338
93
|
export function registerMusic(app) {
|
|
339
94
|
app.get('/api/music/stream', async (c) => {
|
|
340
95
|
const videoId = String(c.req.query('videoId') || c.req.query('id') || '').trim();
|
|
@@ -353,61 +108,96 @@ export function registerMusic(app) {
|
|
|
353
108
|
const videoId = String(c.req.query('videoId') || c.req.query('id') || '').trim();
|
|
354
109
|
if (!videoId)
|
|
355
110
|
return c.json({ error: 'videoId is required' }, 400);
|
|
111
|
+
const signal = c.req.raw.signal.aborted
|
|
112
|
+
? c.req.raw.signal
|
|
113
|
+
: AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(30000)]);
|
|
356
114
|
try {
|
|
357
|
-
let
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
: AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(30000)]);
|
|
367
|
-
if (c.req.raw.signal.aborted)
|
|
368
|
-
return new Response(null, { status: 499 });
|
|
369
|
-
let upstream;
|
|
370
|
-
try {
|
|
371
|
-
upstream = await fetch(entry.url, {
|
|
372
|
-
headers: {
|
|
373
|
-
'User-Agent': UPSTREAM_UA,
|
|
374
|
-
Referer: 'https://music.youtube.com/',
|
|
375
|
-
Origin: 'https://music.youtube.com/',
|
|
376
|
-
Range: c.req.header('range') ?? 'bytes=0-',
|
|
377
|
-
},
|
|
378
|
-
signal,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
catch (err) {
|
|
382
|
-
if (c.req.raw.signal.aborted)
|
|
383
|
-
return new Response(null, { status: 499 });
|
|
384
|
-
logger.error({ err }, '[music] audio upstream failed');
|
|
385
|
-
return c.json({ error: 'Upstream error' }, 502);
|
|
386
|
-
}
|
|
387
|
-
if (upstream.status !== 200 && upstream.status !== 206) {
|
|
115
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
116
|
+
if (attempt === 1)
|
|
117
|
+
umpSessions.delete(videoId);
|
|
118
|
+
if (attempt === 2) {
|
|
119
|
+
audioCache.delete(videoId);
|
|
120
|
+
umpSessions.delete(videoId);
|
|
121
|
+
resetPoTokens(videoId);
|
|
122
|
+
}
|
|
123
|
+
let entry;
|
|
388
124
|
try {
|
|
389
|
-
await
|
|
125
|
+
entry = await decipherAudio(videoId);
|
|
390
126
|
}
|
|
391
127
|
catch {
|
|
392
|
-
|
|
128
|
+
return c.json({ error: 'No stream available' }, 502);
|
|
393
129
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
130
|
+
const clen = /[?&]clen=(\d+)/.exec(entry.url)?.[1];
|
|
131
|
+
if (clen && !audioTotalCache.has(videoId))
|
|
132
|
+
audioTotalCache.set(videoId, Number(clen));
|
|
133
|
+
const [rangeStart, rangeEnd] = boundWindow(c.req.header('range') ?? null, videoId);
|
|
134
|
+
const session = getUmpSession(videoId);
|
|
135
|
+
let pot = '';
|
|
136
|
+
try {
|
|
137
|
+
pot = await mintPoToken(videoId);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
logger.warn({ err }, '[music] PoToken mint failed, trying without attestation');
|
|
141
|
+
}
|
|
142
|
+
const upstreamUrl = `${entry.url}&cpn=${session.cpn}&cver=${UMP_CVER}` +
|
|
143
|
+
`&range=${rangeStart}-${rangeEnd}&rn=${session.rn}&rbuf=0` +
|
|
144
|
+
(pot ? `&pot=${encodeURIComponent(pot)}` : '') +
|
|
145
|
+
`&ump=1&srfvp=1`;
|
|
146
|
+
session.rn += 1;
|
|
147
|
+
let raw;
|
|
148
|
+
try {
|
|
149
|
+
const cookie = getMusicCookie();
|
|
150
|
+
const upstream = await fetch(upstreamUrl, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'User-Agent': UPSTREAM_UA,
|
|
154
|
+
Referer: 'https://music.youtube.com/',
|
|
155
|
+
Origin: 'https://music.youtube.com/',
|
|
156
|
+
...(cookie ? { Cookie: cookie } : {}),
|
|
157
|
+
},
|
|
158
|
+
body: 'x\x00',
|
|
159
|
+
signal,
|
|
160
|
+
});
|
|
161
|
+
if (upstream.status !== 200) {
|
|
162
|
+
try {
|
|
163
|
+
await upstream.body?.cancel();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// ignore
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
raw = Buffer.from(await upstream.arrayBuffer());
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (c.req.raw.signal.aborted)
|
|
174
|
+
return new Response(null, { status: 499 });
|
|
175
|
+
logger.error({ err }, '[music] audio upstream failed');
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const parsed = parseUmpResponse(raw, session.carry);
|
|
179
|
+
session.carry = parsed.carry;
|
|
180
|
+
if (parsed.status === 3) {
|
|
181
|
+
logger.warn({ videoId }, '[music] stream protection blocked, refreshing token');
|
|
182
|
+
resetPoTokens(videoId);
|
|
183
|
+
umpSessions.delete(videoId);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (parsed.media.length === 0)
|
|
187
|
+
continue;
|
|
188
|
+
const total = audioTotalCache.get(videoId);
|
|
189
|
+
const headers = new Headers();
|
|
404
190
|
headers.set('Content-Type', entry.mimeType || 'audio/webm');
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
191
|
+
headers.set('Content-Length', String(parsed.media.length));
|
|
192
|
+
if (total) {
|
|
193
|
+
headers.set('Content-Range', `bytes ${rangeStart}-${rangeStart + parsed.media.length - 1}/${total}`);
|
|
194
|
+
}
|
|
195
|
+
headers.set('Accept-Ranges', 'bytes');
|
|
196
|
+
headers.set('Access-Control-Allow-Origin', '*');
|
|
197
|
+
return new Response(parsed.media, { status: 206, headers });
|
|
409
198
|
}
|
|
410
|
-
|
|
199
|
+
logger.warn({ videoId }, '[music] audio upstream gave no media after retries');
|
|
200
|
+
return c.json({ error: 'Upstream error' }, 502);
|
|
411
201
|
}
|
|
412
202
|
catch (err) {
|
|
413
203
|
if (c.req.raw.signal.aborted)
|