Bri-Tunes/src/services/playlists.js

206 lines
6.5 KiB
JavaScript

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,
};