Add VIP visibility tier for songs and playlists
- Introduce `vip` visibility level with CSS badge styling - Update admin routes to restrict VIP setting to admins and VIP users - Extend visibility logic in public routes (`canView`, `denyAccess`) - Add VIP-specific SQL queries in playlists, social, and songs services - Update EJS templates to show VIP badges and radio options for authorized users
This commit is contained in:
parent
5fd5ee6351
commit
771165f042
|
|
@ -392,6 +392,7 @@ button.linklike.danger:hover { color: #fff; text-shadow: 0 0 6px var(--danger);
|
|||
}
|
||||
.badge-public { color: var(--neon-cyan); border: 1px solid var(--neon-cyan); box-shadow: 0 0 5px rgba(0,229,255,0.35); }
|
||||
.badge-logged-in { color: var(--neon-amber); border: 1px solid var(--neon-amber); box-shadow: 0 0 5px rgba(255,179,71,0.4); }
|
||||
.badge-vip { color: #39ff14; border: 1px solid #39ff14; box-shadow: 0 0 5px rgba(57,255,20,0.5); background: rgba(57,255,20,0.08); }
|
||||
.badge-private { color: var(--neon-magenta); border: 1px solid var(--neon-magenta); box-shadow: 0 0 5px rgba(255,43,214,0.4); }
|
||||
|
||||
/* ---------- Visibility radio fieldset ---------- */
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ const upload = multer({
|
|||
const songFields = upload.fields([{ name: 'audio', maxCount: 1 }, { name: 'cover', maxCount: 1 }]);
|
||||
const playlistFields = upload.fields([{ name: 'cover', maxCount: 1 }]);
|
||||
|
||||
const VISIBILITY_VALUES = ['public', 'logged_in', 'private'];
|
||||
const VISIBILITY_RANK = { public: 0, logged_in: 1, private: 2 };
|
||||
const VISIBILITY_VALUES = ['public', 'logged_in', 'vip', 'private'];
|
||||
const VISIBILITY_RANK = { public: 0, logged_in: 1, vip: 2, private: 3 };
|
||||
|
||||
// For uploads, all text fields are optional — we'll fill blanks from ID3 tags.
|
||||
// The edit form enforces title/artist via a stricter schema below.
|
||||
|
|
@ -258,6 +258,8 @@ module.exports = function adminRoutes(csrfProtection) {
|
|||
}
|
||||
}
|
||||
|
||||
const canSetVip = req.session.user.isVip || req.session.user.role === 'admin';
|
||||
const visibility = parsed.data.visibility === 'vip' && !canSetVip ? 'logged_in' : parsed.data.visibility;
|
||||
songs.create({
|
||||
title: merged.title,
|
||||
artist: merged.artist,
|
||||
|
|
@ -267,7 +269,7 @@ module.exports = function adminRoutes(csrfProtection) {
|
|||
durationSeconds: tags.duration,
|
||||
audioPath: relMedia(audioFile, 'audio'),
|
||||
coverPath,
|
||||
visibility: parsed.data.visibility,
|
||||
visibility,
|
||||
uploadedBy: req.session.user.id,
|
||||
});
|
||||
req.flash('success', 'Song uploaded. Tags were read from the file — review and edit if needed.');
|
||||
|
|
@ -299,13 +301,15 @@ module.exports = function adminRoutes(csrfProtection) {
|
|||
if (req.files?.cover?.[0]) unlinkQuiet(req.files.cover[0].path);
|
||||
return res.status(400).render('mymusic/song_form', { title: 'Edit song', song: existing, errors, values: body, genres: songs.listGenres() });
|
||||
}
|
||||
const canSetVip = req.session.user.isVip || req.session.user.role === 'admin';
|
||||
const visibility = parsed.data.visibility === 'vip' && !canSetVip ? 'logged_in' : parsed.data.visibility;
|
||||
songs.update(id, {
|
||||
title: parsed.data.title,
|
||||
artist: parsed.data.artist,
|
||||
album: parsed.data.album || null,
|
||||
genre: parsed.data.genre || null,
|
||||
year: parsed.data.year ?? null,
|
||||
visibility: parsed.data.visibility,
|
||||
visibility,
|
||||
});
|
||||
const coverFile = req.files?.cover?.[0];
|
||||
if (coverFile) {
|
||||
|
|
@ -369,11 +373,13 @@ module.exports = function adminRoutes(csrfProtection) {
|
|||
console.warn('[admin] failed to process playlist cover:', err.message);
|
||||
}
|
||||
}
|
||||
const canSetVip = req.session.user.isVip || req.session.user.role === 'admin';
|
||||
const visibility = parsed.data.visibility === 'vip' && !canSetVip ? 'logged_in' : parsed.data.visibility;
|
||||
const created = playlists.create({
|
||||
title: parsed.data.title,
|
||||
description: parsed.data.description || null,
|
||||
coverPath,
|
||||
visibility: parsed.data.visibility,
|
||||
visibility,
|
||||
createdBy: req.session.user.id,
|
||||
});
|
||||
const preselectIds = [].concat(req.body['preselectIds[]'] || []).map(Number).filter(Boolean);
|
||||
|
|
@ -426,10 +432,12 @@ module.exports = function adminRoutes(csrfProtection) {
|
|||
errors, values: req.body,
|
||||
});
|
||||
}
|
||||
const canSetVip = req.session.user.isVip || req.session.user.role === 'admin';
|
||||
const visibility = parsed.data.visibility === 'vip' && !canSetVip ? 'logged_in' : parsed.data.visibility;
|
||||
playlists.update(id, {
|
||||
title: parsed.data.title,
|
||||
description: parsed.data.description || null,
|
||||
visibility: parsed.data.visibility,
|
||||
visibility,
|
||||
});
|
||||
const coverFile = req.files?.cover?.[0];
|
||||
if (coverFile) {
|
||||
|
|
|
|||
|
|
@ -18,50 +18,58 @@ const MIME = {
|
|||
};
|
||||
|
||||
// Can the current viewer access an item with this visibility/owner?
|
||||
function canView(visibility, ownerId, userId) {
|
||||
function canView(visibility, ownerId, userId, isVip = false) {
|
||||
if (visibility === 'public') return true;
|
||||
if (visibility === 'logged_in') return !!userId;
|
||||
if (visibility === 'vip') return !!userId && isVip;
|
||||
if (visibility === 'private') return userId != null && ownerId === userId;
|
||||
return false;
|
||||
}
|
||||
|
||||
// For members-only items, redirect guests to login rather than 404.
|
||||
// VIP items redirect non-VIP logged-in users with a flash message.
|
||||
// Private items always 404 (don't reveal they exist).
|
||||
function denyAccess(req, res, next, visibility) {
|
||||
if (visibility === 'logged_in' && !req.session.user) {
|
||||
if ((visibility === 'logged_in' || visibility === 'vip') && !req.session.user) {
|
||||
req.session.returnTo = req.originalUrl;
|
||||
req.flash('error', 'You must be logged in to access that.');
|
||||
return res.redirect('/login');
|
||||
}
|
||||
if (visibility === 'vip' && req.session.user) {
|
||||
req.flash('error', 'VIP access required.');
|
||||
return res.redirect('/songs');
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
const loggedIn = !!req.session.user;
|
||||
const userId = req.session.user?.id || null;
|
||||
const isVip = !!(req.session.user?.isVip || req.session.user?.role === 'admin');
|
||||
res.render('public/home', {
|
||||
title: res.locals.siteName,
|
||||
featured: social.enrichPlaylists(playlists.featured(5, loggedIn), userId),
|
||||
recent: social.enrichSongs(songs.recent(8, loggedIn), userId),
|
||||
featured: social.enrichPlaylists(playlists.featured(5, loggedIn, isVip), userId),
|
||||
recent: social.enrichSongs(songs.recent(8, loggedIn, isVip), userId),
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/songs', (req, res) => {
|
||||
const loggedIn = !!req.session.user;
|
||||
const userId = req.session.user?.id || null;
|
||||
const isVip = !!(req.session.user?.isVip || req.session.user?.role === 'admin');
|
||||
const page = Math.max(1, Number(req.query.page) || 1);
|
||||
const pageSize = 25;
|
||||
const q = req.query.q || '';
|
||||
const genre = req.query.genre || '';
|
||||
const uploadedBy = req.query.uploader || '';
|
||||
|
||||
const { rows, total } = songs.list({ q, genre, uploadedBy, limit: pageSize, offset: (page - 1) * pageSize, loggedIn });
|
||||
const { rows, total } = songs.list({ q, genre, uploadedBy, limit: pageSize, offset: (page - 1) * pageSize, loggedIn, isVip });
|
||||
social.enrichSongs(rows, userId);
|
||||
|
||||
const genres = songs.listGenres();
|
||||
const uploaders = songs.listUploaders(loggedIn);
|
||||
const uploaders = songs.listUploaders(loggedIn, isVip);
|
||||
|
||||
const recentlyLiked = social.getRecentlyLikedSongs(5, loggedIn).map(songs.toView);
|
||||
const recentlyLiked = social.getRecentlyLikedSongs(5, loggedIn, isVip).map(songs.toView);
|
||||
social.enrichSongs(recentlyLiked, userId);
|
||||
|
||||
res.render('public/songs', {
|
||||
|
|
@ -74,13 +82,14 @@ router.get('/songs', (req, res) => {
|
|||
router.get('/songs/:slug', (req, res, next) => {
|
||||
const song = songs.findBySlug(req.params.slug);
|
||||
if (!song) return next();
|
||||
if (!canView(song.visibility, song.uploadedBy, req.session.user?.id)) {
|
||||
const userId = req.session.user?.id || null;
|
||||
const isVip = !!(req.session.user?.isVip || req.session.user?.role === 'admin');
|
||||
if (!canView(song.visibility, song.uploadedBy, userId, isVip)) {
|
||||
return denyAccess(req, res, next, song.visibility);
|
||||
}
|
||||
const userId = req.session.user?.id || null;
|
||||
social.enrichSongs([song], userId);
|
||||
const creator = song.uploadedBy ? users.findById(song.uploadedBy) : null;
|
||||
const songPlaylists = playlists.getPlaylistsForSong(song.id, !!req.session.user);
|
||||
const songPlaylists = playlists.getPlaylistsForSong(song.id, !!req.session.user, isVip);
|
||||
const og = {
|
||||
title: `${res.locals.siteName}: ${song.title} by ${song.artist}`,
|
||||
description: `Check out ${song.artist}'s hit ${song.title}, available on ${res.locals.siteName}`,
|
||||
|
|
@ -93,7 +102,8 @@ router.get('/songs/:slug', (req, res, next) => {
|
|||
router.get('/playlists', (req, res) => {
|
||||
const loggedIn = !!req.session.user;
|
||||
const userId = req.session.user?.id || null;
|
||||
const list = social.enrichPlaylists(playlists.listVisible(loggedIn), userId);
|
||||
const isVip = !!(req.session.user?.isVip || req.session.user?.role === 'admin');
|
||||
const list = social.enrichPlaylists(playlists.listVisible(loggedIn, isVip), userId);
|
||||
res.render('public/playlists', { title: 'Playlists', playlists: list });
|
||||
});
|
||||
|
||||
|
|
@ -101,13 +111,14 @@ router.get('/playlists/:slug', (req, res, next) => {
|
|||
const playlist = playlists.findBySlug(req.params.slug);
|
||||
if (!playlist) return next();
|
||||
const userId = req.session.user?.id || null;
|
||||
const isVip = !!(req.session.user?.isVip || req.session.user?.role === 'admin');
|
||||
// Use effective visibility (most restrictive of playlist + all its tracks).
|
||||
const effectiveVisibility = playlists.getEffectiveVisibility(playlist.id);
|
||||
if (!canView(effectiveVisibility, playlist.createdBy, userId)) {
|
||||
if (!canView(effectiveVisibility, playlist.createdBy, userId, isVip)) {
|
||||
return denyAccess(req, res, next, effectiveVisibility);
|
||||
}
|
||||
social.enrichPlaylists([playlist], userId);
|
||||
const tracks = playlists.getTracks(playlist.id).filter(t => canView(t.visibility, t.uploadedBy, userId));
|
||||
const tracks = playlists.getTracks(playlist.id).filter(t => canView(t.visibility, t.uploadedBy, userId, isVip));
|
||||
social.enrichSongs(tracks, userId);
|
||||
const creator = playlist.createdBy ? users.findById(playlist.createdBy) : null;
|
||||
const og = {
|
||||
|
|
@ -129,18 +140,19 @@ router.get('/profiles/:slug', (req, res, next) => {
|
|||
if (!profile) return next();
|
||||
const loggedIn = !!req.session.user;
|
||||
const userId = req.session.user?.id || null;
|
||||
const isVip = !!(req.session.user?.isVip || req.session.user?.role === 'admin');
|
||||
|
||||
const userPlaylists = social.enrichPlaylists(playlists.listPublicByUser(profile.id, loggedIn), userId);
|
||||
const userSongs = social.enrichSongs(songs.listPublicByUser(profile.id, loggedIn), userId);
|
||||
const userPlaylists = social.enrichPlaylists(playlists.listPublicByUser(profile.id, loggedIn, isVip), userId);
|
||||
const userSongs = social.enrichSongs(songs.listPublicByUser(profile.id, loggedIn, isVip), userId);
|
||||
|
||||
const likedSongRows = social.getLikedSongs(profile.id, loggedIn);
|
||||
const likedSongRows = social.getLikedSongs(profile.id, loggedIn, isVip);
|
||||
// Map raw DB rows to song view objects
|
||||
const likedSongs = social.enrichSongs(
|
||||
likedSongRows.map(row => songs.findById(row.id)).filter(Boolean),
|
||||
userId
|
||||
);
|
||||
|
||||
const likedPlaylistRows = social.getLikedPlaylists(profile.id, loggedIn);
|
||||
const likedPlaylistRows = social.getLikedPlaylists(profile.id, loggedIn, isVip);
|
||||
const likedPlaylists = social.enrichPlaylists(
|
||||
likedPlaylistRows.map(row => playlists.findById(row.id)).filter(Boolean),
|
||||
userId
|
||||
|
|
@ -160,7 +172,8 @@ router.get('/profiles/:slug', (req, res, next) => {
|
|||
router.get('/stream/:id', (req, res, next) => {
|
||||
const song = songs.findById(Number(req.params.id));
|
||||
if (!song) return next();
|
||||
if (!canView(song.visibility, song.uploadedBy, req.session.user?.id)) {
|
||||
const isVip = !!(req.session.user?.isVip || req.session.user?.role === 'admin');
|
||||
if (!canView(song.visibility, song.uploadedBy, req.session.user?.id, isVip)) {
|
||||
return denyAccess(req, res, next, song.visibility);
|
||||
}
|
||||
const absPath = path.join(mediaDir, song.audioPath);
|
||||
|
|
|
|||
|
|
@ -14,21 +14,32 @@ 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.
|
||||
// Guests only see playlists where nothing in them is logged_in, vip, 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')
|
||||
WHERE ps.playlist_id = p.id AND s.visibility IN ('logged_in', 'vip', 'private')
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
// Logged-in users see public/logged_in playlists where nothing in them is private.
|
||||
// Logged-in non-VIP users see public/logged_in playlists where nothing in them is vip or 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 IN ('vip', 'private')
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
// VIP users: public + logged_in + vip playlists (no private tracks)
|
||||
const listVipStmt = db.prepare(`
|
||||
SELECT p.* FROM playlists p
|
||||
WHERE p.visibility IN ('public', 'logged_in', 'vip')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM playlist_songs ps
|
||||
JOIN songs s ON s.id = ps.song_id
|
||||
|
|
@ -36,6 +47,33 @@ const listUserStmt = db.prepare(`
|
|||
)
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
const featuredVipStmt = db.prepare(`
|
||||
SELECT p.* FROM playlists p
|
||||
WHERE p.visibility IN ('public', 'logged_in', 'vip')
|
||||
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 ?
|
||||
`);
|
||||
const listPublicByUserVipStmt = db.prepare(`
|
||||
SELECT p.* FROM playlists p
|
||||
WHERE p.created_by = ? AND p.visibility IN ('public', 'logged_in', 'vip')
|
||||
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 forSongVipStmt = db.prepare(`
|
||||
SELECT p.* FROM playlists p
|
||||
JOIN playlist_songs ps ON ps.playlist_id = p.id
|
||||
WHERE ps.song_id = ? AND p.visibility IN ('public', 'logged_in', 'vip')
|
||||
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(`
|
||||
|
|
@ -44,7 +82,7 @@ const featuredGuestStmt = db.prepare(`
|
|||
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')
|
||||
WHERE ps.playlist_id = p.id AND s.visibility IN ('logged_in', 'vip', 'private')
|
||||
)
|
||||
ORDER BY RANDOM() LIMIT ?
|
||||
`);
|
||||
|
|
@ -54,7 +92,7 @@ const featuredUserStmt = db.prepare(`
|
|||
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'
|
||||
WHERE ps.playlist_id = p.id AND s.visibility IN ('vip', 'private')
|
||||
)
|
||||
ORDER BY RANDOM() LIMIT ?
|
||||
`);
|
||||
|
|
@ -66,6 +104,9 @@ const effectiveVisibilityStmt = db.prepare(`
|
|||
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 = 'vip'
|
||||
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 = 'vip')
|
||||
THEN 'vip'
|
||||
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'
|
||||
|
|
@ -198,8 +239,8 @@ function getEffectiveVisibility(id) {
|
|||
return row ? row.effective_visibility : null;
|
||||
}
|
||||
|
||||
function listVisible(loggedIn = false) {
|
||||
return (loggedIn ? listUserStmt : listGuestStmt).all().map(toView);
|
||||
function listVisible(loggedIn = false, isVip = false) {
|
||||
return (isVip ? listVipStmt : loggedIn ? listUserStmt : listGuestStmt).all().map(toView);
|
||||
}
|
||||
|
||||
function listAll() {
|
||||
|
|
@ -210,8 +251,8 @@ function listByUser(userId) {
|
|||
return listByUserStmt.all(userId).map(toView);
|
||||
}
|
||||
|
||||
function featured(n = 6, loggedIn = false) {
|
||||
return (loggedIn ? featuredUserStmt : featuredGuestStmt).all(n).map(toView);
|
||||
function featured(n = 6, loggedIn = false, isVip = false) {
|
||||
return (isVip ? featuredVipStmt : loggedIn ? featuredUserStmt : featuredGuestStmt).all(n).map(toView);
|
||||
}
|
||||
|
||||
function getTracks(playlistId) {
|
||||
|
|
@ -245,7 +286,7 @@ const listPublicByUserGuestStmt = db.prepare(`
|
|||
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')
|
||||
WHERE ps.playlist_id = p.id AND s.visibility IN ('logged_in', 'vip', 'private')
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
|
|
@ -255,13 +296,13 @@ const listPublicByUserLoggedStmt = db.prepare(`
|
|||
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'
|
||||
WHERE ps.playlist_id = p.id AND s.visibility IN ('vip', 'private')
|
||||
)
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
|
||||
function listPublicByUser(userId, loggedIn = false) {
|
||||
return (loggedIn ? listPublicByUserLoggedStmt : listPublicByUserGuestStmt).all(userId).map(toView);
|
||||
function listPublicByUser(userId, loggedIn = false, isVip = false) {
|
||||
return (isVip ? listPublicByUserVipStmt : loggedIn ? listPublicByUserLoggedStmt : listPublicByUserGuestStmt).all(userId).map(toView);
|
||||
}
|
||||
|
||||
// Playlists that contain a given song and are visible to the viewer.
|
||||
|
|
@ -278,8 +319,8 @@ const forSongUserStmt = db.prepare(`
|
|||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
|
||||
function getPlaylistsForSong(songId, loggedIn = false) {
|
||||
return (loggedIn ? forSongUserStmt : forSongGuestStmt).all(songId).map(toView);
|
||||
function getPlaylistsForSong(songId, loggedIn = false, isVip = false) {
|
||||
return (isVip ? forSongVipStmt : loggedIn ? forSongUserStmt : forSongGuestStmt).all(songId).map(toView);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ const likedSongsUserStmt = db.prepare(`
|
|||
WHERE sl.user_id = ? AND s.visibility IN ('public', 'logged_in')
|
||||
ORDER BY sl.created_at DESC
|
||||
`);
|
||||
const likedSongsVipStmt = db.prepare(`
|
||||
SELECT s.* FROM song_likes sl
|
||||
JOIN songs s ON s.id = sl.song_id
|
||||
WHERE sl.user_id = ? AND s.visibility IN ('public', 'logged_in', 'vip')
|
||||
ORDER BY sl.created_at DESC
|
||||
`);
|
||||
const likedPlaylistsGuestStmt = db.prepare(`
|
||||
SELECT p.* FROM playlist_likes pl
|
||||
JOIN playlists p ON p.id = pl.playlist_id
|
||||
|
|
@ -121,7 +127,7 @@ const likedPlaylistsGuestStmt = db.prepare(`
|
|||
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')
|
||||
WHERE ps.playlist_id = p.id AND s.visibility IN ('logged_in', 'vip', 'private')
|
||||
)
|
||||
ORDER BY pl.created_at DESC
|
||||
`);
|
||||
|
|
@ -129,6 +135,17 @@ const likedPlaylistsUserStmt = db.prepare(`
|
|||
SELECT p.* FROM playlist_likes pl
|
||||
JOIN playlists p ON p.id = pl.playlist_id
|
||||
WHERE pl.user_id = ? AND 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 IN ('vip', 'private')
|
||||
)
|
||||
ORDER BY pl.created_at DESC
|
||||
`);
|
||||
const likedPlaylistsVipStmt = db.prepare(`
|
||||
SELECT p.* FROM playlist_likes pl
|
||||
JOIN playlists p ON p.id = pl.playlist_id
|
||||
WHERE pl.user_id = ? AND p.visibility IN ('public', 'logged_in', 'vip')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM playlist_songs ps
|
||||
JOIN songs s ON s.id = ps.song_id
|
||||
|
|
@ -137,12 +154,12 @@ const likedPlaylistsUserStmt = db.prepare(`
|
|||
ORDER BY pl.created_at DESC
|
||||
`);
|
||||
|
||||
function getLikedSongs(userId, loggedIn = false) {
|
||||
return (loggedIn ? likedSongsUserStmt : likedSongsGuestStmt).all(userId);
|
||||
function getLikedSongs(userId, loggedIn = false, isVip = false) {
|
||||
return (isVip ? likedSongsVipStmt : loggedIn ? likedSongsUserStmt : likedSongsGuestStmt).all(userId);
|
||||
}
|
||||
|
||||
function getLikedPlaylists(userId, loggedIn = false) {
|
||||
return (loggedIn ? likedPlaylistsUserStmt : likedPlaylistsGuestStmt).all(userId);
|
||||
function getLikedPlaylists(userId, loggedIn = false, isVip = false) {
|
||||
return (isVip ? likedPlaylistsVipStmt : loggedIn ? likedPlaylistsUserStmt : likedPlaylistsGuestStmt).all(userId);
|
||||
}
|
||||
|
||||
// Site-wide recently liked songs (distinct songs, ordered by most recent like from any user).
|
||||
|
|
@ -160,9 +177,16 @@ const recentlyLikedUserStmt = db.prepare(`
|
|||
ORDER BY (SELECT MAX(sl2.created_at) FROM song_likes sl2 WHERE sl2.song_id = s.id) DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
const recentlyLikedVipStmt = db.prepare(`
|
||||
SELECT s.* FROM songs s
|
||||
WHERE s.visibility IN ('public', 'logged_in', 'vip')
|
||||
AND EXISTS (SELECT 1 FROM song_likes sl WHERE sl.song_id = s.id)
|
||||
ORDER BY (SELECT MAX(sl2.created_at) FROM song_likes sl2 WHERE sl2.song_id = s.id) DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
function getRecentlyLikedSongs(n = 5, loggedIn = false) {
|
||||
return (loggedIn ? recentlyLikedUserStmt : recentlyLikedGuestStmt).all(n);
|
||||
function getRecentlyLikedSongs(n = 5, loggedIn = false, isVip = false) {
|
||||
return (isVip ? recentlyLikedVipStmt : loggedIn ? recentlyLikedUserStmt : recentlyLikedGuestStmt).all(n);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
|||
|
|
@ -24,12 +24,16 @@ const countUserStmt = db.prepare(`SELECT COUNT(*) AS c FROM songs WHERE visibili
|
|||
const searchUserStmt = db.prepare(`SELECT * FROM songs WHERE visibility IN ('public', 'logged_in') AND (title LIKE @q OR artist LIKE @q OR album LIKE @q) ORDER BY created_at DESC LIMIT @limit OFFSET @offset`);
|
||||
const countSearchUserStmt = db.prepare(`SELECT COUNT(*) AS c FROM songs WHERE visibility IN ('public', 'logged_in') AND (title LIKE @q OR artist LIKE @q OR album LIKE @q)`);
|
||||
|
||||
// VIP user: public + logged_in + vip
|
||||
const recentVipStmt = db.prepare(`SELECT * FROM songs WHERE visibility IN ('public', 'logged_in', 'vip') ORDER BY created_at DESC LIMIT ?`);
|
||||
const listPublicByUserVipStmt = db.prepare(`SELECT * FROM songs WHERE uploaded_by = ? AND visibility IN ('public', 'logged_in', 'vip') ORDER BY created_at DESC`);
|
||||
|
||||
const listAllStmt = db.prepare('SELECT * FROM songs ORDER BY created_at DESC');
|
||||
const listByUserStmt = db.prepare('SELECT * FROM songs WHERE uploaded_by = ? ORDER BY created_at DESC');
|
||||
// For the "add song to playlist" dropdown: public + logged_in + the user's own private songs.
|
||||
const listForManagementStmt = db.prepare(`
|
||||
SELECT * FROM songs
|
||||
WHERE visibility IN ('public', 'logged_in') OR uploaded_by = ?
|
||||
WHERE visibility IN ('public', 'logged_in', 'vip') OR uploaded_by = ?
|
||||
ORDER BY title ASC
|
||||
`);
|
||||
const listGenresStmt = db.prepare(`SELECT DISTINCT genre FROM songs WHERE genre IS NOT NULL AND genre != '' ORDER BY genre ASC`);
|
||||
|
|
@ -146,11 +150,14 @@ function findById(id) {
|
|||
}
|
||||
|
||||
// loggedIn=true means the viewer is authenticated (sees public + logged_in).
|
||||
// isVip=true adds vip visibility to the visible set.
|
||||
// Supports optional genre and uploadedBy filters for faceted browsing.
|
||||
function list({ q, genre, uploadedBy, limit = 25, offset = 0, loggedIn = false }) {
|
||||
const vis = loggedIn
|
||||
? `visibility IN ('public', 'logged_in')`
|
||||
: `visibility = 'public'`;
|
||||
function list({ q, genre, uploadedBy, limit = 25, offset = 0, loggedIn = false, isVip = false }) {
|
||||
const vis = isVip
|
||||
? `visibility IN ('public', 'logged_in', 'vip')`
|
||||
: loggedIn
|
||||
? `visibility IN ('public', 'logged_in')`
|
||||
: `visibility = 'public'`;
|
||||
const conds = [vis];
|
||||
const params = { limit, offset };
|
||||
|
||||
|
|
@ -185,23 +192,25 @@ function listForManagement(userId) {
|
|||
return listForManagementStmt.all(userId).map(toView);
|
||||
}
|
||||
|
||||
function recent(n = 8, loggedIn = false) {
|
||||
return (loggedIn ? recentUserStmt : recentGuestStmt).all(n).map(toView);
|
||||
function recent(n = 8, loggedIn = false, isVip = false) {
|
||||
return (isVip ? recentVipStmt : loggedIn ? recentUserStmt : recentGuestStmt).all(n).map(toView);
|
||||
}
|
||||
|
||||
const listPublicByUserGuestStmt = db.prepare(`SELECT * FROM songs WHERE uploaded_by = ? AND visibility = 'public' ORDER BY created_at DESC`);
|
||||
const listPublicByUserLoggedStmt = db.prepare(`SELECT * FROM songs WHERE uploaded_by = ? AND visibility IN ('public', 'logged_in') ORDER BY created_at DESC`);
|
||||
|
||||
function listPublicByUser(userId, loggedIn = false) {
|
||||
return (loggedIn ? listPublicByUserLoggedStmt : listPublicByUserGuestStmt).all(userId).map(toView);
|
||||
function listPublicByUser(userId, loggedIn = false, isVip = false) {
|
||||
return (isVip ? listPublicByUserVipStmt : loggedIn ? listPublicByUserLoggedStmt : listPublicByUserGuestStmt).all(userId).map(toView);
|
||||
}
|
||||
|
||||
function listGenres() {
|
||||
return listGenresStmt.all().map(r => r.genre);
|
||||
}
|
||||
|
||||
function listUploaders(loggedIn = false) {
|
||||
const vis = loggedIn ? `IN ('public', 'logged_in')` : `= 'public'`;
|
||||
function listUploaders(loggedIn = false, isVip = false) {
|
||||
const vis = isVip
|
||||
? `IN ('public', 'logged_in', 'vip')`
|
||||
: loggedIn ? `IN ('public', 'logged_in')` : `= 'public'`;
|
||||
return db.prepare(`
|
||||
SELECT DISTINCT u.id, u.display_name
|
||||
FROM songs s JOIN users u ON u.id = s.uploaded_by
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@
|
|||
<input type="radio" name="visibility" value="logged_in" <%= (!values.visibility || values.visibility === 'logged_in') ? 'checked' : '' %>>
|
||||
Logged in only — members only
|
||||
</label>
|
||||
<% if (user && (user.isVip || user.role === 'admin')) { %>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="visibility" value="vip" <%= (values.visibility === 'vip') ? 'checked' : '' %>>
|
||||
VIP only — visible to VIP members
|
||||
</label>
|
||||
<% } %>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="visibility" value="private" <%= (values.visibility === 'private') ? 'checked' : '' %>>
|
||||
Private — only you
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
<td>
|
||||
<% if (p.visibility === 'logged_in') { %>
|
||||
<span class="badge badge-logged-in">Members</span>
|
||||
<% } else if (p.visibility === 'vip') { %>
|
||||
<span class="badge badge-vip">VIP</span>
|
||||
<% } else if (p.visibility === 'private') { %>
|
||||
<span class="badge badge-private">Private</span>
|
||||
<% } else { %>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,12 @@
|
|||
<input type="radio" name="visibility" value="logged_in" <%= (!values.visibility || values.visibility === 'logged_in') ? 'checked' : '' %>>
|
||||
Logged in only — members only
|
||||
</label>
|
||||
<% if (user && (user.isVip || user.role === 'admin')) { %>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="visibility" value="vip" <%= (values.visibility === 'vip') ? 'checked' : '' %>>
|
||||
VIP only — visible to VIP members
|
||||
</label>
|
||||
<% } %>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="visibility" value="private" <%= (values.visibility === 'private') ? 'checked' : '' %>>
|
||||
Private — only you
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@
|
|||
<td>
|
||||
<% if (s.visibility === 'logged_in') { %>
|
||||
<span class="badge badge-logged-in">Members</span>
|
||||
<% } else if (s.visibility === 'vip') { %>
|
||||
<span class="badge badge-vip">VIP</span>
|
||||
<% } else if (s.visibility === 'private') { %>
|
||||
<span class="badge badge-private">Private</span>
|
||||
<% } else { %>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<div class="song-main">
|
||||
<a class="song-title" href="/songs/<%= s.slug %>"><%= s.title %></a>
|
||||
<% if (s.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members</span><% } %>
|
||||
<% if (s.visibility === 'vip') { %><span class="badge badge-vip">VIP</span><% } %>
|
||||
<% if (s.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
||||
<div class="song-subtitle">
|
||||
<span class="muted small"><%= s.artist %><% if (s.album) { %> · <%= s.album %><% } %></span>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
<div class="card-body">
|
||||
<div class="card-title"><%= p.title %>
|
||||
<% if (p.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members</span><% } %>
|
||||
<% if (p.visibility === 'vip') { %><span class="badge badge-vip">VIP</span><% } %>
|
||||
<% if (p.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
||||
</div>
|
||||
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<div class="card-body">
|
||||
<div class="card-title"><%= p.title %>
|
||||
<% if (p.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members</span><% } %>
|
||||
<% if (p.visibility === 'vip') { %><span class="badge badge-vip">VIP</span><% } %>
|
||||
<% if (p.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
||||
</div>
|
||||
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
<div class="card-body">
|
||||
<div class="card-title"><%= p.title %>
|
||||
<% if (p.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members</span><% } %>
|
||||
<% if (p.visibility === 'vip') { %><span class="badge badge-vip">VIP</span><% } %>
|
||||
</div>
|
||||
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
||||
<div class="card-footer">
|
||||
|
|
@ -134,6 +135,7 @@
|
|||
<div class="card-body">
|
||||
<div class="card-title"><%= p.title %>
|
||||
<% if (p.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members</span><% } %>
|
||||
<% if (p.visibility === 'vip') { %><span class="badge badge-vip">VIP</span><% } %>
|
||||
</div>
|
||||
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
||||
<div class="card-footer">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
<div class="song-detail-meta">
|
||||
<h1><%= song.title %>
|
||||
<% if (song.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members only</span><% } %>
|
||||
<% if (song.visibility === 'vip') { %><span class="badge badge-vip">VIP</span><% } %>
|
||||
<% if (song.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
||||
</h1>
|
||||
<p class="muted"><%= song.artist %><% if (song.album) { %> · <%= song.album %><% } %></p>
|
||||
|
|
@ -185,6 +186,7 @@
|
|||
<% } %>
|
||||
<span class="appears-in-title"><%= pl.title %></span>
|
||||
<% if (pl.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members only</span><% } %>
|
||||
<% if (pl.visibility === 'vip') { %><span class="badge badge-vip">VIP</span><% } %>
|
||||
</a>
|
||||
</li>
|
||||
<% }) %>
|
||||
|
|
|
|||
Loading…
Reference in New Issue