Finish backend _in theory_ (we need more testing)

This commit is contained in:
Tamius Han 2025-12-09 22:52:33 +01:00
parent fec862e223
commit e623ca717e
7 changed files with 144 additions and 59 deletions

5
api/package-lock.json generated
View File

@ -1454,6 +1454,11 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true "dev": true
}, },
"ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="
},
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -15,7 +15,8 @@
"express": "^4.18.2", "express": "^4.18.2",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"pg": "^8.16.3", "pg": "^8.16.3",
"uuid": "^9.0.1" "uuid": "^9.0.1",
"ws": "^8.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",

View File

@ -1,50 +1,75 @@
import { pool as db } from "../../db"; import { pool as db } from "../../db";
import { broadcastDisplayUpdate, broadcastUserUpdate } from '../ws/wsClients';
export async function lockSong(id: number, locked: boolean, res: any) {
async function normalizePositions() {
const songs = await db.query(`SELECT id FROM songs ORDER BY position ASC`);
let pos = 0;
for (const s of songs.rows) {
await db.query(`UPDATE songs SET position=$1 WHERE id=$2`, [pos, s.id]);
pos++;
}
}
async function moveSongDb(id: string, newPos: number) {
const allSongs = await db.query(`SELECT id FROM songs ORDER BY position ASC`);
const songIds = allSongs.rows.map((s: any) => s.id);
const oldIndex = songIds.indexOf(id);
if (oldIndex === -1) throw new Error("Song not found");
songIds.splice(oldIndex, 1); // remove from old position
songIds.splice(newPos, 0, id); // insert at new position
// Update positions in DB
await db.query("BEGIN");
for (let i = 0; i < songIds.length; i++) {
await db.query(`UPDATE songs SET position=$1 WHERE id=$2`, [i, songIds[i]]);
}
await db.query("COMMIT");
// Return updated song info
const res = await db.query(`SELECT * FROM songs WHERE id=$1`, [id]);
return res.rows[0];
}
export async function lockSong(id: string, locked: boolean, res: any) {
if (typeof locked !== "boolean") return res.status(400).json({ error: "locked must be boolean" }); if (typeof locked !== "boolean") return res.status(400).json({ error: "locked must be boolean" });
try {
const result = await db.query(
`UPDATE songs SET locked=$1 WHERE id=$2 RETURNING *`,
[locked, id]
);
if (result.rowCount === 0) return res.status(404).json({ error: "Song not found" });
const result = await db.query(`UPDATE songs SET locked=$1 WHERE id=$2 RETURNING *`, [locked,id]);
if(result.rowCount === 0) {
return res.status(404).json({ error:"Song not found" });
}
broadcastDisplayUpdate("songLocked", result.rows[0]);
broadcastUserUpdate(result.rows[0].user_id,"yourSongLocked", result.rows[0]);
res.json({ song: result.rows[0] }); res.json({ song: result.rows[0] });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Server error" });
}
} }
export async function moveSong(id: number, position: number, res: any) {
export async function moveSong(id: string, position: number, res: any) {
try { try {
await db.query("BEGIN"); await db.query("BEGIN");
// Fetch the song // Fetch the song
const songRes = await db.query(`SELECT * FROM songs WHERE id=$1`, [id]); const updated = await moveSongDb(id, position);
if (songRes.rowCount === 0) {
await db.query("ROLLBACK");
return res.status(404).json({ error: "Song not found" });
}
const song = songRes.rows[0];
// Update position // Auto status adjustment
await db.query(`UPDATE songs SET position=$1 WHERE id=$2`, [position, id]); if(updated.status === 2 && position < +(await db.query(`SELECT COUNT(*) FROM songs`))) {
// Auto-adjust status based on movement
if (song.status === 2) {
// Moved from played → queued
await db.query(`UPDATE songs SET status=0 WHERE id=$1`, [id]); await db.query(`UPDATE songs SET status=0 WHERE id=$1`, [id]);
} else if (song.status === 0 && position === 0) {
// Optional: if moved to top, mark as playing
await db.query(`UPDATE songs SET status=1 WHERE id=$1`, [id]);
} }
await db.query("COMMIT"); broadcastDisplayUpdate("songMoved", updated);
broadcastUserUpdate(updated.user_id,"yourSongPositionChanged", updated);
const updatedRes = await db.query(`SELECT * FROM songs WHERE id=$1`, [id]); // If song became next to be played
res.json({ song: updatedRes.rows[0] }); const nextSongRes = await db.query(`SELECT * FROM songs WHERE status=0 ORDER BY position ASC LIMIT 1`);
if(nextSongRes.rowCount !== null && nextSongRes.rowCount > 0 && nextSongRes.rows[0].id === id) {
broadcastUserUpdate(updated.user_id,"yourSongIsNext", updated);
}
res.json({ song: updated });
} catch (err) { } catch (err) {
await db.query("ROLLBACK"); await db.query("ROLLBACK");
console.error("Error moving song:", err); console.error("Error moving song:", err);
@ -52,7 +77,7 @@ export async function moveSong(id: number, position: number, res: any) {
} }
} }
export async function changeSongStatus(id: number, status: number, res: any) { export async function changeSongStatus(id: string, status: number, res: any) {
if (![0, 1, 2].includes(status)) return res.status(400).json({ error: "Invalid status" }); if (![0, 1, 2].includes(status)) return res.status(400).json({ error: "Invalid status" });
try { try {
@ -73,38 +98,39 @@ export async function nextSong(res: any, lockCount: number = 0) {
try { try {
await db.query("BEGIN"); await db.query("BEGIN");
// 1Find currently playing song // 1Currently playing song → played
const currentRes = await db.query(`SELECT * FROM songs WHERE status=1 ORDER BY position ASC LIMIT 1`); const currentRes = await db.query(`SELECT * FROM songs WHERE status=1 ORDER BY position ASC LIMIT 1`);
const currentSong = currentRes.rows[0]; const currentSong = currentRes.rows[0];
if (currentSong) { if (currentSong) {
// Mark current as played
await db.query(`UPDATE songs SET status=2 WHERE id=$1`, [currentSong.id]); await db.query(`UPDATE songs SET status=2 WHERE id=$1`, [currentSong.id]);
} }
// 2Find next queued song // 2Next queued song → playing
const nextRes = await db.query(`SELECT * FROM songs WHERE status=0 ORDER BY position ASC LIMIT 1`); const nextRes = await db.query(`SELECT * FROM songs WHERE status=0 ORDER BY position ASC LIMIT 1`);
const nextSong = nextRes.rows[0]; const nextSong = nextRes.rows[0];
if (!nextSong) { if (!nextSong) {
await db.query("COMMIT"); await db.query("COMMIT");
return res.status(200).json({ message: "No more queued songs" }); return res.status(200).json({ message: "No more queued songs" });
} }
// Mark next as playing
await db.query(`UPDATE songs SET status=1 WHERE id=$1`, [nextSong.id]); await db.query(`UPDATE songs SET status=1 WHERE id=$1`, [nextSong.id]);
// 3⃣ Apply lockCount // 3⃣ Apply lockCount
if (lockCount > 0) { if (lockCount > 0) {
const songsToLock = await db.query( const toLockRes = await db.query(
`SELECT * FROM songs WHERE status=0 ORDER BY position ASC LIMIT $1`, `SELECT * FROM songs WHERE status=0 ORDER BY position ASC LIMIT $1`,
[lockCount] [lockCount]
); );
for (const song of songsToLock.rows) { for (const s of toLockRes.rows) {
await db.query(`UPDATE songs SET locked=true WHERE id=$1`, [song.id]); await db.query(`UPDATE songs SET locked=true WHERE id=$1`, [s.id]);
broadcastUserUpdate(s.user_id, "yourSongLocked", s);
} }
} }
// 4⃣ WebSocket updates
broadcastDisplayUpdate("nextSong", nextSong);
broadcastUserUpdate(nextSong.user_id, "yourSongIsNext", nextSong);
await db.query("COMMIT"); await db.query("COMMIT");
res.json({ playing: nextSong }); res.json({ playing: nextSong });
} catch (err) { } catch (err) {
@ -118,30 +144,31 @@ export async function prevSong(res: any) {
try { try {
await db.query("BEGIN"); await db.query("BEGIN");
// Find currently playing song // 1⃣ Currently playing song → queued
const currentRes = await db.query(`SELECT * FROM songs WHERE status=1 ORDER BY position ASC LIMIT 1`); const currentRes = await db.query(`SELECT * FROM songs WHERE status=1 ORDER BY position ASC LIMIT 1`);
const currentSong = currentRes.rows[0]; const currentSong = currentRes.rows[0];
if (!currentSong) { if (!currentSong) {
await db.query("COMMIT"); await db.query("COMMIT");
return res.status(200).json({ message: "No song currently playing" }); return res.status(200).json({ message: "No song currently playing" });
} }
// Find previous played song (highest position < current) // 2⃣ Previous played song → playing
const prevRes = await db.query( const prevRes = await db.query(
`SELECT * FROM songs WHERE status=2 AND position < $1 ORDER BY position DESC LIMIT 1`, `SELECT * FROM songs WHERE status=2 AND position < $1 ORDER BY position DESC LIMIT 1`,
[currentSong.position] [currentSong.position]
); );
const prevSong = prevRes.rows[0]; const prevSong = prevRes.rows[0];
if (!prevSong) { if (!prevSong) {
await db.query("COMMIT"); await db.query("COMMIT");
return res.status(200).json({ message: "No previous song" }); return res.status(200).json({ message: "No previous song" });
} }
// Swap statuses await db.query(`UPDATE songs SET status=0 WHERE id=$1`, [currentSong.id]);
await db.query(`UPDATE songs SET status=0 WHERE id=$1`, [currentSong.id]); // back to queued await db.query(`UPDATE songs SET status=1 WHERE id=$1`, [prevSong.id]);
await db.query(`UPDATE songs SET status=1 WHERE id=$1`, [prevSong.id]); // now playing
// WebSocket updates
broadcastDisplayUpdate("prevSong", prevSong);
broadcastUserUpdate(prevSong.user_id, "yourSongIsNext", prevSong);
await db.query("COMMIT"); await db.query("COMMIT");
res.json({ playing: prevSong }); res.json({ playing: prevSong });
@ -152,15 +179,18 @@ export async function prevSong(res: any) {
} }
} }
export async function removeSong(id: number, res: any) { export async function removeSong(id: string, res: any) {
try { try {
const result = await db.query( const result = await db.query(`DELETE FROM songs WHERE id=$1 RETURNING *`, [id]);
`DELETE FROM songs WHERE id=$1 RETURNING *`,
[id]
);
if (result.rowCount === 0) return res.status(404).json({ error: "Song not found" }); if (result.rowCount===0) {
return res.status(404).json({ error:"Song not found" });
}
await normalizePositions();
broadcastDisplayUpdate("songRemoved", result.rows[0]);
broadcastUserUpdate(result.rows[0].user_id,"yourSongRemoved", result.rows[0]);
res.json({ removed: result.rows[0] }); res.json({ removed: result.rows[0] });
} catch (err) { } catch (err) {
console.error("Error removing song:", err); console.error("Error removing song:", err);

View File

@ -1,5 +1,5 @@
import { Router } from "express"; import { Router } from "express";
import { requireRole, AuthRequest } from "./middleware"; import { requireRole, AuthRequest } from "../auth/auth-middleware";
import { changeSongStatus, clearPlaylist, lockSong, moveSong, nextSong, prevSong, removeSong } from './dj'; import { changeSongStatus, clearPlaylist, lockSong, moveSong, nextSong, prevSong, removeSong } from './dj';
const router = Router(); const router = Router();

View File

@ -1,6 +1,7 @@
import {v4 as uuid} from 'uuid'; import {v4 as uuid} from 'uuid';
import { QueueSongRequest as QueueSongRequestData } from '../../itf.queue-song-request'; import { QueueSongRequest as QueueSongRequestData } from '../../itf.queue-song-request';
import { pool as db } from '../../db'; import { pool as db } from '../../db';
import { broadcastDisplayUpdate } from '../ws/wsClients';
export async function getId(res: any) { export async function getId(res: any) {
@ -49,7 +50,9 @@ export async function queueSong(requestData: QueueSongRequestData, res: any) {
[artist, title, comment || "", youtubeLink || "", userId, fingerprint, locked, position] [artist, title, comment || "", youtubeLink || "", userId, fingerprint, locked, position]
); );
res.json({ song: result.rows[0] }); const newSong = result.rows[0];
broadcastDisplayUpdate("songAdded", newSong);
res.json({ song: newSong });
} catch (err) { } catch (err) {
console.error("Error adding song:", err); console.error("Error adding song:", err);
res.status(500).json({ error: "Server error" }); res.status(500).json({ error: "Server error" });

View File

@ -0,0 +1,10 @@
import { clients } from "./wsServer";
export function broadcastDisplayUpdate(event: string, payload: any) {
clients.filter(c => c.type === "display").forEach(c => c.ws.send(JSON.stringify({ event, payload })));
}
export function broadcastUserUpdate(userId: string, event: string, payload: any) {
clients.filter(c => c.type === "user" && c.userId === userId)
.forEach(c => c.ws.send(JSON.stringify({ event, payload })));
}

View File

@ -0,0 +1,36 @@
import { Server as HTTPServer } from "http";
import WebSocket, { WebSocketServer } from "ws";
// Keep track of connected clients
interface Client {
ws: WebSocket;
type: "display" | "user";
userId?: string;
}
export const clients: Client[] = [];
export function setupWsServer(server: HTTPServer) {
const wss = new WebSocketServer({ server });
wss.on("connection", (ws, req) => {
// Example: client can send type info after connecting
ws.on("message", (msg) => {
try {
const data = JSON.parse(msg.toString());
if (data.type === "register") {
const type = data.view === "display" ? "display" : "user";
const userId = type === "user" ? data.userId : undefined;
clients.push({ ws, type, userId });
}
} catch {}
});
ws.on("close", () => {
const idx = clients.findIndex(c => c.ws === ws);
if (idx !== -1) clients.splice(idx, 1);
});
});
console.log("WebSocket server running");
}