@serifpersia/dango 3.4.3 → 3.4.5
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-Chlfp7Cm.js → AnimeCard-I3S6qw9d.js} +1 -1
- package/client/dist/assets/{AnimeInfo-C1XZcEzj.js → AnimeInfo-CLU2NSBR.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-BvNYYbe1.js → AnimeInfoPage-4Mw9ArDy.js} +2 -2
- package/client/dist/assets/{Asmr-_jVaYDfT.js → Asmr-DN9XBc5Y.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-ngn9hdA0.js → AsmrResetProgressModal-DyU1Z2Gw.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-Rcv7aubm.js → AvSyncCalibrator-DxL0mr_4.js} +1 -1
- package/client/dist/assets/{Home-CEbLBbmD.js → Home-CeTeJ-gM.js} +1 -1
- package/client/dist/assets/{Insights-Bia4rfp0.js → Insights-i34-qV4C.js} +1 -1
- package/client/dist/assets/{LibraryListPage-BVHcMPqZ.js → LibraryListPage-DE-ASOX2.js} +1 -1
- package/client/dist/assets/{ListeningList-DpRNQKk7.js → ListeningList-DrNce5ll.js} +1 -1
- package/client/dist/assets/{Manga-wHddjSTh.js → Manga-DOrf7Fk6.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-CVz8z5wn.js → MangaInfoPage-CAZMLMGw.js} +1 -1
- package/client/dist/assets/{MangaReadPage-CC3cgtIZ.js → MangaReadPage-DQVEo8_F.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-DD7a0log.js → MangaResetProgressModal-DOjpLAMp.js} +1 -1
- package/client/dist/assets/{Mature-D8uFpIDj.js → Mature-D2xb9EGB.js} +1 -1
- package/client/dist/assets/{MediaCard-6Yy_-Whn.js → MediaCard-CKUEcFTd.js} +1 -1
- package/client/dist/assets/{Music-BgM0bcAU.js → Music-w_a34r7r.js} +1 -1
- package/client/dist/assets/{OptionListMenu-C9saW22f.js → OptionListMenu-DxZHBMIZ.js} +1 -1
- package/client/dist/assets/{Player-EwAWFxqQ.js → Player-B8YHeATv.js} +2 -2
- package/client/dist/assets/{PlayerSettings-BYqhlu3F.js → PlayerSettings-Ddlg-MvW.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-BJhkcl2X.js → QueueOptionsButton-BMvAC6Ut.js} +1 -1
- package/client/dist/assets/{QueueRail-BLfkt_s5.js → QueueRail-CAoGJvHP.js} +1 -1
- package/client/dist/assets/{Radio-D2Er2JEU.js → Radio-6O7rAUv1.js} +1 -1
- package/client/dist/assets/{ReadingList-DWOnwYO1.js → ReadingList-k3g8oHvs.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-HUj5zATK.js → ResetProgressModal-vFC6N-Qw.js} +1 -1
- package/client/dist/assets/{Search-CzD9J9ip.js → Search-Bh9Y6nIJ.js} +1 -1
- package/client/dist/assets/{Settings-BgUothvZ.js → Settings-DRumujfh.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-C7Gj9ODL.js → SkeletonGrid-BtzUnQE0.js} +1 -1
- package/client/dist/assets/{Trackers-BoC2z0Nd.js → Trackers-D0nqy8wu.js} +1 -1
- package/client/dist/assets/{Tv-Q8TIMWaP.js → Tv-DVebLMNS.js} +1 -1
- package/client/dist/assets/{TvInfo-C6JoNbev.js → TvInfo-BkECbOKN.js} +1 -1
- package/client/dist/assets/{TvPopup-BArrygql.js → TvPopup-D8WWY6o4.js} +1 -1
- package/client/dist/assets/{TvSearch-BsoJ7WZp.js → TvSearch-kWPep3gL.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BFmAd2fE.js → TvWatchlist-dD4emIHx.js} +1 -1
- package/client/dist/assets/{Watchlist-DG5Y--YA.js → Watchlist-cWxw38nU.js} +1 -1
- package/client/dist/assets/{hls-QeZPjik8.js → hls-uG_Y3i5s.js} +1 -1
- package/client/dist/assets/{index-Rm4h42Uz.js → index-BWIWE3Qy.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-B9O3Rr_u.js → useAnimeInfoData-DjPBg24u.js} +1 -1
- package/client/dist/assets/{useAsmr-Da7AVA9e.js → useAsmr-DQyobNQX.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-B_S5s4q3.js → useAsmrPopup-CDoroMBD.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-COpztbMg.js → useMangaLibrary-C6L8mrnS.js} +1 -1
- package/client/dist/assets/{useMangaPopup-BJeHmEpe.js → useMangaPopup-BSuMa_vA.js} +1 -1
- package/client/dist/assets/{useProviders-BWRYjB77.js → useProviders-XIdvDof6.js} +1 -1
- package/client/dist/assets/{useTv-DtiSJ756.js → useTv-DzSYasru.js} +1 -1
- package/client/dist/assets/{useTvLibrary-DFfgk9YS.js → useTvLibrary-BnbHS-gg.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 +3 -24
- package/server/dist/hono/tv.js +3 -56
- package/server/dist/lib/mal.js +4 -3
- 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 +2 -1
- 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 -52
- package/server/dist/repositories/settings.repository.js +8 -0
- package/server/dist/repositories/tv.repository.js +3 -46
- 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/package.json +4 -1
|
@@ -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)
|
|
@@ -6,7 +6,7 @@ import fs from 'fs';
|
|
|
6
6
|
import logger from '../logger.js';
|
|
7
7
|
import { buildCfClearanceCookie, sanitizeCfClearance } from '../utils/cookie.utils.js';
|
|
8
8
|
import { isSafeExternalUrl } from '../utils/security.utils.js';
|
|
9
|
-
import { fetchWithRetry } from '../utils/http.utils.js';
|
|
9
|
+
import { fetchWithRetry, linkAbort, setProxyHeaders } from '../utils/http.utils.js';
|
|
10
10
|
import { MEGAPLAY_ORIGIN, isNexabloomMasterUrl, signNexabloomMasterUrl, } from '../utils/megaplay.utils.js';
|
|
11
11
|
const proxyCache = new AppCache({ ttlSeconds: 30, maxKeys: 500 });
|
|
12
12
|
function assTimeToVtt(value) {
|
|
@@ -299,9 +299,6 @@ function placeholderSvg() {
|
|
|
299
299
|
return null;
|
|
300
300
|
}
|
|
301
301
|
const PROXY_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0';
|
|
302
|
-
function linkAbort(signal, abort) {
|
|
303
|
-
signal.addEventListener('abort', () => abort.abort(), { once: true });
|
|
304
|
-
}
|
|
305
302
|
export function registerProxy(app) {
|
|
306
303
|
app.get('/api/proxy', async (c) => {
|
|
307
304
|
const url = c.req.query('url');
|
|
@@ -463,16 +460,7 @@ export function registerProxy(app) {
|
|
|
463
460
|
const isVtt = urlStr.endsWith('.vtt') || urlStr.includes('.vtt?');
|
|
464
461
|
const outHeaders = new Headers();
|
|
465
462
|
outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
|
|
466
|
-
|
|
467
|
-
if (cl)
|
|
468
|
-
outHeaders.set('Content-Length', cl);
|
|
469
|
-
const cr = resp.headers['content-range'];
|
|
470
|
-
if (cr)
|
|
471
|
-
outHeaders.set('Content-Range', cr);
|
|
472
|
-
const ar = resp.headers['accept-ranges'];
|
|
473
|
-
if (ar)
|
|
474
|
-
outHeaders.set('Accept-Ranges', ar);
|
|
475
|
-
outHeaders.set('Access-Control-Allow-Origin', '*');
|
|
463
|
+
setProxyHeaders(outHeaders, (name) => resp.headers[name]);
|
|
476
464
|
return new Response(resp.body, {
|
|
477
465
|
status: resp.statusCode,
|
|
478
466
|
headers: outHeaders,
|
|
@@ -516,16 +504,7 @@ export function registerProxy(app) {
|
|
|
516
504
|
const ct = upstream.headers.get('content-type');
|
|
517
505
|
const outHeaders = new Headers();
|
|
518
506
|
outHeaders.set('Content-Type', isVtt ? 'text/vtt; charset=utf-8' : ct || 'application/octet-stream');
|
|
519
|
-
|
|
520
|
-
if (cl)
|
|
521
|
-
outHeaders.set('Content-Length', cl);
|
|
522
|
-
const cr = upstream.headers.get('content-range');
|
|
523
|
-
if (cr)
|
|
524
|
-
outHeaders.set('Content-Range', cr);
|
|
525
|
-
const ar = upstream.headers.get('accept-ranges');
|
|
526
|
-
if (ar)
|
|
527
|
-
outHeaders.set('Accept-Ranges', ar);
|
|
528
|
-
outHeaders.set('Access-Control-Allow-Origin', '*');
|
|
507
|
+
setProxyHeaders(outHeaders, (name) => upstream.headers.get(name));
|
|
529
508
|
if (!upstream.body) {
|
|
530
509
|
return c.text('Upstream error', 502);
|
|
531
510
|
}
|
package/server/dist/hono/tv.js
CHANGED
|
@@ -1,34 +1,7 @@
|
|
|
1
1
|
import logger from '../logger.js';
|
|
2
|
-
import {
|
|
3
|
-
import { getTmdbKey, TMDB_BASE, TMDB_IMAGE } from '../lib/tmdb.js';
|
|
2
|
+
import { linkAbort, setProxyHeaders } from '../utils/http.utils.js';
|
|
4
3
|
import { URL } from 'url';
|
|
5
4
|
import { isSafeExternalUrl } from '../utils/security.utils.js';
|
|
6
|
-
async function resolveTvMeta(mediaType, numericTmdbId, partial) {
|
|
7
|
-
const meta = { ...partial };
|
|
8
|
-
if (meta.title && meta.imdbId && meta.year)
|
|
9
|
-
return meta;
|
|
10
|
-
try {
|
|
11
|
-
const tmdbKey = await getTmdbKey();
|
|
12
|
-
if (tmdbKey) {
|
|
13
|
-
const tmdbRes = await fetch(`${TMDB_BASE}/${mediaType}/${numericTmdbId}?api_key=${tmdbKey}&append_to_response=external_ids`, { headers: { 'User-Agent': 'Mozilla/5.0' }, signal: AbortSignal.timeout(4000) });
|
|
14
|
-
if (tmdbRes.ok) {
|
|
15
|
-
const d = await parseJsonBody(tmdbRes);
|
|
16
|
-
if (!meta.title)
|
|
17
|
-
meta.title = d.title || d.name || '';
|
|
18
|
-
if (!meta.year)
|
|
19
|
-
meta.year = (d.release_date || d.first_air_date || '').split('-')[0] || '';
|
|
20
|
-
if (!meta.imdbId)
|
|
21
|
-
meta.imdbId = d.external_ids?.imdb_id || d.imdb_id || '';
|
|
22
|
-
if (mediaType === 'tv' && d.number_of_seasons)
|
|
23
|
-
meta.totalSeasons = String(d.number_of_seasons);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// ignore
|
|
29
|
-
}
|
|
30
|
-
return meta;
|
|
31
|
-
}
|
|
32
5
|
function proxiedMediaUrl(targetUrl, refererStr) {
|
|
33
6
|
return `/api/tv/stream-proxy?url=${encodeURIComponent(targetUrl)}&referer=${encodeURIComponent(refererStr)}`;
|
|
34
7
|
}
|
|
@@ -59,20 +32,6 @@ export function isPlaylistBody(body) {
|
|
|
59
32
|
.trimStart()
|
|
60
33
|
.startsWith('#EXTM3U'));
|
|
61
34
|
}
|
|
62
|
-
function toSearchResult(item, type) {
|
|
63
|
-
return {
|
|
64
|
-
id: item.id,
|
|
65
|
-
title: item.title || item.name || '',
|
|
66
|
-
year: (item.release_date || item.first_air_date || '').split('-')[0],
|
|
67
|
-
type,
|
|
68
|
-
image: item.poster_path ? `${TMDB_IMAGE}/w500${item.poster_path}` : '',
|
|
69
|
-
backdrop: item.backdrop_path ? `${TMDB_IMAGE}/w780${item.backdrop_path}` : '',
|
|
70
|
-
overview: item.overview || '',
|
|
71
|
-
vote_average: item.vote_average || 0,
|
|
72
|
-
adult: item.adult === true,
|
|
73
|
-
genre_ids: item.genre_ids || [],
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
35
|
export function registerTv(app, getApiCache, getTvProvider) {
|
|
77
36
|
app.get('/api/tv/embed/:provider/:type/:tmdbId', async (c) => {
|
|
78
37
|
const provider = c.req.param('provider');
|
|
@@ -117,10 +76,7 @@ export function registerTv(app, getApiCache, getTvProvider) {
|
|
|
117
76
|
}
|
|
118
77
|
const abort = new AbortController();
|
|
119
78
|
const timeout = setTimeout(() => abort.abort(), 30000);
|
|
120
|
-
c.req.raw.signal
|
|
121
|
-
clearTimeout(timeout);
|
|
122
|
-
abort.abort();
|
|
123
|
-
}, { once: true });
|
|
79
|
+
linkAbort(c.req.raw.signal, abort);
|
|
124
80
|
try {
|
|
125
81
|
const headers = {
|
|
126
82
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
@@ -137,18 +93,9 @@ export function registerTv(app, getApiCache, getTvProvider) {
|
|
|
137
93
|
return c.text('Upstream error', (status || 502));
|
|
138
94
|
}
|
|
139
95
|
const contentType = fetchResp.headers.get('content-type') || 'application/octet-stream';
|
|
140
|
-
const contentLength = fetchResp.headers.get('content-length');
|
|
141
|
-
const contentRange = fetchResp.headers.get('content-range');
|
|
142
|
-
const acceptRanges = fetchResp.headers.get('accept-ranges');
|
|
143
96
|
const outHeaders = new Headers();
|
|
144
97
|
outHeaders.set('Content-Type', contentType);
|
|
145
|
-
|
|
146
|
-
outHeaders.set('Content-Length', contentLength);
|
|
147
|
-
if (contentRange)
|
|
148
|
-
outHeaders.set('Content-Range', contentRange);
|
|
149
|
-
if (acceptRanges)
|
|
150
|
-
outHeaders.set('Accept-Ranges', acceptRanges);
|
|
151
|
-
outHeaders.set('Access-Control-Allow-Origin', '*');
|
|
98
|
+
setProxyHeaders(outHeaders, (name) => fetchResp.headers.get(name));
|
|
152
99
|
outHeaders.set('Connection', 'keep-alive');
|
|
153
100
|
if (urlStr.includes('.m3u8') || /mpegurl|m3u8/i.test(contentType)) {
|
|
154
101
|
const body = await fetchResp.text();
|
package/server/dist/lib/mal.js
CHANGED
|
@@ -603,9 +603,10 @@ export async function enrichCardTitles(store, items) {
|
|
|
603
603
|
export async function malSearchMedia(store, options = {}, enrichTitles = true) {
|
|
604
604
|
const { query, page = 1, format, status, season, seasonYear, genre, genre_in, genre_not_in, averageScore_greater, episodes_greater, isAdult, sort, } = options;
|
|
605
605
|
const params = new URLSearchParams();
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
606
|
+
const trimmedQuery = query?.trim();
|
|
607
|
+
const hasQuery = !!trimmedQuery;
|
|
608
|
+
if (trimmedQuery)
|
|
609
|
+
params.set('q', trimmedQuery);
|
|
609
610
|
params.set('cat', 'anime');
|
|
610
611
|
if (page > 1)
|
|
611
612
|
params.set('show', String((page - 1) * 50));
|