mirror of
https://github.com/tamius-han/ghetto-karaoke-playlist.git
synced 2026-08-12 18:47:04 +02:00
Finish backend _in theory_ (we need more testing)
This commit is contained in:
parent
fec862e223
commit
e623ca717e
5
api/package-lock.json
generated
5
api/package-lock.json
generated
@ -1454,6 +1454,11 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@ -15,7 +15,8 @@
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.16.3",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
|
||||
@ -1,50 +1,75 @@
|
||||
import { pool as db } from "../../db";
|
||||
import { broadcastDisplayUpdate, broadcastUserUpdate } from '../ws/wsClients';
|
||||
|
||||
export async function lockSong(id: number, locked: boolean, res: any) {
|
||||
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" });
|
||||
|
||||
res.json({ song: result.rows[0] });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Server error" });
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveSong(id: number, position: number, res: any) {
|
||||
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" });
|
||||
|
||||
|
||||
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] });
|
||||
}
|
||||
|
||||
|
||||
export async function moveSong(id: string, position: number, res: any) {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
// Fetch the song
|
||||
const songRes = await db.query(`SELECT * FROM songs WHERE id=$1`, [id]);
|
||||
if (songRes.rowCount === 0) {
|
||||
await db.query("ROLLBACK");
|
||||
return res.status(404).json({ error: "Song not found" });
|
||||
}
|
||||
const song = songRes.rows[0];
|
||||
const updated = await moveSongDb(id, position);
|
||||
|
||||
// Update position
|
||||
await db.query(`UPDATE songs SET position=$1 WHERE id=$2`, [position, id]);
|
||||
|
||||
// Auto-adjust status based on movement
|
||||
if (song.status === 2) {
|
||||
// Moved from played → queued
|
||||
// Auto status adjustment
|
||||
if(updated.status === 2 && position < +(await db.query(`SELECT COUNT(*) FROM songs`))) {
|
||||
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]);
|
||||
res.json({ song: updatedRes.rows[0] });
|
||||
// If song became next to be played
|
||||
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) {
|
||||
await db.query("ROLLBACK");
|
||||
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" });
|
||||
|
||||
try {
|
||||
@ -73,38 +98,39 @@ export async function nextSong(res: any, lockCount: number = 0) {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
// 1️⃣ Find currently playing song
|
||||
// 1️⃣ Currently playing song → played
|
||||
const currentRes = await db.query(`SELECT * FROM songs WHERE status=1 ORDER BY position ASC LIMIT 1`);
|
||||
const currentSong = currentRes.rows[0];
|
||||
|
||||
if (currentSong) {
|
||||
// Mark current as played
|
||||
await db.query(`UPDATE songs SET status=2 WHERE id=$1`, [currentSong.id]);
|
||||
}
|
||||
|
||||
// 2️⃣ Find next queued song
|
||||
// 2️⃣ Next queued song → playing
|
||||
const nextRes = await db.query(`SELECT * FROM songs WHERE status=0 ORDER BY position ASC LIMIT 1`);
|
||||
const nextSong = nextRes.rows[0];
|
||||
|
||||
if (!nextSong) {
|
||||
await db.query("COMMIT");
|
||||
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]);
|
||||
|
||||
// 3️⃣ Apply lockCount
|
||||
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`,
|
||||
[lockCount]
|
||||
);
|
||||
for (const song of songsToLock.rows) {
|
||||
await db.query(`UPDATE songs SET locked=true WHERE id=$1`, [song.id]);
|
||||
for (const s of toLockRes.rows) {
|
||||
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");
|
||||
res.json({ playing: nextSong });
|
||||
} catch (err) {
|
||||
@ -118,30 +144,31 @@ export async function prevSong(res: any) {
|
||||
try {
|
||||
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 currentSong = currentRes.rows[0];
|
||||
|
||||
if (!currentSong) {
|
||||
await db.query("COMMIT");
|
||||
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(
|
||||
`SELECT * FROM songs WHERE status=2 AND position < $1 ORDER BY position DESC LIMIT 1`,
|
||||
[currentSong.position]
|
||||
);
|
||||
const prevSong = prevRes.rows[0];
|
||||
|
||||
if (!prevSong) {
|
||||
await db.query("COMMIT");
|
||||
return res.status(200).json({ message: "No previous song" });
|
||||
}
|
||||
|
||||
// Swap statuses
|
||||
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]); // now playing
|
||||
await db.query(`UPDATE songs SET status=0 WHERE id=$1`, [currentSong.id]);
|
||||
await db.query(`UPDATE songs SET status=1 WHERE id=$1`, [prevSong.id]);
|
||||
|
||||
// WebSocket updates
|
||||
broadcastDisplayUpdate("prevSong", prevSong);
|
||||
broadcastUserUpdate(prevSong.user_id, "yourSongIsNext", prevSong);
|
||||
|
||||
await db.query("COMMIT");
|
||||
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 {
|
||||
const result = await db.query(
|
||||
`DELETE FROM songs WHERE id=$1 RETURNING *`,
|
||||
[id]
|
||||
);
|
||||
const result = await db.query(`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] });
|
||||
} catch (err) {
|
||||
console.error("Error removing song:", err);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {v4 as uuid} from 'uuid';
|
||||
import { QueueSongRequest as QueueSongRequestData } from '../../itf.queue-song-request';
|
||||
import { pool as db } from '../../db';
|
||||
import { broadcastDisplayUpdate } from '../ws/wsClients';
|
||||
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
res.json({ song: result.rows[0] });
|
||||
const newSong = result.rows[0];
|
||||
broadcastDisplayUpdate("songAdded", newSong);
|
||||
res.json({ song: newSong });
|
||||
} catch (err) {
|
||||
console.error("Error adding song:", err);
|
||||
res.status(500).json({ error: "Server error" });
|
||||
|
||||
10
api/src/modules/ws/wsClients.ts
Normal file
10
api/src/modules/ws/wsClients.ts
Normal 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 })));
|
||||
}
|
||||
36
api/src/modules/ws/wsServer.ts
Normal file
36
api/src/modules/ws/wsServer.ts
Normal 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");
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user