@serifpersia/dango 3.3.8 → 3.4.0
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-D4qo4289.js → AnimeCard-CJy1SXH3.js} +1 -1
- package/client/dist/assets/{AnimeInfo-BdRRm4So.js → AnimeInfo-BoXlVvZ-.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DeJNAKMR.js → AnimeInfoPage-BxbA_7AV.js} +2 -2
- package/client/dist/assets/Asmr--Zc8G9zv.js +2 -0
- package/client/dist/assets/AsmrResetProgressModal-CKYm-8cp.js +1 -0
- package/client/dist/assets/AvSyncCalibrator-3yjZS7s6.js +10 -0
- package/client/dist/assets/Home-rrhOGQcg.js +1 -0
- package/client/dist/assets/Insights-Bay90Yeg.js +1 -0
- package/client/dist/assets/{LibraryListPage-D3hTqTRU.js → LibraryListPage-HzuDavbb.js} +1 -1
- package/client/dist/assets/{ListeningList-BgWsfu40.js → ListeningList-DYcQnnsG.js} +1 -1
- package/client/dist/assets/{Manga-Dn-CYins.js → Manga-D8CT9aO0.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-EmQoK7Bv.js → MangaInfoPage-DGK6CYp1.js} +1 -1
- package/client/dist/assets/MangaReadPage-B9XoDKF9.js +1 -0
- package/client/dist/assets/{MangaResetProgressModal-Dian37PQ.js → MangaResetProgressModal-QjZyIAjl.js} +1 -1
- package/client/dist/assets/Mature-CHt_-Hz5.js +1 -0
- package/client/dist/assets/MediaCard-B5584Cq9.js +1 -0
- package/client/dist/assets/Music-HeLrEZ2d.js +1 -0
- package/client/dist/assets/OptionListMenu-CJ90CdwI.js +1 -0
- package/client/dist/assets/Player-BYtEax_z.js +56 -0
- package/client/dist/assets/PlayerSettings-4-cUAY4B.js +1 -0
- package/client/dist/assets/QueueOptionsButton-Ba0dg_82.js +1 -0
- package/client/dist/assets/{QueueRail-B7DeSFoA.js → QueueRail-O7f4RqjB.js} +1 -1
- package/client/dist/assets/Radio-DPZxQnyD.js +1 -0
- package/client/dist/assets/{ReadingList-BT8kUjCB.js → ReadingList-n6rr8ygy.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-DWeiO-hs.js → ResetProgressModal-CcStlbhP.js} +1 -1
- package/client/dist/assets/Search-C2wrWOdy.js +1 -0
- package/client/dist/assets/Settings-CluUYs8U.js +21 -0
- package/client/dist/assets/{SkeletonGrid-B5loz7ii.js → SkeletonGrid-CLEPY9nP.js} +1 -1
- package/client/dist/assets/Trackers-CTGPxs0V.js +2 -0
- package/client/dist/assets/Tv-BwHlDdi8.js +4 -0
- package/client/dist/assets/Tv-ZEA45gXK.css +1 -0
- package/client/dist/assets/TvInfo-CXnrLekr.js +1 -0
- package/client/dist/assets/TvPopup-BekfiNcX.js +1 -0
- package/client/dist/assets/{TvSearch-BYwCf5kO.js → TvSearch-DuqR-z3v.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BiJU6pKT.js → TvWatchlist-CBvgFrA0.js} +1 -1
- package/client/dist/assets/Watchlist-mj8mTOam.js +1 -0
- package/client/dist/assets/{hls-C11FOWHj.js → hls-Boje3IOE.js} +1 -1
- package/client/dist/assets/index-BXwhFWB_.js +178 -0
- package/client/dist/assets/useAnimeInfoData-DJ7A4B4T.js +1 -0
- package/client/dist/assets/useAsmr-DIwIsJXF.js +1 -0
- package/client/dist/assets/{useAsmrPopup-cT_Uu41e.js → useAsmrPopup-e4zNN9ig.js} +1 -1
- package/client/dist/assets/useMangaLibrary-C9Lci5N4.js +1 -0
- package/client/dist/assets/{useMangaPopup-PnmSctHu.js → useMangaPopup-B8nnpvyi.js} +1 -1
- package/client/dist/assets/useProviders-B5Qr4I1y.js +1 -0
- package/client/dist/assets/useTv-Dm8MCYlL.js +1 -0
- package/client/dist/assets/useTvLibrary-B7jEk2Q7.js +1 -0
- package/client/dist/assets/{vendor-query-BXRECSI6.js → vendor-query-BUTye89G.js} +1 -1
- package/client/dist/index.html +2 -2
- package/orchestrator.js +1 -2
- package/package.json +2 -6
- package/server/dist/app-auth.js +9 -29
- package/server/dist/app-hono.js +621 -0
- package/server/dist/config.js +0 -1
- package/server/dist/db/drizzle.js +32 -0
- package/server/dist/db/schema-anime.js +64 -0
- package/server/dist/db/schema-asmr.js +25 -0
- package/server/dist/db/schema-manga.js +31 -0
- package/server/dist/db/schema-tv.js +33 -0
- package/server/dist/db.js +75 -4
- package/server/dist/discord-gateway.js +1 -1
- package/server/dist/google.js +0 -7
- package/server/dist/hono/asmr.js +1 -0
- package/server/dist/hono/auth.js +56 -0
- package/server/dist/hono/music.js +419 -0
- package/server/dist/{controllers/proxy.controller.js → hono/proxy.js} +390 -394
- package/server/dist/hono/tv.js +197 -0
- package/server/dist/hono/watchlist.js +733 -0
- package/server/dist/lib/anilist.js +3 -3
- package/server/dist/lib/insights.js +185 -0
- package/server/dist/lib/ipc.js +1 -2
- package/server/dist/lib/kitsu.js +1 -28
- package/server/dist/lib/mal-import.js +291 -0
- package/server/dist/lib/migration.js +20 -34
- package/server/dist/lib/offline-db.js +36 -36
- package/server/dist/lib/tracker/manga-sync.service.js +21 -21
- package/server/dist/lib/tracker/sync.service.js +20 -20
- package/server/dist/lib/translate.js +79 -0
- package/server/dist/lib/ytmusic.js +15 -4
- package/server/dist/providers/title-matching.js +0 -1
- package/server/dist/repositories/asmr.repository.js +90 -87
- package/server/dist/repositories/insights.repository.js +24 -13
- package/server/dist/repositories/library.repository.js +33 -93
- package/server/dist/repositories/manga.repository.js +108 -116
- package/server/dist/repositories/notifications.repository.js +50 -31
- package/server/dist/repositories/queue.repository.js +36 -25
- package/server/dist/repositories/settings.repository.js +18 -8
- package/server/dist/repositories/shows-meta.repository.js +54 -41
- package/server/dist/repositories/temp-show-ids.repository.js +43 -31
- package/server/dist/repositories/tv.repository.js +98 -115
- package/server/dist/repositories/watched-episodes.repository.js +52 -20
- package/server/dist/repositories/watchlist.repository.js +36 -48
- package/server/dist/server.js +59 -151
- package/server/dist/sync.js +41 -4
- package/server/dist/trpc/context.js +7 -0
- package/server/dist/trpc/errors.js +21 -0
- package/server/dist/trpc/index.js +19 -0
- package/server/dist/trpc/router.js +49 -0
- package/server/dist/trpc/routers/asmr-library.js +122 -0
- package/server/dist/trpc/routers/asmr-progress.js +133 -0
- package/server/dist/trpc/routers/asmr.js +87 -0
- package/server/dist/trpc/routers/auth.js +247 -0
- package/server/dist/trpc/routers/continue-watching.js +83 -0
- package/server/dist/trpc/routers/data.js +1039 -0
- package/server/dist/trpc/routers/discord.js +288 -0
- package/server/dist/trpc/routers/discovery.js +15 -0
- package/server/dist/trpc/routers/insights.js +45 -0
- package/server/dist/trpc/routers/manga-library.js +280 -0
- package/server/dist/trpc/routers/manga-progress.js +181 -0
- package/server/dist/trpc/routers/manga.js +201 -0
- package/server/dist/{controllers/music.controller.js → trpc/routers/music.js} +235 -295
- package/server/dist/trpc/routers/notifications.js +80 -0
- package/server/dist/trpc/routers/progress.js +15 -0
- package/server/dist/trpc/routers/queue.js +95 -0
- package/server/dist/{routes/radio.routes.js → trpc/routers/radio.js} +30 -23
- package/server/dist/trpc/routers/settings.js +105 -0
- package/server/dist/trpc/routers/tracker.js +219 -0
- package/server/dist/trpc/routers/tv-library.js +145 -0
- package/server/dist/trpc/routers/tv-progress.js +171 -0
- package/server/dist/trpc/routers/tv.js +565 -0
- package/server/dist/trpc/routers/watchlist.js +564 -0
- package/server/dist/trpc/validation.js +159 -0
- package/server/dist/utils/security.utils.js +0 -18
- package/server/package.json +5 -10
- package/client/dist/assets/Asmr-D0EGRYsv.js +0 -2
- package/client/dist/assets/AsmrResetProgressModal-Gs0GmVaT.js +0 -1
- package/client/dist/assets/AvSyncCalibrator-D8hqarVB.js +0 -10
- package/client/dist/assets/Home-BnBzjFkW.js +0 -1
- package/client/dist/assets/Insights-i9QgY3nE.js +0 -1
- package/client/dist/assets/MangaReadPage-xJB8K2YU.js +0 -1
- package/client/dist/assets/Mature-DQtXKOXc.js +0 -1
- package/client/dist/assets/MediaCard-DpKoY-1j.js +0 -1
- package/client/dist/assets/Music-BP43Zqmq.js +0 -1
- package/client/dist/assets/OptionListMenu-Bj9ebsW3.js +0 -1
- package/client/dist/assets/Player-O2Cy2GF1.js +0 -56
- package/client/dist/assets/PlayerSettings-DYZ272Yp.js +0 -1
- package/client/dist/assets/QueueOptionsButton-lv26-6jp.js +0 -1
- package/client/dist/assets/Radio-qQBYnVfY.js +0 -1
- package/client/dist/assets/Search-BkvPYN33.js +0 -1
- package/client/dist/assets/Settings-CyZEBF6S.js +0 -21
- package/client/dist/assets/Trackers-JK-aXBYC.js +0 -2
- package/client/dist/assets/Tv-BwXWJxMy.js +0 -4
- package/client/dist/assets/Tv-C2SWcVzN.css +0 -1
- package/client/dist/assets/TvInfo-Dzyq6ucH.js +0 -1
- package/client/dist/assets/TvPopup-PlCU30FO.js +0 -1
- package/client/dist/assets/Watchlist-BOEeGXH9.js +0 -1
- package/client/dist/assets/index-CtoKRNLc.js +0 -178
- package/client/dist/assets/useAnimeInfoData-kr3WOIWH.js +0 -1
- package/client/dist/assets/useAsmr-CB9THf0m.js +0 -1
- package/client/dist/assets/useMangaLibrary-rM6z3_0E.js +0 -1
- package/client/dist/assets/useProviders-B3VB7A9M.js +0 -1
- package/client/dist/assets/useTv-BTUKMH7Q.js +0 -1
- package/client/dist/assets/useTvLibrary-C0sX-ilZ.js +0 -1
- package/server/dist/controllers/asmr-library.controller.js +0 -288
- package/server/dist/controllers/auth.controller.js +0 -225
- package/server/dist/controllers/data.controller.js +0 -767
- package/server/dist/controllers/insights.controller.js +0 -203
- package/server/dist/controllers/manga-library.controller.js +0 -350
- package/server/dist/controllers/settings.controller.js +0 -609
- package/server/dist/controllers/tv-library.controller.js +0 -314
- package/server/dist/controllers/watchlist.controller.js +0 -1069
- package/server/dist/request-logger.js +0 -16
- package/server/dist/routes/asmr-library.routes.js +0 -23
- package/server/dist/routes/asmr.routes.js +0 -81
- package/server/dist/routes/auth.routes.js +0 -25
- package/server/dist/routes/data.routes.js +0 -47
- package/server/dist/routes/discord-gateway.routes.js +0 -51
- package/server/dist/routes/insights.routes.js +0 -35
- package/server/dist/routes/lan-auth.routes.js +0 -46
- package/server/dist/routes/manga-library.routes.js +0 -25
- package/server/dist/routes/manga.routes.js +0 -141
- package/server/dist/routes/music.routes.js +0 -20
- package/server/dist/routes/providers.routes.js +0 -17
- package/server/dist/routes/proxy.routes.js +0 -12
- package/server/dist/routes/settings.routes.js +0 -24
- package/server/dist/routes/tracker.routes.js +0 -287
- package/server/dist/routes/translate.routes.js +0 -88
- package/server/dist/routes/tv-library.routes.js +0 -23
- package/server/dist/routes/tv.routes.js +0 -629
- package/server/dist/routes/watchlist.routes.js +0 -341
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { AppCache } from '../utils/cache.utils.js';
|
|
2
|
+
import logger from '../logger.js';
|
|
3
|
+
import { getInnertube, getAuthedInnertube, refreshAuthedInnertube, isParserVariantError, } from '../lib/ytmusic.js';
|
|
4
|
+
const musicCache = new AppCache({ ttlSeconds: 3600, maxKeys: 5000 });
|
|
5
|
+
const NAME_SUFFIXES = [
|
|
6
|
+
/\s*(- topic)$/i,
|
|
7
|
+
/\s*vevo$/i,
|
|
8
|
+
/\s*[(|[]official(.*?)[)|\]]/i,
|
|
9
|
+
/\s*[(|[]((lyrics?|visualizer|audio)\s*(video)?)[)|\]]/i,
|
|
10
|
+
/\s*[(|[](performance video)[)|\]]/i,
|
|
11
|
+
/\s*[(|[](clip official)[)|\]]/i,
|
|
12
|
+
/\s*[(|[](video version)[)|\]]/i,
|
|
13
|
+
/\s*[(|[](HD|HQ)\s*?(?:audio)?[)|\]]$/i,
|
|
14
|
+
/\s*[(|[](live)[)|\]]$/i,
|
|
15
|
+
/\s*[(|[]4K\s*?(?:upgrade)?[)|\]]$/i,
|
|
16
|
+
];
|
|
17
|
+
export function cleanupMusicName(name) {
|
|
18
|
+
if (!name)
|
|
19
|
+
return name;
|
|
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 '';
|
|
77
|
+
}
|
|
78
|
+
function thumbsOf(value) {
|
|
79
|
+
if (!value)
|
|
80
|
+
return [];
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
return value
|
|
83
|
+
.filter((t) => t && typeof t.url === 'string')
|
|
84
|
+
.map((t) => ({ url: t.url, width: t.width, height: t.height }));
|
|
85
|
+
}
|
|
86
|
+
const obj = value;
|
|
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 ?? '';
|
|
96
|
+
}
|
|
97
|
+
function upnextToTrack(node, trustLike = false) {
|
|
98
|
+
const kind = nodeType(node);
|
|
99
|
+
if (kind === 'AutomixPreviewVideo')
|
|
100
|
+
return null;
|
|
101
|
+
const n = node;
|
|
102
|
+
const video = kind === 'PlaylistPanelVideoWrapper'
|
|
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 } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
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
|
+
async function decipherWith(yt, videoId) {
|
|
291
|
+
const info = await yt.music.getInfo(videoId);
|
|
292
|
+
const streaming = info.streaming_data;
|
|
293
|
+
const formats = streaming?.adaptive_formats ?? [];
|
|
294
|
+
const sorted = [...formats]
|
|
295
|
+
.filter((f) => f.has_audio && !f.has_video)
|
|
296
|
+
.sort((a, b) => (b.bitrate ?? 0) - (a.bitrate ?? 0));
|
|
297
|
+
const player = yt.session.player;
|
|
298
|
+
for (const fmt of sorted) {
|
|
299
|
+
try {
|
|
300
|
+
const url = await fmt.decipher(player);
|
|
301
|
+
if (url && url.startsWith('http')) {
|
|
302
|
+
const entry = { url, mimeType: fmt.mime_type, fetchedAt: Date.now() };
|
|
303
|
+
audioCache.set(videoId, entry);
|
|
304
|
+
return entry;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// ignore
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
throw new Error('No stream available');
|
|
312
|
+
}
|
|
313
|
+
async function decipherAudio(videoId) {
|
|
314
|
+
const cached = audioCache.get(videoId);
|
|
315
|
+
if (cached && Date.now() - cached.fetchedAt < AUDIO_CACHE_MS)
|
|
316
|
+
return cached;
|
|
317
|
+
const authed = await getAuthedInnertube().catch(() => null);
|
|
318
|
+
if (authed) {
|
|
319
|
+
try {
|
|
320
|
+
return await decipherWith(authed, videoId);
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
logger.warn({ err }, '[music] authed stream lookup failed, retrying with a fresh saved-cookie session');
|
|
324
|
+
const fresh = await refreshAuthedInnertube();
|
|
325
|
+
if (fresh) {
|
|
326
|
+
try {
|
|
327
|
+
return await decipherWith(fresh, videoId);
|
|
328
|
+
}
|
|
329
|
+
catch (retryErr) {
|
|
330
|
+
logger.warn({ err: retryErr }, '[music] refreshed stream lookup failed, falling back to public session');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return decipherWith(await getInnertube(), videoId);
|
|
336
|
+
}
|
|
337
|
+
const PASSTHROUGH_HEADERS = ['content-type', 'content-length', 'content-range', 'accept-ranges'];
|
|
338
|
+
export function registerMusic(app) {
|
|
339
|
+
app.get('/api/music/stream', async (c) => {
|
|
340
|
+
const videoId = String(c.req.query('videoId') || c.req.query('id') || '').trim();
|
|
341
|
+
if (!videoId)
|
|
342
|
+
return c.json({ error: 'videoId is required' }, 400);
|
|
343
|
+
try {
|
|
344
|
+
const entry = await decipherAudio(videoId);
|
|
345
|
+
return c.json({ url: entry.url, mimeType: entry.mimeType });
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
logger.error({ err }, '[music] stream failed');
|
|
349
|
+
return c.json({ error: 'Stream lookup failed' }, 502);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
app.get('/api/music/audio', async (c) => {
|
|
353
|
+
const videoId = String(c.req.query('videoId') || c.req.query('id') || '').trim();
|
|
354
|
+
if (!videoId)
|
|
355
|
+
return c.json({ error: 'videoId is required' }, 400);
|
|
356
|
+
try {
|
|
357
|
+
let entry;
|
|
358
|
+
try {
|
|
359
|
+
entry = await decipherAudio(videoId);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return c.json({ error: 'No stream available' }, 502);
|
|
363
|
+
}
|
|
364
|
+
const signal = c.req.raw.signal.aborted
|
|
365
|
+
? c.req.raw.signal
|
|
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) {
|
|
388
|
+
try {
|
|
389
|
+
await upstream.body?.cancel();
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
// ignore
|
|
393
|
+
}
|
|
394
|
+
audioCache.delete(videoId);
|
|
395
|
+
return c.json({ error: 'Upstream error' }, 502);
|
|
396
|
+
}
|
|
397
|
+
const headers = new Headers();
|
|
398
|
+
for (const name of PASSTHROUGH_HEADERS) {
|
|
399
|
+
const value = upstream.headers.get(name);
|
|
400
|
+
if (value)
|
|
401
|
+
headers.set(name === 'content-type' ? 'Content-Type' : name, value);
|
|
402
|
+
}
|
|
403
|
+
if (!headers.get('Content-Type'))
|
|
404
|
+
headers.set('Content-Type', entry.mimeType || 'audio/webm');
|
|
405
|
+
headers.set('Accept-Ranges', 'bytes');
|
|
406
|
+
headers.set('Access-Control-Allow-Origin', '*');
|
|
407
|
+
if (!upstream.body) {
|
|
408
|
+
return c.json({ error: 'Upstream error' }, 502);
|
|
409
|
+
}
|
|
410
|
+
return new Response(upstream.body, { status: upstream.status, headers });
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
if (c.req.raw.signal.aborted)
|
|
414
|
+
return new Response(null, { status: 499 });
|
|
415
|
+
logger.error({ err }, '[music] audio failed');
|
|
416
|
+
return c.json({ error: 'Stream lookup failed' }, 502);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|