renamed all references of playlist to queue
This commit is contained in:
parent
df47dd46c3
commit
b0afc1cf5b
|
|
@ -65,7 +65,7 @@ All tracks are identified by a **content hash** (`sha256:` prefix + first 64KB h
|
||||||
|
|
||||||
Benefits:
|
Benefits:
|
||||||
- Deduplication (same file with different names = same track)
|
- Deduplication (same file with different names = same track)
|
||||||
- Renaming files without breaking playlists
|
- Renaming files without breaking queues
|
||||||
- Reliable client-side caching by content hash
|
- Reliable client-side caching by content hash
|
||||||
|
|
||||||
The client uses `track.id` for:
|
The client uses `track.id` for:
|
||||||
|
|
@ -149,7 +149,7 @@ interface Track {
|
||||||
channelId: string,
|
channelId: string,
|
||||||
description: string,
|
description: string,
|
||||||
paused: boolean,
|
paused: boolean,
|
||||||
queue: Track[], // alias: playlist (for compatibility)
|
queue: Track[],
|
||||||
currentIndex: number,
|
currentIndex: number,
|
||||||
listenerCount: number,
|
listenerCount: number,
|
||||||
listeners: string[], // usernames of connected users
|
listeners: string[], // usernames of connected users
|
||||||
|
|
@ -186,7 +186,7 @@ On WebSocket message:
|
||||||
|
|
||||||
Available in browser console:
|
Available in browser console:
|
||||||
```js
|
```js
|
||||||
MusicRoom.debugCacheMismatch() // Compare playlist IDs vs cached IDs
|
MusicRoom.debugCacheMismatch() // Compare queue IDs vs cached IDs
|
||||||
MusicRoom.debugTrack(index) // Detailed cache state for track at index
|
MusicRoom.debugTrack(index) // Detailed cache state for track at index
|
||||||
MusicRoom.debugCacheStatus() // Current track cache state
|
MusicRoom.debugCacheStatus() // Current track cache state
|
||||||
MusicRoom.clearAllCaches() // Clear IndexedDB and in-memory caches
|
MusicRoom.clearAllCaches() // Clear IndexedDB and in-memory caches
|
||||||
|
|
|
||||||
52
channel.ts
52
channel.ts
|
|
@ -22,7 +22,7 @@ export class Channel {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
playlist: Track[];
|
queue: Track[];
|
||||||
currentIndex: number = 0;
|
currentIndex: number = 0;
|
||||||
startedAt: number = Date.now();
|
startedAt: number = Date.now();
|
||||||
clients: Set<ServerWebSocket<WsData>> = new Set();
|
clients: Set<ServerWebSocket<WsData>> = new Set();
|
||||||
|
|
@ -31,22 +31,22 @@ export class Channel {
|
||||||
createdBy: number | null;
|
createdBy: number | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
private lastPlaylistBroadcast: number = 0;
|
private lastQueueBroadcast: number = 0;
|
||||||
private playlistDirty: boolean = false;
|
private queueDirty: boolean = false;
|
||||||
|
|
||||||
constructor(config: ChannelConfig) {
|
constructor(config: ChannelConfig) {
|
||||||
this.id = config.id;
|
this.id = config.id;
|
||||||
this.name = config.name;
|
this.name = config.name;
|
||||||
this.description = config.description || "";
|
this.description = config.description || "";
|
||||||
this.playlist = config.tracks;
|
this.queue = config.tracks;
|
||||||
this.createdBy = config.createdBy ?? null;
|
this.createdBy = config.createdBy ?? null;
|
||||||
this.createdAt = Date.now();
|
this.createdAt = Date.now();
|
||||||
this.isDefault = config.isDefault ?? false;
|
this.isDefault = config.isDefault ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentTrack(): Track | null {
|
get currentTrack(): Track | null {
|
||||||
if (this.playlist.length === 0) return null;
|
if (this.queue.length === 0) return null;
|
||||||
return this.playlist[this.currentIndex];
|
return this.queue[this.currentIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentTimestamp(): number {
|
get currentTimestamp(): number {
|
||||||
|
|
@ -66,13 +66,13 @@ export class Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
advance() {
|
advance() {
|
||||||
if (this.playlist.length === 0) return;
|
if (this.queue.length === 0) return;
|
||||||
this.currentIndex = (this.currentIndex + 1) % this.playlist.length;
|
this.currentIndex = (this.currentIndex + 1) % this.queue.length;
|
||||||
this.startedAt = Date.now();
|
this.startedAt = Date.now();
|
||||||
this.broadcast();
|
this.broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(includePlaylist: boolean = false) {
|
getState(includeQueue: boolean = false) {
|
||||||
const state: Record<string, unknown> = {
|
const state: Record<string, unknown> = {
|
||||||
track: this.currentTrack,
|
track: this.currentTrack,
|
||||||
currentTimestamp: this.currentTimestamp,
|
currentTimestamp: this.currentTimestamp,
|
||||||
|
|
@ -84,8 +84,8 @@ export class Channel {
|
||||||
listenerCount: this.clients.size,
|
listenerCount: this.clients.size,
|
||||||
isDefault: this.isDefault,
|
isDefault: this.isDefault,
|
||||||
};
|
};
|
||||||
if (includePlaylist) {
|
if (includeQueue) {
|
||||||
state.playlist = this.playlist;
|
state.queue = this.queue;
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +105,7 @@ export class Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
jumpTo(index: number) {
|
jumpTo(index: number) {
|
||||||
if (index < 0 || index >= this.playlist.length) return;
|
if (index < 0 || index >= this.queue.length) return;
|
||||||
this.currentIndex = index;
|
this.currentIndex = index;
|
||||||
if (this.paused) {
|
if (this.paused) {
|
||||||
this.pausedAt = 0;
|
this.pausedAt = 0;
|
||||||
|
|
@ -127,27 +127,27 @@ export class Channel {
|
||||||
this.broadcast();
|
this.broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
markPlaylistDirty() {
|
markQueueDirty() {
|
||||||
this.playlistDirty = true;
|
this.queueDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlaylist(tracks: Track[]) {
|
setQueue(tracks: Track[]) {
|
||||||
this.playlist = tracks;
|
this.queue = tracks;
|
||||||
this.currentIndex = 0;
|
this.currentIndex = 0;
|
||||||
this.startedAt = Date.now();
|
this.startedAt = Date.now();
|
||||||
this.pausedAt = 0;
|
this.pausedAt = 0;
|
||||||
this.playlistDirty = true;
|
this.queueDirty = true;
|
||||||
this.broadcast();
|
this.broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcast() {
|
broadcast() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const includePlaylist = this.playlistDirty || (now - this.lastPlaylistBroadcast >= 60000);
|
const includeQueue = this.queueDirty || (now - this.lastQueueBroadcast >= 60000);
|
||||||
if (includePlaylist) {
|
if (includeQueue) {
|
||||||
this.lastPlaylistBroadcast = now;
|
this.lastQueueBroadcast = now;
|
||||||
this.playlistDirty = false;
|
this.queueDirty = false;
|
||||||
}
|
}
|
||||||
const msg = JSON.stringify(this.getState(includePlaylist));
|
const msg = JSON.stringify(this.getState(includeQueue));
|
||||||
|
|
||||||
for (const ws of this.clients) {
|
for (const ws of this.clients) {
|
||||||
ws.send(msg);
|
ws.send(msg);
|
||||||
|
|
@ -158,10 +158,10 @@ export class Channel {
|
||||||
this.clients.add(ws);
|
this.clients.add(ws);
|
||||||
console.log(`[Channel] "${this.name}" added client, now ${this.clients.size} clients`);
|
console.log(`[Channel] "${this.name}" added client, now ${this.clients.size} clients`);
|
||||||
|
|
||||||
// Always send full state with playlist on connect
|
// Always send full state with queue on connect
|
||||||
ws.send(JSON.stringify(this.getState(true)));
|
ws.send(JSON.stringify(this.getState(true)));
|
||||||
// Reset timer so next playlist broadcast is in 60s
|
// Reset timer so next queue broadcast is in 60s
|
||||||
this.lastPlaylistBroadcast = Date.now();
|
this.lastQueueBroadcast = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeClient(ws: ServerWebSocket<WsData>) {
|
removeClient(ws: ServerWebSocket<WsData>) {
|
||||||
|
|
@ -175,7 +175,7 @@ export class Channel {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
trackCount: this.playlist.length,
|
trackCount: this.queue.length,
|
||||||
listenerCount: this.clients.size,
|
listenerCount: this.clients.size,
|
||||||
listeners,
|
listeners,
|
||||||
isDefault: this.isDefault,
|
isDefault: this.isDefault,
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
// Update cache status indicator
|
// Update cache status indicator
|
||||||
if (!M.cachedTracks.has(trackId)) {
|
if (!M.cachedTracks.has(trackId)) {
|
||||||
M.cachedTracks.add(trackId);
|
M.cachedTracks.add(trackId);
|
||||||
M.renderPlaylist();
|
M.renderQueue();
|
||||||
M.renderLibrary();
|
M.renderLibrary();
|
||||||
}
|
}
|
||||||
return blobUrl;
|
return blobUrl;
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
// Update cache status and re-render lists
|
// Update cache status and re-render lists
|
||||||
console.log("[Cache] Track cached:", trackId.slice(0, 16) + "...", "| size:", (data.byteLength / 1024 / 1024).toFixed(2) + "MB");
|
console.log("[Cache] Track cached:", trackId.slice(0, 16) + "...", "| size:", (data.byteLength / 1024 / 1024).toFixed(2) + "MB");
|
||||||
M.cachedTracks.add(trackId);
|
M.cachedTracks.add(trackId);
|
||||||
M.renderPlaylist();
|
M.renderQueue();
|
||||||
M.renderLibrary();
|
M.renderLibrary();
|
||||||
|
|
||||||
// Update download speed
|
// Update download speed
|
||||||
|
|
|
||||||
|
|
@ -236,14 +236,14 @@
|
||||||
const wasServerPaused = M.serverPaused;
|
const wasServerPaused = M.serverPaused;
|
||||||
M.serverPaused = data.paused ?? true;
|
M.serverPaused = data.paused ?? true;
|
||||||
|
|
||||||
// Update playlist if provided
|
// Update queue if provided
|
||||||
if (data.playlist) {
|
if (data.queue) {
|
||||||
M.playlist = data.playlist;
|
M.queue = data.queue;
|
||||||
M.currentIndex = data.currentIndex ?? 0;
|
M.currentIndex = data.currentIndex ?? 0;
|
||||||
M.renderPlaylist();
|
M.renderQueue();
|
||||||
} else if (data.currentIndex !== undefined && data.currentIndex !== M.currentIndex) {
|
} else if (data.currentIndex !== undefined && data.currentIndex !== M.currentIndex) {
|
||||||
M.currentIndex = data.currentIndex;
|
M.currentIndex = data.currentIndex;
|
||||||
M.renderPlaylist();
|
M.renderQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache track info for local mode - use track.id (content hash) as the identifier
|
// Cache track info for local mode - use track.id (content hash) as the identifier
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@
|
||||||
|
|
||||||
// Jump to a specific track index
|
// Jump to a specific track index
|
||||||
async function jumpToTrack(index) {
|
async function jumpToTrack(index) {
|
||||||
if (M.playlist.length === 0) return;
|
if (M.queue.length === 0) return;
|
||||||
const newIndex = (index + M.playlist.length) % M.playlist.length;
|
const newIndex = (index + M.queue.length) % M.queue.length;
|
||||||
|
|
||||||
if (M.synced && M.currentChannelId) {
|
if (M.synced && M.currentChannelId) {
|
||||||
const res = await fetch("/api/channels/" + M.currentChannelId + "/jump", {
|
const res = await fetch("/api/channels/" + M.currentChannelId + "/jump", {
|
||||||
|
|
@ -49,9 +49,9 @@
|
||||||
body: JSON.stringify({ index: newIndex })
|
body: JSON.stringify({ index: newIndex })
|
||||||
});
|
});
|
||||||
if (res.status === 403) M.flashPermissionDenied();
|
if (res.status === 403) M.flashPermissionDenied();
|
||||||
if (res.status === 400) console.warn("Jump failed: 400 - newIndex:", newIndex, "playlist length:", M.playlist.length);
|
if (res.status === 400) console.warn("Jump failed: 400 - newIndex:", newIndex, "queue length:", M.queue.length);
|
||||||
} else {
|
} else {
|
||||||
const track = M.playlist[newIndex];
|
const track = M.queue[newIndex];
|
||||||
const trackId = track.id || track.filename;
|
const trackId = track.id || track.filename;
|
||||||
M.currentIndex = newIndex;
|
M.currentIndex = newIndex;
|
||||||
M.currentTrackId = trackId;
|
M.currentTrackId = trackId;
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
M.audio.currentTime = 0;
|
M.audio.currentTime = 0;
|
||||||
M.localTimestamp = 0;
|
M.localTimestamp = 0;
|
||||||
M.audio.play();
|
M.audio.play();
|
||||||
M.renderPlaylist();
|
M.renderQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ window.MusicRoom = {
|
||||||
|
|
||||||
// Playback state
|
// Playback state
|
||||||
localTimestamp: 0,
|
localTimestamp: 0,
|
||||||
playlist: [],
|
queue: [],
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
|
|
||||||
// User state
|
// User state
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
console.log("M.trackBlobs keys:", [...M.trackBlobs.keys()]);
|
console.log("M.trackBlobs keys:", [...M.trackBlobs.keys()]);
|
||||||
console.log("M.bulkDownloadStarted:", M.bulkDownloadStarted);
|
console.log("M.bulkDownloadStarted:", M.bulkDownloadStarted);
|
||||||
console.log("=== Queue Tracks ===");
|
console.log("=== Queue Tracks ===");
|
||||||
M.playlist.forEach((t, i) => {
|
M.queue.forEach((t, i) => {
|
||||||
const id = t.id || t.filename;
|
const id = t.id || t.filename;
|
||||||
console.log(` [${i}] ${t.title?.slice(0, 30)} | id: ${id?.slice(0, 12)}... | cached: ${M.cachedTracks.has(id)}`);
|
console.log(` [${i}] ${t.title?.slice(0, 30)} | id: ${id?.slice(0, 12)}... | cached: ${M.cachedTracks.has(id)}`);
|
||||||
});
|
});
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
|
|
||||||
// Debug: detailed info for a specific track
|
// Debug: detailed info for a specific track
|
||||||
M.debugTrack = function(index) {
|
M.debugTrack = function(index) {
|
||||||
const track = M.playlist[index];
|
const track = M.queue[index];
|
||||||
if (!track) {
|
if (!track) {
|
||||||
console.log("[Debug] No track at index", index);
|
console.log("[Debug] No track at index", index);
|
||||||
return;
|
return;
|
||||||
|
|
@ -99,26 +99,26 @@
|
||||||
console.log("[Cache] All caches cleared. Refresh the page.");
|
console.log("[Cache] All caches cleared. Refresh the page.");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render the current queue (channel's playlist)
|
// Render the current queue
|
||||||
M.renderQueue = function() {
|
M.renderQueue = function() {
|
||||||
const container = M.$("#queue");
|
const container = M.$("#queue");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
if (M.playlist.length === 0) {
|
if (M.queue.length === 0) {
|
||||||
container.innerHTML = '<div class="empty">Queue empty</div>';
|
container.innerHTML = '<div class="empty">Queue empty</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: log first few track cache statuses
|
// Debug: log first few track cache statuses
|
||||||
if (M.playlist.length > 0 && M.cachedTracks.size > 0) {
|
if (M.queue.length > 0 && M.cachedTracks.size > 0) {
|
||||||
const sample = M.playlist.slice(0, 3).map(t => {
|
const sample = M.queue.slice(0, 3).map(t => {
|
||||||
const id = t.id || t.filename;
|
const id = t.id || t.filename;
|
||||||
return { title: t.title?.slice(0, 20), id: id?.slice(0, 12), cached: M.cachedTracks.has(id) };
|
return { title: t.title?.slice(0, 20), id: id?.slice(0, 12), cached: M.cachedTracks.has(id) };
|
||||||
});
|
});
|
||||||
console.log("[Queue Render] Sample tracks:", sample, "| cachedTracks sample:", [...M.cachedTracks].slice(0, 3).map(s => s.slice(0, 12)));
|
console.log("[Queue Render] Sample tracks:", sample, "| cachedTracks sample:", [...M.cachedTracks].slice(0, 3).map(s => s.slice(0, 12)));
|
||||||
}
|
}
|
||||||
|
|
||||||
M.playlist.forEach((track, i) => {
|
M.queue.forEach((track, i) => {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
const trackId = track.id || track.filename;
|
const trackId = track.id || track.filename;
|
||||||
const isCached = M.cachedTracks.has(trackId);
|
const isCached = M.cachedTracks.has(trackId);
|
||||||
|
|
@ -135,7 +135,7 @@
|
||||||
body: JSON.stringify({ index: i })
|
body: JSON.stringify({ index: i })
|
||||||
});
|
});
|
||||||
if (res.status === 403) M.flashPermissionDenied();
|
if (res.status === 403) M.flashPermissionDenied();
|
||||||
if (res.status === 400) console.warn("Jump failed: 400 - index:", i, "queue length:", M.playlist.length);
|
if (res.status === 400) console.warn("Jump failed: 400 - index:", i, "queue length:", M.queue.length);
|
||||||
} else {
|
} else {
|
||||||
M.currentIndex = i;
|
M.currentIndex = i;
|
||||||
M.currentTrackId = trackId;
|
M.currentTrackId = trackId;
|
||||||
|
|
@ -155,9 +155,6 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Alias for backward compatibility
|
|
||||||
M.renderPlaylist = M.renderQueue;
|
|
||||||
|
|
||||||
// Render the library
|
// Render the library
|
||||||
M.renderLibrary = function() {
|
M.renderLibrary = function() {
|
||||||
const container = M.$("#library");
|
const container = M.$("#library");
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ h3 { font-size: 0.9rem; color: #666; margin-bottom: 0.5rem; text-transform: uppe
|
||||||
#auth-section .admin-badge { background: #c4f; color: #111; padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.7rem; }
|
#auth-section .admin-badge { background: #c4f; color: #111; padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.7rem; }
|
||||||
#stream-select select { background: #222; color: #eee; border: 1px solid #333; padding: 0.4rem 0.8rem; border-radius: 4px; font-size: 0.9rem; }
|
#stream-select select { background: #222; color: #eee; border: 1px solid #333; padding: 0.4rem 0.8rem; border-radius: 4px; font-size: 0.9rem; }
|
||||||
|
|
||||||
/* Main content - library and playlist */
|
/* Main content - library and queue */
|
||||||
#main-content { display: flex; gap: 1rem; flex: 1; min-height: 0; margin-bottom: 1rem; }
|
#main-content { display: flex; gap: 1rem; flex: 1; min-height: 0; margin-bottom: 1rem; }
|
||||||
#channels-panel { flex: 0 0 180px; background: #1a1a1a; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; min-height: 300px; max-height: 60vh; }
|
#channels-panel { flex: 0 0 180px; background: #1a1a1a; border-radius: 8px; padding: 1rem; display: flex; flex-direction: column; min-height: 300px; max-height: 60vh; }
|
||||||
#channels-list { flex: 1; overflow-y: auto; }
|
#channels-list { flex: 1; overflow-y: auto; }
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@
|
||||||
const currentSize = M.cachedTracks.size;
|
const currentSize = M.cachedTracks.size;
|
||||||
if (currentSize !== lastCacheSize) {
|
if (currentSize !== lastCacheSize) {
|
||||||
lastCacheSize = currentSize;
|
lastCacheSize = currentSize;
|
||||||
M.renderPlaylist();
|
M.renderQueue();
|
||||||
M.renderLibrary();
|
M.renderLibrary();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ console.log(`Config loaded: port=${config.port}, musicDir=${MUSIC_DIR}, allowGue
|
||||||
// Initialize library
|
// Initialize library
|
||||||
const library = new Library(MUSIC_DIR);
|
const library = new Library(MUSIC_DIR);
|
||||||
|
|
||||||
// Auto-discover tracks if playlist is empty
|
// Auto-discover tracks if queue is empty
|
||||||
async function discoverTracks(): Promise<string[]> {
|
async function discoverTracks(): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const files = await readdir(MUSIC_DIR);
|
const files = await readdir(MUSIC_DIR);
|
||||||
|
|
@ -480,7 +480,7 @@ serve({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// API: jump to track in playlist
|
// API: jump to track in queue
|
||||||
const jumpMatch = path.match(/^\/api\/channels\/([^/]+)\/jump$/);
|
const jumpMatch = path.match(/^\/api\/channels\/([^/]+)\/jump$/);
|
||||||
if (jumpMatch && req.method === "POST") {
|
if (jumpMatch && req.method === "POST") {
|
||||||
const channelId = jumpMatch[1];
|
const channelId = jumpMatch[1];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue