@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.
Files changed (53) hide show
  1. package/README.md +1 -1
  2. package/client/dist/assets/{AnimeCard-D4qo4289.js → AnimeCard-URCiiy5W.js} +1 -1
  3. package/client/dist/assets/{AnimeInfo-BdRRm4So.js → AnimeInfo-e3z3er9t.js} +1 -1
  4. package/client/dist/assets/{AnimeInfoPage-DeJNAKMR.js → AnimeInfoPage-B7Djlmy_.js} +2 -2
  5. package/client/dist/assets/{Asmr-D0EGRYsv.js → Asmr-C28TfSvX.js} +1 -1
  6. package/client/dist/assets/{AsmrResetProgressModal-Gs0GmVaT.js → AsmrResetProgressModal-6Xh71AgR.js} +1 -1
  7. package/client/dist/assets/{AvSyncCalibrator-D8hqarVB.js → AvSyncCalibrator-BwCtRW0P.js} +1 -1
  8. package/client/dist/assets/{Home-BnBzjFkW.js → Home-BJdbHYja.js} +1 -1
  9. package/client/dist/assets/{Insights-i9QgY3nE.js → Insights-CjRy42YE.js} +1 -1
  10. package/client/dist/assets/{LibraryListPage-D3hTqTRU.js → LibraryListPage-B0qWqhb_.js} +1 -1
  11. package/client/dist/assets/{ListeningList-BgWsfu40.js → ListeningList-CSqD8j1C.js} +1 -1
  12. package/client/dist/assets/{Manga-Dn-CYins.js → Manga-Cg2YuObF.js} +1 -1
  13. package/client/dist/assets/{MangaInfoPage-EmQoK7Bv.js → MangaInfoPage-DcArhEsy.js} +1 -1
  14. package/client/dist/assets/{MangaReadPage-xJB8K2YU.js → MangaReadPage-YCETZRte.js} +1 -1
  15. package/client/dist/assets/{MangaResetProgressModal-Dian37PQ.js → MangaResetProgressModal-D_yT3d1v.js} +1 -1
  16. package/client/dist/assets/{Mature-DQtXKOXc.js → Mature-BGxRWkV5.js} +1 -1
  17. package/client/dist/assets/{MediaCard-DpKoY-1j.js → MediaCard-CAzoxsRS.js} +1 -1
  18. package/client/dist/assets/Music-Cgo-yQ7E.js +1 -0
  19. package/client/dist/assets/{OptionListMenu-Bj9ebsW3.js → OptionListMenu--Owe9Jle.js} +1 -1
  20. package/client/dist/assets/Player-yRXG2BrP.js +56 -0
  21. package/client/dist/assets/{PlayerSettings-DYZ272Yp.js → PlayerSettings-DrZLa0CC.js} +1 -1
  22. package/client/dist/assets/{QueueOptionsButton-lv26-6jp.js → QueueOptionsButton-BVZyPyKf.js} +1 -1
  23. package/client/dist/assets/{QueueRail-B7DeSFoA.js → QueueRail-B7f1-e6i.js} +1 -1
  24. package/client/dist/assets/{Radio-qQBYnVfY.js → Radio-BFjxlzuF.js} +1 -1
  25. package/client/dist/assets/{ReadingList-BT8kUjCB.js → ReadingList-BaIem8H1.js} +1 -1
  26. package/client/dist/assets/{ResetProgressModal-DWeiO-hs.js → ResetProgressModal-DES5Y-Fl.js} +1 -1
  27. package/client/dist/assets/{Search-BkvPYN33.js → Search-BOBo0Ogh.js} +1 -1
  28. package/client/dist/assets/{Settings-CyZEBF6S.js → Settings-DOvNQ0gX.js} +1 -1
  29. package/client/dist/assets/{SkeletonGrid-B5loz7ii.js → SkeletonGrid-BxfgjP4i.js} +1 -1
  30. package/client/dist/assets/{Trackers-JK-aXBYC.js → Trackers-DZ9KqOWv.js} +1 -1
  31. package/client/dist/assets/{Tv-BwXWJxMy.js → Tv-5mATEU4p.js} +1 -1
  32. package/client/dist/assets/{TvInfo-Dzyq6ucH.js → TvInfo-CF-5C5Fs.js} +1 -1
  33. package/client/dist/assets/{TvPopup-PlCU30FO.js → TvPopup-Dfqifj3s.js} +1 -1
  34. package/client/dist/assets/{TvSearch-BYwCf5kO.js → TvSearch-BWDwWnHe.js} +1 -1
  35. package/client/dist/assets/{TvWatchlist-BiJU6pKT.js → TvWatchlist-D3n5_4MI.js} +1 -1
  36. package/client/dist/assets/{Watchlist-BOEeGXH9.js → Watchlist-B8MHNsAU.js} +1 -1
  37. package/client/dist/assets/{hls-C11FOWHj.js → hls-BO_L1WoN.js} +1 -1
  38. package/client/dist/assets/{index-CtoKRNLc.js → index-D2RkUJIN.js} +2 -2
  39. package/client/dist/assets/{useAnimeInfoData-kr3WOIWH.js → useAnimeInfoData-BWShDHqE.js} +1 -1
  40. package/client/dist/assets/{useAsmr-CB9THf0m.js → useAsmr-FyfD9G7b.js} +1 -1
  41. package/client/dist/assets/{useAsmrPopup-cT_Uu41e.js → useAsmrPopup-D_o9-QlR.js} +1 -1
  42. package/client/dist/assets/{useMangaLibrary-rM6z3_0E.js → useMangaLibrary-BJU5x8Vz.js} +1 -1
  43. package/client/dist/assets/{useMangaPopup-PnmSctHu.js → useMangaPopup-TFzkC-tG.js} +1 -1
  44. package/client/dist/assets/{useProviders-B3VB7A9M.js → useProviders-DDRxa8vl.js} +1 -1
  45. package/client/dist/assets/{useTv-BTUKMH7Q.js → useTv-Bh6WmNnZ.js} +1 -1
  46. package/client/dist/assets/{useTvLibrary-C0sX-ilZ.js → useTvLibrary-_Pqm8re1.js} +1 -1
  47. package/client/dist/index.html +1 -1
  48. package/package.json +2 -2
  49. package/server/dist/controllers/music.controller.js +123 -50
  50. package/server/dist/lib/ytmusic.js +15 -2
  51. package/server/package.json +1 -1
  52. package/client/dist/assets/Music-BP43Zqmq.js +0 -1
  53. 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-CtoKRNLc.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};
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};
@@ -33,7 +33,7 @@
33
33
  background-color: #0c0d12;
