feat: 스포티파이 재생목록 최대 50,000곡까지 불러오기
- 봇: partner API(fetchPlaylist/getAlbum, 토크너 익명 토큰)로 첫 100곡을 받아 바로 재생, 나머지는 1000곡씩(동시 3페이지) 백그라운드로 이어 붙임. 곡마다 앨범 커버 포함. 실패 시 임베드 페이지(100곡) → Lavalink 순으로 폴백 - 정지·끝·모두 지우기 시 queueEpoch 증가 → 이어 받기 중단. '-s'는 페이지마다 재생목록 곡끼리 재섞기 - 토크너 토큰 캐시를 정식 API 토큰 캐시와 분리 - queue_list: 앞 200곡 + total/totalMs만 전송, queue_set: windowSize 부분만 교체·clearAll 지원 - 대시보드: 곡 수·총 길이는 전체 기준, '외 N곡' 표시, 모두 지우기는 clearAll Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,8 @@ interface QueueListProps {
|
||||
*/
|
||||
export default function QueueList({ player, queue, variant }: QueueListProps) {
|
||||
const { track: nowPlaying, isPaused, autoplay, toggleAutoplay } = player;
|
||||
const { queue: items, moveTo, moveBy, playFrom, remove, shuffle, clear } = queue;
|
||||
// items 는 대기열 앞부분(최대 200곡)만, total·totalMs 는 대기열 전체 기준
|
||||
const { queue: items, total, totalMs, moveTo, moveBy, playFrom, remove, shuffle, clear } = queue;
|
||||
|
||||
// "모두 지우기"는 되돌릴 수 없어서 한 번 더 눌러야 실행한다(3초 안에).
|
||||
const [confirmClear, setConfirmClear] = useState(false);
|
||||
@@ -31,8 +32,8 @@ export default function QueueList({ player, queue, variant }: QueueListProps) {
|
||||
return () => clearTimeout(t);
|
||||
}, [confirmClear]);
|
||||
|
||||
const totalMs = items.reduce((sum, t) => sum + (Number(t.info?.length) || 0), 0);
|
||||
const totalMin = Math.round(totalMs / 60000);
|
||||
const hiddenCount = Math.max(total - items.length, 0);
|
||||
|
||||
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
@@ -62,8 +63,8 @@ export default function QueueList({ player, queue, variant }: QueueListProps) {
|
||||
<div className="min-w-0">
|
||||
{isPage && <h2 className="text-2xl font-bold">재생 목록</h2>}
|
||||
<p className="text-sm text-white/60">
|
||||
{items.length > 0
|
||||
? `다음 트랙 ${items.length}곡${totalMin > 0 ? ` · 약 ${totalMin}분` : ""}`
|
||||
{total > 0
|
||||
? `다음 트랙 ${total.toLocaleString()}곡${totalMin > 0 ? ` · 약 ${totalMin >= 60 ? `${Math.floor(totalMin / 60).toLocaleString()}시간 ${totalMin % 60}분` : `${totalMin}분`}` : ""}`
|
||||
: "다음 트랙 없음"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -71,7 +72,7 @@ export default function QueueList({ player, queue, variant }: QueueListProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={shuffle}
|
||||
disabled={items.length < 2}
|
||||
disabled={total < 2}
|
||||
title="다음 트랙 섞기"
|
||||
className="flex h-8 items-center gap-1.5 rounded-full bg-yt-chip px-3 text-sm text-white transition-colors hover:bg-yt-chip-hover disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
@@ -86,7 +87,7 @@ export default function QueueList({ player, queue, variant }: QueueListProps) {
|
||||
clear();
|
||||
} else setConfirmClear(true);
|
||||
}}
|
||||
disabled={items.length === 0}
|
||||
disabled={total === 0}
|
||||
title="다음 트랙 모두 지우기(지금 곡은 계속 재생)"
|
||||
className={`flex h-8 items-center gap-1.5 rounded-full px-3 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-40
|
||||
${confirmClear ? "bg-yt-red text-white" : "bg-yt-chip text-white hover:bg-yt-chip-hover"}`}
|
||||
@@ -157,7 +158,7 @@ export default function QueueList({ player, queue, variant }: QueueListProps) {
|
||||
|
||||
return (
|
||||
<li
|
||||
key={(item.encoded as string) || `${index}-${title}`}
|
||||
key={`${index}-${(item.encoded as string) || title}`}
|
||||
draggable
|
||||
onDragStart={() => {
|
||||
dragItem.current = index;
|
||||
@@ -253,6 +254,11 @@ export default function QueueList({ player, queue, variant }: QueueListProps) {
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{hiddenCount > 0 && (
|
||||
<p className="px-2 pt-3 text-center text-xs text-white/50">
|
||||
외 {hiddenCount.toLocaleString()}곡 · 앞의 곡이 재생되면 차례대로 보여요
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user