const { db } = require('../../db'); const insertStmt = db.prepare(` INSERT INTO playlists (title, description, cover_path, visibility, created_by) VALUES (@title, @description, @cover_path, @visibility, @created_by) `); const updateStmt = db.prepare(` UPDATE playlists SET title = @title, description = @description, visibility = @visibility WHERE id = @id `); const updateCoverStmt = db.prepare('UPDATE playlists SET cover_path = ? WHERE id = ?'); const setVisibilityStmt = db.prepare('UPDATE playlists SET visibility = ? WHERE id = ?'); const deleteStmt = db.prepare('DELETE FROM playlists WHERE id = ?'); const findByIdStmt = db.prepare('SELECT * FROM playlists WHERE id = ?'); // For list queries, effective visibility = max(playlist.visibility, most restrictive track). // Guests only see playlists where nothing in them is logged_in or private. const listGuestStmt = db.prepare(` SELECT p.* FROM playlists p WHERE p.visibility = 'public' AND NOT EXISTS ( SELECT 1 FROM playlist_songs ps JOIN songs s ON s.id = ps.song_id WHERE ps.playlist_id = p.id AND s.visibility IN ('logged_in', 'private') ) ORDER BY p.created_at DESC `); // Logged-in users see public/logged_in playlists where nothing in them is private. const listUserStmt = db.prepare(` SELECT p.* FROM playlists p WHERE p.visibility IN ('public', 'logged_in') AND NOT EXISTS ( SELECT 1 FROM playlist_songs ps JOIN songs s ON s.id = ps.song_id WHERE ps.playlist_id = p.id AND s.visibility = 'private' ) ORDER BY p.created_at DESC `); const listAllStmt = db.prepare('SELECT * FROM playlists ORDER BY created_at DESC'); const listByUserStmt = db.prepare('SELECT * FROM playlists WHERE created_by = ? ORDER BY created_at DESC'); const featuredGuestStmt = db.prepare(` SELECT p.* FROM playlists p WHERE p.visibility = 'public' AND NOT EXISTS ( SELECT 1 FROM playlist_songs ps JOIN songs s ON s.id = ps.song_id WHERE ps.playlist_id = p.id AND s.visibility IN ('logged_in', 'private') ) ORDER BY RANDOM() LIMIT ? `); const featuredUserStmt = db.prepare(` SELECT p.* FROM playlists p WHERE p.visibility IN ('public', 'logged_in') AND NOT EXISTS ( SELECT 1 FROM playlist_songs ps JOIN songs s ON s.id = ps.song_id WHERE ps.playlist_id = p.id AND s.visibility = 'private' ) ORDER BY RANDOM() LIMIT ? `); // Compute the single most-restrictive visibility for one playlist (stored + tracks). const effectiveVisibilityStmt = db.prepare(` SELECT CASE WHEN p.visibility = 'private' OR EXISTS (SELECT 1 FROM playlist_songs ps JOIN songs s ON s.id = ps.song_id WHERE ps.playlist_id = p.id AND s.visibility = 'private') THEN 'private' WHEN p.visibility = 'logged_in' OR EXISTS (SELECT 1 FROM playlist_songs ps JOIN songs s ON s.id = ps.song_id WHERE ps.playlist_id = p.id AND s.visibility = 'logged_in') THEN 'logged_in' ELSE 'public' END AS effective_visibility FROM playlists p WHERE p.id = ? `); const tracksStmt = db.prepare(` SELECT s.*, ps.position FROM playlist_songs ps JOIN songs s ON s.id = ps.song_id WHERE ps.playlist_id = ? ORDER BY ps.position ASC, s.title ASC `); const maxPosStmt = db.prepare('SELECT COALESCE(MAX(position), 0) AS m FROM playlist_songs WHERE playlist_id = ?'); const insertTrackStmt = db.prepare('INSERT OR IGNORE INTO playlist_songs (playlist_id, song_id, position) VALUES (?, ?, ?)'); const removeTrackStmt = db.prepare('DELETE FROM playlist_songs WHERE playlist_id = ? AND song_id = ?'); const clearTracksStmt = db.prepare('DELETE FROM playlist_songs WHERE playlist_id = ?'); const countStmt = db.prepare('SELECT COUNT(*) AS c FROM playlist_songs WHERE playlist_id = ?'); const creatorNameStmt = db.prepare('SELECT display_name FROM users WHERE id = ?'); function toView(row) { if (!row) return null; return { id: row.id, title: row.title, description: row.description, coverPath: row.cover_path, visibility: row.visibility || 'logged_in', createdBy: row.created_by, createdByName: row.created_by ? (creatorNameStmt.get(row.created_by)?.display_name || null) : null, createdAt: row.created_at, }; } function trackView(row) { return { id: row.id, title: row.title, artist: row.artist, album: row.album, durationSeconds: row.duration_seconds, coverPath: row.cover_path, visibility: row.visibility || 'logged_in', uploadedBy: row.uploaded_by, position: row.position, }; } function create(data) { const info = insertStmt.run({ title: data.title, description: data.description || null, cover_path: data.coverPath || null, visibility: data.visibility || 'logged_in', created_by: data.createdBy || null, }); return toView(findByIdStmt.get(info.lastInsertRowid)); } function update(id, data) { updateStmt.run({ id, title: data.title, description: data.description || null, visibility: data.visibility || 'logged_in', }); return toView(findByIdStmt.get(id)); } function setCover(id, coverPath) { updateCoverStmt.run(coverPath, id); } function setVisibility(id, visibility) { setVisibilityStmt.run(visibility, id); } function remove(id) { deleteStmt.run(id); } function findById(id) { return toView(findByIdStmt.get(id)); } function getEffectiveVisibility(id) { const row = effectiveVisibilityStmt.get(id); return row ? row.effective_visibility : null; } function listVisible(loggedIn = false) { return (loggedIn ? listUserStmt : listGuestStmt).all().map(toView); } function listAll() { return listAllStmt.all().map(toView); } function listByUser(userId) { return listByUserStmt.all(userId).map(toView); } function featured(n = 6, loggedIn = false) { return (loggedIn ? featuredUserStmt : featuredGuestStmt).all(n).map(toView); } function getTracks(playlistId) { return tracksStmt.all(playlistId).map(trackView); } function trackCount(playlistId) { return countStmt.get(playlistId).c; } function addTrack(playlistId, songId) { const nextPos = maxPosStmt.get(playlistId).m + 1; insertTrackStmt.run(playlistId, songId, nextPos); } function removeTrack(playlistId, songId) { removeTrackStmt.run(playlistId, songId); } function setOrder(playlistId, songIds) { const tx = db.transaction(() => { clearTracksStmt.run(playlistId); songIds.forEach((songId, idx) => insertTrackStmt.run(playlistId, songId, idx + 1)); }); tx(); } module.exports = { create, update, setCover, setVisibility, remove, findById, getEffectiveVisibility, listVisible, listAll, listByUser, featured, getTracks, trackCount, addTrack, removeTrack, setOrder, };