@serifpersia/dango 3.3.8 → 3.3.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-D4qo4289.js → AnimeCard-URCiiy5W.js} +1 -1
- package/client/dist/assets/{AnimeInfo-BdRRm4So.js → AnimeInfo-e3z3er9t.js} +1 -1
- package/client/dist/assets/{AnimeInfoPage-DeJNAKMR.js → AnimeInfoPage-B7Djlmy_.js} +2 -2
- package/client/dist/assets/{Asmr-D0EGRYsv.js → Asmr-C28TfSvX.js} +1 -1
- package/client/dist/assets/{AsmrResetProgressModal-Gs0GmVaT.js → AsmrResetProgressModal-6Xh71AgR.js} +1 -1
- package/client/dist/assets/{AvSyncCalibrator-D8hqarVB.js → AvSyncCalibrator-BwCtRW0P.js} +1 -1
- package/client/dist/assets/{Home-BnBzjFkW.js → Home-BJdbHYja.js} +1 -1
- package/client/dist/assets/{Insights-i9QgY3nE.js → Insights-CjRy42YE.js} +1 -1
- package/client/dist/assets/{LibraryListPage-D3hTqTRU.js → LibraryListPage-B0qWqhb_.js} +1 -1
- package/client/dist/assets/{ListeningList-BgWsfu40.js → ListeningList-CSqD8j1C.js} +1 -1
- package/client/dist/assets/{Manga-Dn-CYins.js → Manga-Cg2YuObF.js} +1 -1
- package/client/dist/assets/{MangaInfoPage-EmQoK7Bv.js → MangaInfoPage-DcArhEsy.js} +1 -1
- package/client/dist/assets/{MangaReadPage-xJB8K2YU.js → MangaReadPage-YCETZRte.js} +1 -1
- package/client/dist/assets/{MangaResetProgressModal-Dian37PQ.js → MangaResetProgressModal-D_yT3d1v.js} +1 -1
- package/client/dist/assets/{Mature-DQtXKOXc.js → Mature-BGxRWkV5.js} +1 -1
- package/client/dist/assets/{MediaCard-DpKoY-1j.js → MediaCard-CAzoxsRS.js} +1 -1
- package/client/dist/assets/Music-Cgo-yQ7E.js +1 -0
- package/client/dist/assets/{OptionListMenu-Bj9ebsW3.js → OptionListMenu--Owe9Jle.js} +1 -1
- package/client/dist/assets/Player-yRXG2BrP.js +56 -0
- package/client/dist/assets/{PlayerSettings-DYZ272Yp.js → PlayerSettings-DrZLa0CC.js} +1 -1
- package/client/dist/assets/{QueueOptionsButton-lv26-6jp.js → QueueOptionsButton-BVZyPyKf.js} +1 -1
- package/client/dist/assets/{QueueRail-B7DeSFoA.js → QueueRail-B7f1-e6i.js} +1 -1
- package/client/dist/assets/{Radio-qQBYnVfY.js → Radio-BFjxlzuF.js} +1 -1
- package/client/dist/assets/{ReadingList-BT8kUjCB.js → ReadingList-BaIem8H1.js} +1 -1
- package/client/dist/assets/{ResetProgressModal-DWeiO-hs.js → ResetProgressModal-DES5Y-Fl.js} +1 -1
- package/client/dist/assets/{Search-BkvPYN33.js → Search-BOBo0Ogh.js} +1 -1
- package/client/dist/assets/{Settings-CyZEBF6S.js → Settings-DOvNQ0gX.js} +1 -1
- package/client/dist/assets/{SkeletonGrid-B5loz7ii.js → SkeletonGrid-BxfgjP4i.js} +1 -1
- package/client/dist/assets/{Trackers-JK-aXBYC.js → Trackers-DZ9KqOWv.js} +1 -1
- package/client/dist/assets/{Tv-BwXWJxMy.js → Tv-5mATEU4p.js} +1 -1
- package/client/dist/assets/{TvInfo-Dzyq6ucH.js → TvInfo-CF-5C5Fs.js} +1 -1
- package/client/dist/assets/{TvPopup-PlCU30FO.js → TvPopup-Dfqifj3s.js} +1 -1
- package/client/dist/assets/{TvSearch-BYwCf5kO.js → TvSearch-BWDwWnHe.js} +1 -1
- package/client/dist/assets/{TvWatchlist-BiJU6pKT.js → TvWatchlist-D3n5_4MI.js} +1 -1
- package/client/dist/assets/{Watchlist-BOEeGXH9.js → Watchlist-B8MHNsAU.js} +1 -1
- package/client/dist/assets/{hls-C11FOWHj.js → hls-BO_L1WoN.js} +1 -1
- package/client/dist/assets/{index-CtoKRNLc.js → index-D2RkUJIN.js} +2 -2
- package/client/dist/assets/{useAnimeInfoData-kr3WOIWH.js → useAnimeInfoData-BWShDHqE.js} +1 -1
- package/client/dist/assets/{useAsmr-CB9THf0m.js → useAsmr-FyfD9G7b.js} +1 -1
- package/client/dist/assets/{useAsmrPopup-cT_Uu41e.js → useAsmrPopup-D_o9-QlR.js} +1 -1
- package/client/dist/assets/{useMangaLibrary-rM6z3_0E.js → useMangaLibrary-BJU5x8Vz.js} +1 -1
- package/client/dist/assets/{useMangaPopup-PnmSctHu.js → useMangaPopup-TFzkC-tG.js} +1 -1
- package/client/dist/assets/{useProviders-B3VB7A9M.js → useProviders-DDRxa8vl.js} +1 -1
- package/client/dist/assets/{useTv-BTUKMH7Q.js → useTv-Bh6WmNnZ.js} +1 -1
- package/client/dist/assets/{useTvLibrary-C0sX-ilZ.js → useTvLibrary-_Pqm8re1.js} +1 -1
- package/client/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/dist/controllers/music.controller.js +123 -50
- package/server/dist/lib/ytmusic.js +15 -2
- package/server/package.json +1 -1
- package/client/dist/assets/Music-BP43Zqmq.js +0 -1
- package/client/dist/assets/Player-O2Cy2GF1.js +0 -56
|
@@ -1 +1 @@
|
|
|
1
|
-
import{T as m}from"./vendor-core-8R2fFbyN.js";import{b as y,u as o,a as u}from"./vendor-query-BXRECSI6.js";import{q as i,z as a}from"./index-
|
|
1
|
+
import{T as m}from"./vendor-core-8R2fFbyN.js";import{b as y,u as o,a as u}from"./vendor-query-BXRECSI6.js";import{q as i,z as a}from"./index-D2RkUJIN.js";const d=e=>String(e).toLowerCase()==="movie"?"movie":"tv",g=(e,t)=>`${d(e)}:${t}`,K=(e,t)=>{const r=d(e);return`/tv/${t}?type=${r}`},Q=(e,t,r,s)=>{const n=new URLSearchParams;return n.set("type",d(e)),r!==void 0&&n.set("s",String(r)),s!==void 0&&n.set("e",String(s)),`/tv/${t}/watch?${n.toString()}`},$=e=>e.adult===!0||e.adult===1,f=["Watching","Completed","On-Hold","Dropped","Planned"],l=(e,t)=>g(e,t),w=(e="All",t=1,r=24)=>y({queryKey:["tv-library",e,t,r],queryFn:()=>{const s=new URLSearchParams;return s.set("status",e),s.set("page",String(t)),s.set("limit",String(r)),i(`/api/tv/library?${s.toString()}`)}}),p=()=>y({queryKey:["tv-library-ids"],queryFn:()=>i("/api/tv/library/ids"),staleTime:1e3*60}),P=e=>y({queryKey:["tv-library-check",e],queryFn:()=>i(`/api/tv/library/check/${encodeURIComponent(e||"")}`),enabled:!!e,staleTime:1e3*60}),F=(e=24)=>y({queryKey:["tv-continue-watching",e],queryFn:()=>i(`/api/tv/continue-watching?limit=${e}`)}),O=e=>y({queryKey:["tv-progress",e],queryFn:()=>i(`/api/tv/progress/${encodeURIComponent(e||"")}`),enabled:!!e}),C=(e,t,r)=>y({queryKey:["tv-progress-latest",e,t,r],queryFn:()=>{const s=new URLSearchParams;t!==void 0&&s.set("season",String(t)),r!==void 0&&s.set("episode",String(r));const n=s.toString();return i(`/api/tv/progress/${encodeURIComponent(e||"")}/latest${n?`?${n}`:""}`)},enabled:!!e,staleTime:0}),q=()=>{const e=o();return u({mutationFn:async t=>{const{silent:r,...s}=t;return i("/api/tv/library/add",{method:"POST",body:JSON.stringify(s)})},onSuccess:(t,r)=>{r.silent||a.success("Added to TV watchlist"),e.invalidateQueries({queryKey:["tv-library"]}),e.invalidateQueries({queryKey:["tv-library-ids"]}),e.invalidateQueries({queryKey:["tv-library-check",l(r.mediaType,r.tmdbId)]})},onError:t=>{a.error(`Failed to add: ${t.message}`)}})},b=()=>{const e=o();return u({mutationFn:async t=>{await i("/api/tv/library/remove",{method:"POST",body:JSON.stringify({id:t})})},onSuccess:(t,r)=>{a.success("Removed from TV watchlist"),e.invalidateQueries({queryKey:["tv-library"]}),e.invalidateQueries({queryKey:["tv-library-ids"]}),e.invalidateQueries({queryKey:["tv-continue-watching"]}),e.invalidateQueries({queryKey:["tv-library-check",r]})},onError:t=>{a.error(`Failed to remove: ${t.message}`)}})},R=()=>{const e=o();return u({mutationFn:async({id:t,status:r})=>{await i("/api/tv/library/status",{method:"POST",body:JSON.stringify({id:t,status:r})})},onSuccess:()=>{a.success("Status updated"),e.invalidateQueries({queryKey:["tv-library"]}),e.invalidateQueries({queryKey:["tv-continue-watching"]})},onError:t=>{a.error(`Failed to update status: ${t.message}`)}})},k=()=>{const e=o();return u({mutationFn:async t=>{await i("/api/tv/progress",{method:"POST",body:JSON.stringify(t)})},onSuccess:(t,r)=>{e.invalidateQueries({queryKey:["tv-progress",r.mediaId]}),e.invalidateQueries({queryKey:["tv-progress-latest",r.mediaId]}),e.invalidateQueries({queryKey:["tv-continue-watching"]}),e.invalidateQueries({queryKey:["tv-library"]}),e.invalidateQueries({queryKey:["tv-library-ids"]})}})},L=()=>{const e=o();return u({mutationFn:async({mediaId:t,season:r,episode:s})=>{await i("/api/tv/progress/remove",{method:"POST",body:JSON.stringify({mediaId:t,season:r,episode:s})})},onSuccess:()=>{a.success("Progress reset"),e.invalidateQueries({queryKey:["tv-progress"]}),e.invalidateQueries({queryKey:["tv-continue-watching"]}),e.invalidateQueries({queryKey:["tv-library"]})},onError:t=>{a.error(`Failed to reset progress: ${t.message}`)}})},I=()=>{const e=o();return u({mutationFn:async({ids:t,status:r})=>i("/api/tv/library/batch-status",{method:"POST",body:JSON.stringify({ids:t,status:r})}),onSuccess:t=>{a.success(`Status updated for ${t.updated??0} items`),e.invalidateQueries({queryKey:["tv-library"]}),e.invalidateQueries({queryKey:["tv-continue-watching"]})},onError:t=>{a.error(`Failed to update statuses: ${t.message}`)}})},U=()=>{const e=o();return u({mutationFn:async t=>i("/api/tv/library/remove-many",{method:"POST",body:JSON.stringify({ids:t})}),onSuccess:t=>{const r=t.removed??0;a.success(`Removed ${r} ${r===1?"item":"items"} from TV watchlist`),e.invalidateQueries({queryKey:["tv-library"]}),e.invalidateQueries({queryKey:["tv-library-ids"]}),e.invalidateQueries({queryKey:["tv-continue-watching"]})},onError:t=>{a.error(`Failed to remove: ${t.message}`)}})},E=()=>{const e=o();return u({mutationFn:async t=>i("/api/tv/progress/remove-many",{method:"POST",body:JSON.stringify({ids:t})}),onSuccess:t=>{const r=t.removed??0;a.success(`Reset progress for ${r} ${r===1?"item":"items"}`),e.invalidateQueries({queryKey:["tv-progress"]}),e.invalidateQueries({queryKey:["tv-continue-watching"]}),e.invalidateQueries({queryKey:["tv-library"]})},onError:t=>{a.error(`Failed to reset progress: ${t.message}`)}})},J=()=>{const e=q(),t=b(),{data:r}=p(),s=m(()=>new Set(r?.ids||[]),[r]);return{toggle:c=>{const v=l(c.mediaType,c.tmdbId);s.has(v)?t.mutate(v):e.mutate({...c,poster:c.poster||""})},bookmarkedIds:s,pending:e.isPending||t.isPending}};export{f as T,b as a,L as b,K as c,g as d,P as e,J as f,k as g,R as h,$ as i,C as j,O as k,w as l,I as m,d as n,U as o,E as p,Q as t,F as u};
|
package/client/dist/index.html
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
background-color: #0c0d12;
|
|
34
34
|
}
|
|
35
35
|
</style>
|
|
36
|
-
<script type="module" crossorigin src="/assets/index-
|
|
36
|
+
<script type="module" crossorigin src="/assets/index-D2RkUJIN.js"></script>
|
|
37
37
|
<link rel="modulepreload" crossorigin href="/assets/vendor-core-8R2fFbyN.js">
|
|
38
38
|
<link rel="modulepreload" crossorigin href="/assets/vendor-query-BXRECSI6.js">
|
|
39
39
|
<link rel="modulepreload" crossorigin href="/assets/vendor-icons-CDMuNDeL.js">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serifpersia/dango",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.9",
|
|
4
4
|
"description": "A web application for watching anime.",
|
|
5
5
|
"main": "orchestrator.js",
|
|
6
6
|
"repository": {
|
|
@@ -81,5 +81,5 @@
|
|
|
81
81
|
"allowScripts": {
|
|
82
82
|
"esbuild@0.28.2": true
|
|
83
83
|
},
|
|
84
|
-
"versionDate": "2026-09-
|
|
84
|
+
"versionDate": "2026-09-25T21:08:55.146Z"
|
|
85
85
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import logger from '../logger.js';
|
|
3
|
-
import { getInnertube, getAuthedInnertube, saveMusicCookie, getMusicAuthStatus, signOutMusic, isParserVariantError, } from '../lib/ytmusic.js';
|
|
3
|
+
import { getInnertube, getAuthedInnertube, refreshAuthedInnertube, saveMusicCookie, getMusicAuthStatus, signOutMusic, isParserVariantError, } from '../lib/ytmusic.js';
|
|
4
4
|
const NAME_SUFFIXES = [
|
|
5
5
|
/\s*(- topic)$/i,
|
|
6
6
|
/\s*vevo$/i,
|
|
@@ -151,6 +151,19 @@ function upnextToTrack(node, trustLike = false) {
|
|
|
151
151
|
...(liked !== null ? { liked } : {}),
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
|
+
function parseUpnext(panel, trustLike) {
|
|
155
|
+
const tracks = [];
|
|
156
|
+
const contents = panel.contents ?? [];
|
|
157
|
+
for (const item of contents) {
|
|
158
|
+
const track = upnextToTrack(item, trustLike);
|
|
159
|
+
if (track)
|
|
160
|
+
tracks.push(track);
|
|
161
|
+
if (tracks.length >= 50)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
const playlistId = panel.playlist_id ?? null;
|
|
165
|
+
return { tracks, playlistId };
|
|
166
|
+
}
|
|
154
167
|
function asMenuItems(menu) {
|
|
155
168
|
if (!menu || typeof menu !== 'object')
|
|
156
169
|
return [];
|
|
@@ -278,12 +291,7 @@ async function readLibrary(yt) {
|
|
|
278
291
|
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';
|
|
279
292
|
const audioCache = new Map();
|
|
280
293
|
const AUDIO_CACHE_MS = 5 * 60 * 1000;
|
|
281
|
-
async function
|
|
282
|
-
const cached = audioCache.get(videoId);
|
|
283
|
-
if (cached && Date.now() - cached.fetchedAt < AUDIO_CACHE_MS)
|
|
284
|
-
return cached;
|
|
285
|
-
const authed = await getAuthedInnertube().catch(() => null);
|
|
286
|
-
const yt = authed ?? (await getInnertube());
|
|
294
|
+
async function decipherWith(yt, videoId) {
|
|
287
295
|
const info = await yt.music.getInfo(videoId);
|
|
288
296
|
const streaming = info.streaming_data;
|
|
289
297
|
const formats = streaming?.adaptive_formats ?? [];
|
|
@@ -306,6 +314,30 @@ async function decipherAudio(videoId) {
|
|
|
306
314
|
}
|
|
307
315
|
throw new Error('No stream available');
|
|
308
316
|
}
|
|
317
|
+
async function decipherAudio(videoId) {
|
|
318
|
+
const cached = audioCache.get(videoId);
|
|
319
|
+
if (cached && Date.now() - cached.fetchedAt < AUDIO_CACHE_MS)
|
|
320
|
+
return cached;
|
|
321
|
+
const authed = await getAuthedInnertube().catch(() => null);
|
|
322
|
+
if (authed) {
|
|
323
|
+
try {
|
|
324
|
+
return await decipherWith(authed, videoId);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
logger.warn({ err }, '[music] authed stream lookup failed, retrying with a fresh saved-cookie session');
|
|
328
|
+
const fresh = await refreshAuthedInnertube();
|
|
329
|
+
if (fresh) {
|
|
330
|
+
try {
|
|
331
|
+
return await decipherWith(fresh, videoId);
|
|
332
|
+
}
|
|
333
|
+
catch (retryErr) {
|
|
334
|
+
logger.warn({ err: retryErr }, '[music] refreshed stream lookup failed, falling back to public session');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return decipherWith(await getInnertube(), videoId);
|
|
340
|
+
}
|
|
309
341
|
const PASSTHROUGH_HEADERS = ['content-type', 'content-length', 'content-range', 'accept-ranges'];
|
|
310
342
|
export class MusicController {
|
|
311
343
|
cache;
|
|
@@ -547,7 +579,7 @@ export class MusicController {
|
|
|
547
579
|
const id = String(req.query.id || '').trim();
|
|
548
580
|
if (!id)
|
|
549
581
|
return res.status(400).json({ error: 'id is required', tracks: [] });
|
|
550
|
-
const yt = await getAuthedInnertube();
|
|
582
|
+
const yt = await getAuthedInnertube().catch(() => null);
|
|
551
583
|
if (!yt) {
|
|
552
584
|
return res.status(401).json({ error: 'MUSIC_AUTH_REQUIRED', tracks: [] });
|
|
553
585
|
}
|
|
@@ -555,20 +587,41 @@ export class MusicController {
|
|
|
555
587
|
res.json({ tracks: await fetchPlaylistTracks(yt, id) });
|
|
556
588
|
}
|
|
557
589
|
catch (err) {
|
|
558
|
-
logger.warn({ err }, '[music] playlist failed,
|
|
559
|
-
|
|
590
|
+
logger.warn({ err }, '[music] playlist failed, retrying with a fresh saved-cookie session');
|
|
591
|
+
const fresh = await refreshAuthedInnertube();
|
|
592
|
+
if (!fresh)
|
|
593
|
+
return res.json({ tracks: [] });
|
|
594
|
+
try {
|
|
595
|
+
res.json({ tracks: await fetchPlaylistTracks(fresh, id) });
|
|
596
|
+
}
|
|
597
|
+
catch (retryErr) {
|
|
598
|
+
logger.warn({ err: retryErr }, '[music] playlist retry failed, returning empty');
|
|
599
|
+
res.json({ tracks: [] });
|
|
600
|
+
}
|
|
560
601
|
}
|
|
561
602
|
};
|
|
562
603
|
library = async (_req, res) => {
|
|
563
|
-
const yt = await getAuthedInnertube();
|
|
604
|
+
const yt = await getAuthedInnertube().catch(() => null);
|
|
564
605
|
if (!yt) {
|
|
565
606
|
return res.status(401).json({ error: 'MUSIC_AUTH_REQUIRED', tracks: [], playlists: [] });
|
|
566
607
|
}
|
|
567
608
|
try {
|
|
568
|
-
|
|
609
|
+
const first = await readLibrary(yt);
|
|
610
|
+
if (first.tracks.length > 0 || first.playlists.length > 0)
|
|
611
|
+
return res.json(first);
|
|
612
|
+
logger.warn('[music] library came back empty, retrying with a fresh saved-cookie session');
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
logger.warn({ err }, '[music] library failed, retrying with a fresh saved-cookie session');
|
|
616
|
+
}
|
|
617
|
+
const fresh = await refreshAuthedInnertube();
|
|
618
|
+
if (!fresh)
|
|
619
|
+
return res.json({ tracks: [], playlists: [] });
|
|
620
|
+
try {
|
|
621
|
+
res.json(await readLibrary(fresh));
|
|
569
622
|
}
|
|
570
623
|
catch (err) {
|
|
571
|
-
logger.warn({ err }, '[music] library failed, returning empty');
|
|
624
|
+
logger.warn({ err }, '[music] library retry failed, returning empty');
|
|
572
625
|
res.json({ tracks: [], playlists: [] });
|
|
573
626
|
}
|
|
574
627
|
};
|
|
@@ -597,18 +650,7 @@ export class MusicController {
|
|
|
597
650
|
return res.json(cached);
|
|
598
651
|
try {
|
|
599
652
|
const yt = await getInnertube();
|
|
600
|
-
const
|
|
601
|
-
const tracks = [];
|
|
602
|
-
const contents = panel.contents ?? [];
|
|
603
|
-
for (const item of contents) {
|
|
604
|
-
const track = upnextToTrack(item);
|
|
605
|
-
if (track)
|
|
606
|
-
tracks.push(track);
|
|
607
|
-
if (tracks.length >= 50)
|
|
608
|
-
break;
|
|
609
|
-
}
|
|
610
|
-
const playlistId = panel.playlist_id ?? null;
|
|
611
|
-
const payload = { tracks, playlistId };
|
|
653
|
+
const payload = parseUpnext(await yt.music.getUpNext(id), false);
|
|
612
654
|
this.cache.set(cacheKey, payload, 300);
|
|
613
655
|
return res.json(payload);
|
|
614
656
|
}
|
|
@@ -618,22 +660,27 @@ export class MusicController {
|
|
|
618
660
|
}
|
|
619
661
|
}
|
|
620
662
|
try {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
663
|
+
return res.json(parseUpnext(await authed.music.getUpNext(id), true));
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
logger.warn({ err }, '[music] authed upnext failed, retrying with a fresh saved-cookie session');
|
|
667
|
+
}
|
|
668
|
+
const fresh = await refreshAuthedInnertube();
|
|
669
|
+
if (fresh) {
|
|
670
|
+
try {
|
|
671
|
+
return res.json(parseUpnext(await fresh.music.getUpNext(id), true));
|
|
672
|
+
}
|
|
673
|
+
catch (retryErr) {
|
|
674
|
+
logger.warn({ err: retryErr }, '[music] upnext retry failed, falling back to public');
|
|
630
675
|
}
|
|
631
|
-
|
|
632
|
-
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const yt = await getInnertube();
|
|
679
|
+
return res.json(parseUpnext(await yt.music.getUpNext(id), false));
|
|
633
680
|
}
|
|
634
681
|
catch (err) {
|
|
635
682
|
logger.error({ err }, '[music] upnext failed');
|
|
636
|
-
res.json({ tracks: [], playlistId: null });
|
|
683
|
+
return res.json({ tracks: [], playlistId: null });
|
|
637
684
|
}
|
|
638
685
|
};
|
|
639
686
|
likedIds = async (_req, res) => {
|
|
@@ -645,15 +692,29 @@ export class MusicController {
|
|
|
645
692
|
const cached = this.cache.get(cacheKey);
|
|
646
693
|
if (cached)
|
|
647
694
|
return res.json({ likedIds: cached });
|
|
695
|
+
const fetchLikedIds = async (session) => {
|
|
696
|
+
const tracks = await fetchPlaylistTracks(session, 'LM', 500);
|
|
697
|
+
return tracks.map((t) => t.id);
|
|
698
|
+
};
|
|
648
699
|
try {
|
|
649
|
-
const
|
|
650
|
-
const likedIds = tracks.map((t) => t.id);
|
|
700
|
+
const likedIds = await fetchLikedIds(yt);
|
|
651
701
|
this.cache.set(cacheKey, likedIds, 120);
|
|
652
702
|
res.json({ likedIds });
|
|
653
703
|
}
|
|
654
704
|
catch (err) {
|
|
655
|
-
logger.warn({ err }, '[music] liked ids failed,
|
|
656
|
-
|
|
705
|
+
logger.warn({ err }, '[music] liked ids failed, retrying with a fresh saved-cookie session');
|
|
706
|
+
const fresh = await refreshAuthedInnertube();
|
|
707
|
+
if (!fresh)
|
|
708
|
+
return res.json({ likedIds: [] });
|
|
709
|
+
try {
|
|
710
|
+
const likedIds = await fetchLikedIds(fresh);
|
|
711
|
+
this.cache.set(cacheKey, likedIds, 120);
|
|
712
|
+
res.json({ likedIds });
|
|
713
|
+
}
|
|
714
|
+
catch (retryErr) {
|
|
715
|
+
logger.warn({ err: retryErr }, '[music] liked ids retry failed, returning empty');
|
|
716
|
+
res.json({ likedIds: [] });
|
|
717
|
+
}
|
|
657
718
|
}
|
|
658
719
|
};
|
|
659
720
|
rate = async (req, res) => {
|
|
@@ -665,16 +726,16 @@ export class MusicController {
|
|
|
665
726
|
if (!yt) {
|
|
666
727
|
return res.status(401).json({ error: 'MUSIC_AUTH_REQUIRED' });
|
|
667
728
|
}
|
|
668
|
-
|
|
669
|
-
const actions =
|
|
670
|
-
|
|
729
|
+
const attempt = async (session) => {
|
|
730
|
+
const actions = session.actions ??
|
|
731
|
+
session.session?.actions;
|
|
671
732
|
if (!actions)
|
|
672
733
|
throw new Error('Actions unavailable');
|
|
673
734
|
// Only ever call the toggle endpoints YouTube Music itself returns for
|
|
674
735
|
// this track (payload status LIKE/INDIFFERENT). No hand-built endpoints:
|
|
675
736
|
// raw video ids can differ from the canonical song id and a direct
|
|
676
737
|
// /like call would orphan entries in Liked Music.
|
|
677
|
-
const panel = await
|
|
738
|
+
const panel = await session.music.getUpNext(id);
|
|
678
739
|
const contents = panel.contents ?? [];
|
|
679
740
|
let endpoint = null;
|
|
680
741
|
for (const item of contents) {
|
|
@@ -686,13 +747,25 @@ export class MusicController {
|
|
|
686
747
|
if (!endpoint)
|
|
687
748
|
throw new Error('Like toggle not found for track');
|
|
688
749
|
await endpoint.call(actions);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
750
|
+
};
|
|
751
|
+
try {
|
|
752
|
+
await attempt(yt);
|
|
692
753
|
}
|
|
693
754
|
catch (err) {
|
|
694
|
-
logger.
|
|
695
|
-
|
|
755
|
+
logger.warn({ err }, '[music] rate failed, retrying with a fresh saved-cookie session');
|
|
756
|
+
const fresh = await refreshAuthedInnertube();
|
|
757
|
+
if (!fresh)
|
|
758
|
+
return res.status(502).json({ error: 'Like action failed' });
|
|
759
|
+
try {
|
|
760
|
+
await attempt(fresh);
|
|
761
|
+
}
|
|
762
|
+
catch (retryErr) {
|
|
763
|
+
logger.error({ err: retryErr }, '[music] rate retry failed');
|
|
764
|
+
return res.status(502).json({ error: 'Like action failed' });
|
|
765
|
+
}
|
|
696
766
|
}
|
|
767
|
+
this.cache.delete('music-liked-ids');
|
|
768
|
+
logger.info(`[music] rate id=${id} liked=${like} via=toggle`);
|
|
769
|
+
res.json({ success: true, liked: like });
|
|
697
770
|
};
|
|
698
771
|
}
|
|
@@ -72,19 +72,32 @@ export function getInnertube() {
|
|
|
72
72
|
return publicPromise;
|
|
73
73
|
}
|
|
74
74
|
/** Authenticated session built from the saved cookie. Null when not signed in. */
|
|
75
|
-
export async function getAuthedInnertube() {
|
|
75
|
+
export async function getAuthedInnertube(forceRefresh = false) {
|
|
76
76
|
const cookie = readCookie();
|
|
77
77
|
if (!cookie) {
|
|
78
78
|
authedTube = null;
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
|
-
if (authedTube)
|
|
81
|
+
if (authedTube && !forceRefresh)
|
|
82
82
|
return authedTube;
|
|
83
83
|
await ensureShim();
|
|
84
84
|
const { Innertube } = await loadYoutube();
|
|
85
85
|
authedTube = await Innertube.create({ cookie });
|
|
86
86
|
return authedTube;
|
|
87
87
|
}
|
|
88
|
+
export function invalidateAuthedSession() {
|
|
89
|
+
authedTube = null;
|
|
90
|
+
}
|
|
91
|
+
export async function refreshAuthedInnertube() {
|
|
92
|
+
invalidateAuthedSession();
|
|
93
|
+
try {
|
|
94
|
+
return await getAuthedInnertube();
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
logger.warn({ err }, '[ytmusic] saved-cookie session refresh failed');
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
88
101
|
export function getMusicAuthStatus() {
|
|
89
102
|
return { authenticated: readCookie() !== null };
|
|
90
103
|
}
|
package/server/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{A as L,d as b,h as F,g as Ke,u as e,S as w,$ as Qe,o as ze,T as z}from"./vendor-core-8R2fFbyN.js";import{q as P,z as C}from"./index-CtoKRNLc.js";import{I as p}from"./vendor-icons-CDMuNDeL.js";import{a as i}from"./Asmr.module-ChUXDZKx.js";import{r as m}from"./Radio.module-DGSZLpKM.js";import{b as J,u as Ce,a as Le}from"./vendor-query-BXRECSI6.js";function Ie(n){if(!Number.isFinite(n)||n<0)return"0:00";const h=Math.floor(n/60),g=Math.floor(n%60);return`${h}:${String(g).padStart(2,"0")}`}const Ye=({track:n,shuffle:h,onToggleShuffle:g,onTrackStep:O,onTrackFailed:be,onClose:ie,liked:Y,likeVisible:W,likePending:ae,onToggleLike:V})=>{const M=L(null),Z=L(!1),[q,R]=b(!1),[he,l]=b(!0),[N,D]=b(!1),[I,B]=b(0),[U,se]=b(0),[T,H]=b(()=>{const r=parseFloat(localStorage.getItem("musicVolume")||"");return Number.isFinite(r)?r:1}),[$,re]=b(()=>localStorage.getItem("musicAutoplay")!=="false"),j=L(T);j.current=T;const S=L($);S.current=$;const f=L(0),E=L(0),A=L(null),K=L("");K.current||(K.current=typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID():`music-${Date.now()}-${Math.random().toString(36).slice(2)}`);const X=`/api/music/audio?videoId=${encodeURIComponent(n.id)}`,G=()=>{A.current&&(window.clearTimeout(A.current),A.current=null)},ee=r=>{const a=M.current;if(!a)return;G(),l(!0),D(!1);const k=()=>{if(a.removeEventListener("loadedmetadata",k),r>5&&Number.isFinite(a.duration)&&r<a.duration-5)try{a.currentTime=r}catch{}};a.addEventListener("loadedmetadata",k),a.src=X,a.load(),a.volume=j.current,a.play().catch(()=>{})},ve=()=>{l(!1),D(!0),be?.(n),S.current&&O(1)},oe=()=>{if(!A.current){if(E.current>=3){ve();return}E.current+=1,A.current=window.setTimeout(()=>{A.current=null,ee(f.current)},5e3)}},ke=()=>{E.current=0,ee(f.current)};F(()=>{f.current=0,E.current=0,R(!1),B(0),se(0);const r=M.current;return ee(f.current),()=>{G(),r&&r.removeAttribute("src")}},[X]),F(()=>{const r=M.current;r&&(r.volume=T)},[T]),F(()=>(document.body.classList.add("asmr-player-open"),()=>document.body.classList.remove("asmr-player-open")),[]);const _=n.thumbnails?.[n.thumbnails.length-1]?.url??null,te=Ke.useCallback(r=>{const a=M.current;fetch("/api/discord/music",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:n.title,artistLabel:n.artists,isPlaying:r,thumbnail:_||"",currentTime:a?.currentTime??0,duration:Number.isFinite(a?.duration??NaN)?a?.duration:0,sessionId:K.current})}).catch(()=>{}),fetch("/api/discord/heartbeat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionId:K.current})}).catch(()=>{})},[n,_]);F(()=>{te(q)},[te,q,n.id]),F(()=>{if(!q)return;const r=window.setInterval(()=>te(!0),15e3);return()=>window.clearInterval(r)},[q,te]),F(()=>{const r=K.current,a=()=>{if(!r)return;const ye=JSON.stringify({sessionId:r});navigator.sendBeacon?(navigator.sendBeacon("/api/discord/clear",new Blob([ye],{type:"application/json"})),navigator.sendBeacon("/api/discord/heartbeat",new Blob([JSON.stringify({sessionId:r,bye:!0})],{type:"application/json"}))):(fetch("/api/discord/clear",{method:"POST",headers:{"Content-Type":"application/json"},body:ye,keepalive:!0}).catch(()=>{}),fetch("/api/discord/heartbeat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sessionId:r,bye:!0}),keepalive:!0}).catch(()=>{}))},k=()=>a(),v=()=>{document.visibilityState==="hidden"&&a()};return window.addEventListener("pagehide",k),window.addEventListener("beforeunload",k),document.addEventListener("visibilitychange",v),()=>{window.removeEventListener("pagehide",k),window.removeEventListener("beforeunload",k),document.removeEventListener("visibilitychange",v),a()}},[]);const pe=()=>{const r=M.current;r&&(r.paused?r.play().catch(()=>{}):r.pause())},[Ne,le]=b(!1);F(()=>{le(!1)},[_]);const ce=`${n.title} — ${n.artists}`,ge=e(w,{children:[e("div",{className:i.playerButtons,children:[e("button",{className:i.playerBtn,onClick:()=>O(-1),"aria-label":"Previous track",children:e(p,{name:"step-backward"})}),e("button",{className:`${i.playerBtn} ${i.playBtn}`,onClick:pe,"aria-label":q?"Pause":"Play",children:q?e(p,{name:"pause"}):e(p,{name:"play"})}),e("button",{className:i.playerBtn,onClick:()=>O(1),"aria-label":"Next track",children:e(p,{name:"step-forward"})}),N&&e("button",{className:i.playerBtn,onClick:ke,title:"Retry playback","aria-label":"Retry playback",children:e(p,{name:"redo"})})]}),e("div",{className:i.seekRow,children:[e("span",{className:i.timeLabel,children:Ie(I)}),e("input",{className:i.seekBar,type:"range",min:0,max:U||0,step:.1,value:Math.min(I,U||0),onChange:r=>{const a=M.current;!a||!Number.isFinite(a.duration)||(a.currentTime=parseFloat(r.target.value),B(a.currentTime))},onPointerDown:()=>{Z.current=!0},onPointerUp:()=>{Z.current=!1},style:{"--played-percent":`${U>0?I/U*100:0}%`},"aria-label":"Seek"}),e("span",{className:i.timeLabel,children:Ie(U)}),he&&!N&&e("span",{className:i.timeLabel,children:"loading…"})]})]}),ue=e(w,{children:[n.thumbnails?.[0]?.url&&!Ne&&e("img",{src:n.thumbnails[0].url,alt:"",className:m.stationThumb,loading:"lazy",decoding:"async",onError:()=>le(!0)}),e("div",{className:i.playerInfo,children:[e("p",{className:i.playerTitle,title:ce,children:ce}),e("p",{className:i.playerTrack,children:n.artists})]}),e("div",{className:i.playerControls,children:ge}),e("div",{className:i.playerRight,children:[T===0?e(p,{name:"volume-mute",className:i.volumeIcon}):T<.5?e(p,{name:"volume-down",className:i.volumeIcon}):e(p,{name:"volume-up",className:i.volumeIcon}),e("input",{className:i.volumeBar,type:"range",min:0,max:1,step:.01,value:T,onChange:r=>{const a=parseFloat(r.target.value);H(a),localStorage.setItem("musicVolume",String(a))},style:{"--volume-percent":`${T*100}%`},"aria-label":"Volume"})]}),e("div",{className:i.playerActions,children:[W&&V&&e("button",{className:`${i.playerBtn} ${Y?i.playerBtnActive:""}`,onClick:V,disabled:ae,title:Y?"Remove from Liked Music":"Add to Liked Music","aria-label":Y?"Remove from Liked Music":"Add to Liked Music","aria-pressed":!!Y,children:e(p,{name:"heart"})}),e("button",{className:`${i.playerBtn} ${h?i.playerBtnActive:""}`,onClick:g,title:h?"Shuffle on":"Shuffle off","aria-label":"Toggle shuffle","aria-pressed":h,children:e(p,{name:"shuffle"})}),e("button",{className:`${i.playerBtn} ${$?i.playerBtnActive:""}`,onClick:()=>{re(r=>(localStorage.setItem("musicAutoplay",String(!r)),!r))},title:$?"Autoplay on: next track plays automatically":"Autoplay off","aria-label":"Toggle autoplay","aria-pressed":$,children:e(p,{name:"forward"})}),e("a",{href:`https://music.youtube.com/watch?v=${encodeURIComponent(n.id)}`,target:"_blank",rel:"noreferrer",title:"Open in YouTube Music","aria-label":"Open in YouTube Music",className:i.playerBtn,style:{display:"inline-flex",alignItems:"center"},children:e(p,{name:"external-link-alt"})}),e("button",{className:i.playerBtn,onClick:ie,"aria-label":"Close player",children:e(p,{name:"times"})})]})]});return Qe(e(w,{children:[e("audio",{ref:M,preload:"none",onPlay:()=>{R(!0),l(!1),D(!1),E.current=0,G()},onPause:()=>R(!1),onWaiting:()=>{N||l(!0)},onPlaying:()=>{l(!1),E.current=0,G()},onTimeUpdate:r=>{const a=r.target.currentTime;f.current=a,G(),Z.current||B(a)},onStalled:()=>{oe()},onLoadedMetadata:r=>{const a=r.target.duration;Number.isFinite(a)&&se(a)},onDurationChange:r=>{const a=r.target.duration;Number.isFinite(a)&&se(a)},onError:()=>{oe()},onEnded:()=>{S.current&&O(1)}}),e("div",{className:i.playerBar,children:ue})]}),document.body)},je=()=>e("div",{style:{marginTop:"0.9rem",paddingTop:"0.9rem",borderTop:"1px dashed var(--border-color)"},children:[e("h4",{style:{margin:"0 0 0.35rem",fontSize:"0.95rem"},children:"Where to get the cookie"}),e("p",{style:{margin:"0 0 0.6rem",fontSize:"0.85rem",color:"var(--text-secondary)"},children:"Google marks its login cookies unreadable to page scripts, so this has to come from DevTools — it takes about 20 seconds:"}),e("ol",{style:{margin:"0 0 0.75rem",paddingLeft:"1.25rem",fontSize:"0.85rem",color:"var(--text-secondary)",display:"grid",gap:"0.3rem"},children:[e("li",{children:["Open"," ",e("a",{href:"https://music.youtube.com/",target:"_blank",rel:"noopener noreferrer",children:"music.youtube.com"})," ","and make sure you are logged in."]}),e("li",{children:["Press ",e("kbd",{children:"F12"}),", go to the ",e("strong",{children:"Network"})," tab, and type ",e("kbd",{children:"browse"})," ","in the filter box."]}),e("li",{children:["Click any ",e("kbd",{children:"POST"})," browse entry, then scroll to Request Headers."]}),e("li",{children:[e("strong",{children:"Right-click"})," the ",e("kbd",{children:"Cookie"})," value and choose"," ",e("strong",{children:"Copy value"})," — right-click copies the full unwrapped value, selecting the text by hand truncates it."]}),e("li",{children:"Paste it into the sign-in box above and press Sign in."})]})]}),me=300*1e3,Ge=()=>J({queryKey:["music-auth"],queryFn:()=>P("/api/music/auth/status"),staleTime:30*1e3,refetchInterval:n=>n.state.data?.authenticated?!1:3e3,retry:1}),Je=()=>{const n=Ce();return Le({mutationFn:h=>P("/api/music/auth/start",{method:"POST",body:JSON.stringify({cookie:h})}),onSuccess:()=>{n.invalidateQueries({queryKey:["music-auth"]}),n.invalidateQueries({queryKey:["music-library"]})}})},We=()=>{const n=Ce();return Le({mutationFn:()=>P("/api/music/auth/signout",{method:"POST"}),onSuccess:()=>{n.invalidateQueries({queryKey:["music-auth"]}),n.invalidateQueries({queryKey:["music-library"]})}})},Ve=n=>J({queryKey:["music-search",n],queryFn:()=>P(`/api/music/search?q=${encodeURIComponent(n)}`),enabled:n.trim().length>0,staleTime:me,retry:1}),Ze=n=>J({queryKey:["music-library"],queryFn:()=>P("/api/music/library"),enabled:n,staleTime:me,retry:1}),Xe=n=>J({queryKey:["music-track",n],queryFn:()=>P(`/api/music/track?id=${encodeURIComponent(n||"")}`),enabled:!!n,staleTime:me,retry:1}),et=n=>J({queryKey:["music-playlist",n],queryFn:()=>P(`/api/music/playlist?id=${encodeURIComponent(n||"")}`),enabled:!!n,staleTime:me,retry:1}),tt=n=>J({queryKey:["music-upnext",n],queryFn:()=>P(`/api/music/upnext?id=${encodeURIComponent(n||"")}`),enabled:!!n,staleTime:me,retry:1}),nt=n=>J({queryKey:["music-likes"],queryFn:()=>P("/api/music/likes"),enabled:n,staleTime:120*1e3,retry:1}),it=()=>{const n=Ce();return Le({mutationFn:({id:h,like:g})=>P("/api/music/like",{method:"POST",body:JSON.stringify({id:h,like:g})}),onSuccess:()=>{n.invalidateQueries({queryKey:["music-likes"]}),n.invalidateQueries({queryKey:["music-library"]}),n.invalidateQueries({queryKey:["music-upnext"]})}})},at="_authRow_10tvv_1",st="_btn_10tvv_9",rt="_btnGhost_10tvv_42",ot="_cookieForm_10tvv_74",lt="_cookieInput_10tvv_83",ct="_link_10tvv_99",ut="_contentLayout_10tvv_109",dt="_mainCol_10tvv_116",mt="_upnextAside_10tvv_120",ht="_upnextHeader_10tvv_131",pt="_upnextTitle_10tvv_139",gt="_upnextActions_10tvv_149",yt="_nowPlayingLabel_10tvv_156",ft="_nowPlayingWrap_10tvv_168",bt="_likeBtn_10tvv_172",vt="_likeActive_10tvv_196",y={authRow:at,btn:st,btnGhost:rt,cookieForm:ot,cookieInput:lt,link:ct,contentLayout:ut,mainCol:dt,upnextAside:mt,upnextHeader:ht,upnextTitle:pt,upnextActions:gt,nowPlayingLabel:yt,nowPlayingWrap:ft,likeBtn:bt,likeActive:vt};function $e(n){const h=n.trim();if(!h)return null;const g=h.match(/[?&]v=([A-Za-z0-9_-]{11})/)?.[1]??h.match(/youtu\.be\/([A-Za-z0-9_-]{11})/)?.[1]??null;return g||(/^[A-Za-z0-9_-]{11}$/.test(h)?h:null)}const kt="https://github.com/serifpersia/dango/releases/latest/download/dango-extension.zip",Lt=()=>{const[n,h]=ze(),g=$e(n.get("v")??""),[O,be]=b(""),[ie,Y]=b(""),[W,ae]=b(""),[V,M]=b(!1),[Z,q]=b(()=>localStorage.getItem("musicShuffle")==="true"),[R,he]=b(()=>localStorage.getItem("musicRadio")!=="false"),[l,N]=b(null),[D,I]=b([]),[B,U]=b(null),[se,T]=b({}),H=L([]);H.current=D;const $=L(null);$.current=l;const re=L(!1),j=L(new Set),{data:S}=Ge(),f=Je(),E=We(),A=typeof navigator<"u"?navigator.userAgent:"",K=/android/i.test(A),X=/firefox|fxios/i.test(A)&&!K,G=!X&&!K&&/chrome|chromium|edg|brave/i.test(A),ee=X||G,ve=()=>{if(V)return;M(!0);const t=window.setTimeout(()=>{window.removeEventListener("dango:ytmusic-cookie",s),M(!1),C.error("Helper not detected — install the Dango Helper extension first.")},3e3),s=c=>{window.clearTimeout(t),window.removeEventListener("dango:ytmusic-cookie",s),M(!1);try{const o=JSON.parse(c.detail);if(o?.ok&&o.cookie){const d=o.cookie.replace(/^cookie\s*:\s*/i,"").replace(/^["']+|["']+$/g,"").trim();if(d.includes("…")||d.includes("...")){C.error(`Helper returned a truncated cookie (${d.length} chars) — reload music.youtube.com and Extract again.`);return}ae(d),C.success("Cookie extracted from helper — signing in…"),f.mutate(d)}else o?.error==="NO_COOKIE"?C.error("Helper found no YouTube login — log in on a music.youtube.com tab first."):C.error("Helper request failed — try again.")}catch{C.error("Helper request failed — try again.")}};window.addEventListener("dango:ytmusic-cookie",s),window.dispatchEvent(new CustomEvent("dango:get-ytmusic-cookie"))},{data:oe,isLoading:ke}=Ve(ie),{data:_,isLoading:te}=Ze(S?.authenticated===!0),{data:pe,isLoading:Ne}=et(B?.id??null),{data:le,isLoading:ce}=tt(l?.id??null),{data:ge}=nt(S?.authenticated===!0),ue=it(),Q=z(()=>oe?.tracks||[],[oe]),r=z(()=>_?.playlists||[],[_]),a=z(()=>_?.tracks||[],[_]),k=z(()=>pe?.tracks||[],[pe]),v=z(()=>{const t=le?.tracks||[];return l?t.filter(s=>s.id!==l.id):t},[le,l]),ye=z(()=>new Set(ge?.likedIds||[]),[ge]),Se=t=>se[t.id]??(ye.has(t.id)||t.liked===!0);F(()=>{S?.authenticated!==!0&&T({})},[S?.authenticated]);const Ae=z(()=>[...Q,...k,...a,...v],[Q,k,a,v]),Fe=g&&l?.id!==g&&!Ae.some(t=>t.id===g)?g:null,{data:Me}=Xe(Fe),we=l?.id;F(()=>{document.title=l?`${l.title} - dango`:"Music - dango"},[l]),F(()=>{if(!g){we&&N(null);return}if(we===g)return;const t=o=>{const d=o.find(u=>u.id===g);return d?(N(d),I(u=>u.some(ne=>ne.id===g)?u:o.length>0?o:[d]),!0):!1};if(t(Q)||t(k)||t(a))return;const s=v.find(o=>o.id===g);if(s){N(s),I(o=>o.some(d=>d.id===s.id)?o:[s]);return}const c=Me?.track;c&&c.id===g&&(N(c),I(o=>o.some(d=>d.id===c.id)?o:[c]))},[g,we,Q,k,a,v,Me]);const Te=(t,s)=>{j.current.delete(t.id),N(t),I(s.length>0?s:[t]),h({v:t.id})},Be=t=>{j.current.add(t.id),C.error(`Couldn't play “${t.title}” — skipped.`)},Ee=async()=>{if(re.current)return;const t=H.current,s=t[t.length-1]??$.current;if(s){re.current=!0;try{const c=await P(`/api/music/upnext?id=${encodeURIComponent(s.id)}`),o=new Set(t.map(u=>u.id)),d=(c.tracks||[]).filter(u=>!o.has(u.id)&&!j.current.has(u.id));if(d.length===0){const u=H.current[0];u&&(N(u),h({v:u.id}));return}I([...H.current,...d]),N(d[0]),h({v:d[0].id})}catch{const c=H.current[0];c&&(N(c),h({v:c.id}))}finally{re.current=!1}}},Oe=t=>{const s=H.current,c=$.current;if(!c||s.length===0)return;const o=s.findIndex(x=>x.id===c.id);if(t>0&&R&&(o<0||o===s.length-1)){Ee();return}const d=x=>j.current.has(x.id),u=s.length;let ne;if(Z&&u>1){let x=Math.floor(Math.random()*u);for(let xe=0;xe<u&&(x===o||d(s[x]));xe++)x=Math.floor(Math.random()*u);ne=s[x]}else{const x=t>0?1:-1;let fe=(((o<0?x>0?-1:0:o)+x)%u+u)%u;for(let Re=1;Re<u&&d(s[fe]);Re++)fe=((fe+x)%u+u)%u;ne=s[fe]}N(ne),h({v:ne.id})},qe=()=>{if(R){he(!1),localStorage.setItem("musicRadio","false");return}const t=$.current;if(!t)return;if(v.length===0){C.error("No radio suggestions for this song yet.");return}const s=new Set,c=[];for(const o of[t,...v])s.has(o.id)||(s.add(o.id),c.push(o));he(!0),localStorage.setItem("musicRadio","true"),I(c),C.success(`Radio started — ${v.length} songs queued.`)},_e=t=>{if(S?.authenticated!==!0){C.error("Sign in to like songs.");return}const s=!Se(t);T(c=>({...c,[t.id]:s})),ue.mutate({id:t.id,like:s},{onError:()=>{T(c=>{const o={...c};return delete o[t.id],o}),C.error("Like action failed — try again.")}})},De=()=>{q(t=>(localStorage.setItem("musicShuffle",String(!t)),!t))},Ue=t=>e("button",{className:m.stationRow,onClick:()=>U(t),"aria-label":`Open ${t.title}`,children:[t.thumbnails?.[0]?.url?e("img",{src:t.thumbnails[0].url,alt:"",className:m.stationThumb,loading:"lazy",decoding:"async"}):e("span",{className:m.stationThumbPlaceholder,children:e(p,{name:"headphones"})}),e("span",{className:m.stationMeta,children:[e("span",{className:m.stationName,children:t.title}),t.subtitle&&e("span",{className:m.stationSub,children:t.subtitle})]})]},t.id),de=(t,s,c)=>{const o=l?.id===t.id,d=Se(t),u=[t.album,t.duration].filter(Boolean).join(" · ");return e("div",{className:`${m.stationRow} ${o?m.stationRowActive:""}`,style:{cursor:"default"},children:[e("button",{onClick:()=>c?c(t):Te(t,s),className:m.stationRow,style:{flex:1,minWidth:0,background:"transparent",border:0,padding:0},"aria-label":`Play ${t.title}`,children:[t.thumbnails?.[0]?.url?e("img",{src:t.thumbnails[0].url,alt:"",className:m.stationThumb,loading:"lazy",decoding:"async"}):e("span",{className:m.stationThumbPlaceholder,children:e(p,{name:"headphones"})}),e("span",{className:m.stationMeta,children:[e("span",{className:m.stationName,children:t.title}),e("span",{className:m.stationSub,children:[t.artists,u?` · ${u}`:""]})]})]}),S?.authenticated===!0&&e("button",{className:`${y.likeBtn} ${d?y.likeActive:""}`,onClick:()=>_e(t),disabled:ue.isPending,title:d?"Remove from Liked Music":"Add to Liked Music","aria-label":d?`Remove ${t.title} from Liked Music`:`Add ${t.title} to Liked Music`,"aria-pressed":d,children:e(p,{name:"heart"})}),e("a",{href:`https://music.youtube.com/watch?v=${encodeURIComponent(t.id)}`,target:"_blank",rel:"noreferrer",title:"Open in YouTube Music","aria-label":`Open ${t.title} in YouTube Music`,style:{padding:8,color:"inherit",opacity:.7,flexShrink:0},children:e(p,{name:"external-link-alt"})})]},t.id)},Pe=z(()=>{if(!l)return v;const t=new Set,s=[];for(const c of[l,...v])t.has(c.id)||(t.add(c.id),s.push(c));return s},[l,v]),He=t=>{Te(t,Pe)};return e("div",{className:`${i.page} ${l?i.pageWithPlayer:""}`,children:[e("div",{className:i.header,children:[e("h1",{className:i.pageTitle,children:[e(p,{name:"headphones"})," Music"]}),e("form",{className:i.searchForm,onSubmit:t=>{t.preventDefault();const s=$e(O);s?(Y(""),h({v:s})):Y(O)},children:[e("input",{className:i.searchInput,value:O,onChange:t=>be(t.target.value),placeholder:"Search YouTube Music…","aria-label":"Search music"}),e("button",{className:i.searchBtn,type:"submit","aria-label":"Search",children:e(p,{name:"search"})})]})]}),e("div",{className:y.authRow,children:[S?.authenticated?e(w,{children:[e("span",{className:i.statusMsg,children:["Signed in to YouTube Music",_&&` — ${_.tracks.length} songs, ${_.playlists.length} playlists`]}),e("button",{className:y.btnGhost,onClick:()=>E.mutate(),disabled:E.isPending,children:"Sign out"})]}):e(w,{children:[e("form",{onSubmit:t=>{t.preventDefault();const s=W.replace(/^cookie\s*:\s*/i,"").replace(/^["']+|["']+$/g,"").trim();if(s){if(s.includes("…")||s.includes("...")){C.error("That cookie is truncated (contains …). Right-click the Cookie header → Copy value, not the wrapped preview.");return}s!==W&&ae(s),f.mutate(s)}},className:y.cookieForm,children:[e("input",{className:y.cookieInput,value:W,onChange:t=>ae(t.target.value),placeholder:"Paste music.youtube.com Cookie header…","aria-label":"YouTube Music cookie",type:"password"}),e("button",{className:y.btn,type:"submit",disabled:f.isPending||!W.trim(),children:f.isPending?"Checking…":"Sign in"}),ee&&e("button",{className:y.btnGhost,type:"button",onClick:ve,disabled:V||f.isPending,title:"Read the YouTube Music cookie via the Dango Helper extension (same zip as AnimePahe/JapaneseASMR)",children:V?"Extracting…":"Extract from helper"})]}),ee&&e("p",{className:i.statusMsg,children:["Two ways to sign in: paste the Cookie header manually (steps below), or download"," ",e("a",{href:kt,target:"_blank",rel:"noreferrer",className:y.link,children:"dango-extension.zip"})," ","then"," ",X?e(w,{children:[e("code",{children:"about:debugging"})," → ",e("strong",{children:"This Firefox"})," →"," ",e("strong",{children:"Load Temporary Add-on"})," →"," ",e("code",{children:"dango-extension/firefox/manifest.json"})]}):e(w,{children:[e("code",{children:"chrome://extensions"})," → ",e("strong",{children:"Developer mode"})," →"," ",e("strong",{children:"Load unpacked"})," → ",e("code",{children:"dango-extension/chrome"})]}),", log in on a music.youtube.com tab, and click Extract."]}),e(je,{})]}),f.isSuccess&&e("p",{className:i.statusMsg,children:["Signed in — found ",f.data.tracks," songs and ",f.data.playlists," ","playlists."]}),f.isError&&e("p",{className:i.statusMsg,children:f.error?.message||"Sign-in failed. Paste a fresh Cookie header from music.youtube.com."}),!S?.authenticated&&!f.isError&&!f.isSuccess&&e("p",{className:i.statusMsg,children:"Sign in with your YouTube Music cookie to see your library. Search and playback work without sign-in."})]}),e("div",{className:l?y.contentLayout:void 0,children:[e("div",{className:y.mainCol,children:ie?e(w,{children:[e("h2",{className:m.sectionTitle,children:["Results for “",ie,"”"]}),ke?e("p",{className:i.statusMsg,children:"Searching…"}):Q.length===0?e("p",{className:i.statusMsg,children:"No tracks found."}):e("div",{className:m.stationList,children:Q.map(t=>de(t,Q))})]}):S?.authenticated?e(w,{children:[e("h2",{className:m.sectionTitle,children:"Your library"}),te?e("p",{className:i.statusMsg,children:"Loading library…"}):e(w,{children:[r.length>0&&!B&&e(w,{children:[e("h3",{className:m.sectionTitle,children:"Playlists"}),e("div",{className:m.stationList,children:r.map(t=>Ue(t))})]}),a.length>0&&!B&&e(w,{children:[e("h3",{className:m.sectionTitle,children:["Liked songs (",a.length,")"]}),e("div",{style:{marginBottom:8},children:e("button",{className:y.btnGhost,onClick:()=>Te(a[0],a),"aria-label":"Play all liked songs",children:[e(p,{name:"play"})," Play all"]})}),e("div",{className:m.stationList,children:a.map(t=>de(t,a))})]}),B?e(w,{children:[e("button",{className:y.btnGhost,onClick:()=>U(null),style:{marginBottom:8},children:"← Back to library"}),e("h3",{className:m.sectionTitle,children:B.title}),Ne?e("p",{className:i.statusMsg,children:"Loading playlist…"}):k.length===0?e("p",{className:i.statusMsg,children:"No tracks in this playlist."}):e("div",{className:m.stationList,children:k.map(t=>de(t,k))})]}):null]})]}):e("p",{className:i.statusMsg,children:"Search above, or sign in to see your library."})}),l&&e("aside",{className:y.upnextAside,"aria-label":"Up next",children:[e("div",{className:y.upnextHeader,children:e("span",{className:y.upnextTitle,children:[e(p,{name:"list-ul"})," Up Next"]})}),e("div",{className:y.nowPlayingWrap,children:[e("p",{className:y.nowPlayingLabel,children:"Now playing"}),de(l,D.length>0?D:[l])]}),e("div",{className:y.upnextActions,children:e("button",{className:y.btnGhost,onClick:qe,disabled:!R&&(ce||v.length===0),"aria-pressed":R,title:R?"Stop the radio — the queue will loop instead of fetching more songs":"Queue these suggestions after the current song and keep the mix going",children:R?"Stop radio":"Start radio"})}),ce?e("p",{className:i.statusMsg,children:"Loading suggestions…"}):v.length===0?e("p",{className:i.statusMsg,children:"No suggestions for this song."}):e("div",{className:m.stationList,children:v.map(t=>de(t,Pe,He))})]})]}),l&&e(Ye,{track:l,queue:D,shuffle:Z,onToggleShuffle:De,onTrackStep:Oe,onTrackFailed:Be,onClose:()=>{N(null),h({})},liked:l?Se(l):!1,likeVisible:S?.authenticated===!0,likePending:ue.isPending,onToggleLike:()=>_e(l)})]})};export{Lt as default};
|