34
34
  }
35
35
  </style>
36
- <script type="module" crossorigin src="/assets/index-CtoKRNLc.js"></script>
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.8",
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-25T17:32:09.105Z"
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 decipherAudio(videoId) {
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, returning empty');
559
- res.json({ tracks: [] });
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
- res.json(await readLibrary(yt));
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 panel = await yt.music.getUpNext(id);
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
- const panel = await authed.music.getUpNext(id);
622
- const tracks = [];
623
- const contents = panel.contents ?? [];
624
- for (const item of contents) {
625
- const track = upnextToTrack(item, true);
626
- if (track)
627
- tracks.push(track);
628
- if (tracks.length >= 50)
629
- break;
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
- const playlistId = panel.playlist_id ?? null;
632
- res.json({ tracks, playlistId });
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 tracks = await fetchPlaylistTracks(yt, 'LM', 500);
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, returning empty');
656
- res.json({ likedIds: [] });
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
- try {
669
- const actions = yt.actions ??
670
- yt.session?.actions;
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 yt.music.getUpNext(id);
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
- this.cache.delete('music-liked-ids');
690
- logger.info(`[music] rate id=${id} liked=${like} via=toggle`);
691
- res.json({ success: true, liked: like });
750
+ };
751
+ try {
752
+ await attempt(yt);
692
753
  }
693
754
  catch (err) {
694
- logger.error({ err }, '[music] rate failed');
695
- res.status(502).json({ error: 'Like action failed' });
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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dango-server",
3
3
  "private": true,
4
- "version": "3.3.8",
4
+ "version": "3.3.9",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22.12.0"
@@ -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};