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-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-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); }
|
.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 ---------- */
|
/* ---------- Visibility radio fieldset ---------- */
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ const upload = multer({
|
||||||
const songFields = upload.fields([{ name: 'audio', maxCount: 1 }, { name: 'cover', maxCount: 1 }]);
|
const songFields = upload.fields([{ name: 'audio', maxCount: 1 }, { name: 'cover', maxCount: 1 }]);
|
||||||
const playlistFields = upload.fields([{ name: 'cover', maxCount: 1 }]);
|
const playlistFields = upload.fields([{ name: 'cover', maxCount: 1 }]);
|
||||||
|
|
||||||
const VISIBILITY_VALUES = ['public', 'logged_in', 'private'];
|
const VISIBILITY_VALUES = ['public', 'logged_in', 'vip', 'private'];
|
||||||
const VISIBILITY_RANK = { public: 0, logged_in: 1, private: 2 };
|
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.
|
// 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.
|
// 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({
|
songs.create({
|
||||||
title: merged.title,
|
title: merged.title,
|
||||||
artist: merged.artist,
|
artist: merged.artist,
|
||||||
|
|
@ -267,7 +269,7 @@ module.exports = function adminRoutes(csrfProtection) {
|
||||||
durationSeconds: tags.duration,
|
durationSeconds: tags.duration,
|
||||||
audioPath: relMedia(audioFile, 'audio'),
|
audioPath: relMedia(audioFile, 'audio'),
|
||||||
coverPath,
|
coverPath,
|
||||||
visibility: parsed.data.visibility,
|
visibility,
|
||||||
uploadedBy: req.session.user.id,
|
uploadedBy: req.session.user.id,
|
||||||
});
|
});
|
||||||
req.flash('success', 'Song uploaded. Tags were read from the file — review and edit if needed.');
|
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);
|
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() });
|
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, {
|
songs.update(id, {
|
||||||
title: parsed.data.title,
|
title: parsed.data.title,
|
||||||
artist: parsed.data.artist,
|
artist: parsed.data.artist,
|
||||||
album: parsed.data.album || null,
|
album: parsed.data.album || null,
|
||||||
genre: parsed.data.genre || null,
|
genre: parsed.data.genre || null,
|
||||||
year: parsed.data.year ?? null,
|
year: parsed.data.year ?? null,
|
||||||
visibility: parsed.data.visibility,
|
visibility,
|
||||||
});
|
});
|
||||||
const coverFile = req.files?.cover?.[0];
|
const coverFile = req.files?.cover?.[0];
|
||||||
if (coverFile) {
|
if (coverFile) {
|
||||||
|
|
@ -369,11 +373,13 @@ module.exports = function adminRoutes(csrfProtection) {
|
||||||
console.warn('[admin] failed to process playlist cover:', err.message);
|
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({
|
const created = playlists.create({
|
||||||
title: parsed.data.title,
|
title: parsed.data.title,
|
||||||
description: parsed.data.description || null,
|
description: parsed.data.description || null,
|
||||||
coverPath,
|
coverPath,
|
||||||
visibility: parsed.data.visibility,
|
visibility,
|
||||||
createdBy: req.session.user.id,
|
createdBy: req.session.user.id,
|
||||||
});
|
});
|
||||||
const preselectIds = [].concat(req.body['preselectIds[]'] || []).map(Number).filter(Boolean);
|
const preselectIds = [].concat(req.body['preselectIds[]'] || []).map(Number).filter(Boolean);
|
||||||
|
|
@ -426,10 +432,12 @@ module.exports = function adminRoutes(csrfProtection) {
|
||||||
errors, values: req.body,
|
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, {
|
playlists.update(id, {
|
||||||
title: parsed.data.title,
|
title: parsed.data.title,
|
||||||
description: parsed.data.description || null,
|
description: parsed.data.description || null,
|
||||||
visibility: parsed.data.visibility,
|
visibility,
|
||||||
});
|
});
|
||||||
const coverFile = req.files?.cover?.[0];
|
const coverFile = req.files?.cover?.[0];
|
||||||
if (coverFile) {
|
if (coverFile) {
|
||||||
|
|
|
||||||
|
|
@ -18,50 +18,58 @@ const MIME = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Can the current viewer access an item with this visibility/owner?
|
// 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 === 'public') return true;
|
||||||
if (visibility === 'logged_in') return !!userId;
|
if (visibility === 'logged_in') return !!userId;
|
||||||
|
if (visibility === 'vip') return !!userId && isVip;
|
||||||
if (visibility === 'private') return userId != null && ownerId === userId;
|
if (visibility === 'private') return userId != null && ownerId === userId;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For members-only items, redirect guests to login rather than 404.
|
// 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).
|
// Private items always 404 (don't reveal they exist).
|
||||||
function denyAccess(req, res, next, visibility) {
|
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.session.returnTo = req.originalUrl;
|
||||||
req.flash('error', 'You must be logged in to access that.');
|
req.flash('error', 'You must be logged in to access that.');
|
||||||
return res.redirect('/login');
|
return res.redirect('/login');
|
||||||
}
|
}
|
||||||
|
if (visibility === 'vip' && req.session.user) {
|
||||||
|
req.flash('error', 'VIP access required.');
|
||||||
|
return res.redirect('/songs');
|
||||||
|
}
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
const loggedIn = !!req.session.user;
|
const loggedIn = !!req.session.user;
|
||||||
const userId = req.session.user?.id || null;
|
const userId = req.session.user?.id || null;
|
||||||
|
const isVip = !!(req.session.user?.isVip || req.session.user?.role === 'admin');
|
||||||
res.render('public/home', {
|
res.render('public/home', {
|
||||||
title: res.locals.siteName,
|
title: res.locals.siteName,
|
||||||
featured: social.enrichPlaylists(playlists.featured(5, loggedIn), userId),
|
featured: social.enrichPlaylists(playlists.featured(5, loggedIn, isVip), userId),
|
||||||
recent: social.enrichSongs(songs.recent(8, loggedIn), userId),
|
recent: social.enrichSongs(songs.recent(8, loggedIn, isVip), userId),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/songs', (req, res) => {
|
router.get('/songs', (req, res) => {
|
||||||
const loggedIn = !!req.session.user;
|
const loggedIn = !!req.session.user;
|
||||||
const userId = req.session.user?.id || null;
|
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 page = Math.max(1, Number(req.query.page) || 1);
|
||||||
const pageSize = 25;
|
const pageSize = 25;
|
||||||
const q = req.query.q || '';
|
const q = req.query.q || '';
|
||||||
const genre = req.query.genre || '';
|
const genre = req.query.genre || '';
|
||||||
const uploadedBy = req.query.uploader || '';
|
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);
|
social.enrichSongs(rows, userId);
|
||||||
|
|
||||||
const genres = songs.listGenres();
|
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);
|
social.enrichSongs(recentlyLiked, userId);
|
||||||
|
|
||||||
res.render('public/songs', {
|
res.render('public/songs', {
|
||||||
|
|
@ -74,13 +82,14 @@ router.get('/songs', (req, res) => {
|
||||||
router.get('/songs/:slug', (req, res, next) => {
|
router.get('/songs/:slug', (req, res, next) => {
|
||||||
const song = songs.findBySlug(req.params.slug);
|
const song = songs.findBySlug(req.params.slug);
|
||||||
if (!song) return next();
|
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);
|
return denyAccess(req, res, next, song.visibility);
|
||||||
}
|
}
|
||||||
const userId = req.session.user?.id || null;
|
|
||||||
social.enrichSongs([song], userId);
|
social.enrichSongs([song], userId);
|
||||||
const creator = song.uploadedBy ? users.findById(song.uploadedBy) : null;
|
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 = {
|
const og = {
|
||||||
title: `${res.locals.siteName}: ${song.title} by ${song.artist}`,
|
title: `${res.locals.siteName}: ${song.title} by ${song.artist}`,
|
||||||
description: `Check out ${song.artist}'s hit ${song.title}, available on ${res.locals.siteName}`,
|
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) => {
|
router.get('/playlists', (req, res) => {
|
||||||
const loggedIn = !!req.session.user;
|
const loggedIn = !!req.session.user;
|
||||||
const userId = req.session.user?.id || null;
|
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 });
|
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);
|
const playlist = playlists.findBySlug(req.params.slug);
|
||||||
if (!playlist) return next();
|
if (!playlist) return next();
|
||||||
const userId = req.session.user?.id || null;
|
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).
|
// Use effective visibility (most restrictive of playlist + all its tracks).
|
||||||
const effectiveVisibility = playlists.getEffectiveVisibility(playlist.id);
|
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);
|
return denyAccess(req, res, next, effectiveVisibility);
|
||||||
}
|
}
|
||||||
social.enrichPlaylists([playlist], userId);
|
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);
|
social.enrichSongs(tracks, userId);
|
||||||
const creator = playlist.createdBy ? users.findById(playlist.createdBy) : null;
|
const creator = playlist.createdBy ? users.findById(playlist.createdBy) : null;
|
||||||
const og = {
|
const og = {
|
||||||
|
|
@ -129,18 +140,19 @@ router.get('/profiles/:slug', (req, res, next) => {
|
||||||
if (!profile) return next();
|
if (!profile) return next();
|
||||||
const loggedIn = !!req.session.user;
|
const loggedIn = !!req.session.user;
|
||||||
const userId = req.session.user?.id || null;
|
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 userPlaylists = social.enrichPlaylists(playlists.listPublicByUser(profile.id, loggedIn, isVip), userId);
|
||||||
const userSongs = social.enrichSongs(songs.listPublicByUser(profile.id, loggedIn), 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
|
// Map raw DB rows to song view objects
|
||||||
const likedSongs = social.enrichSongs(
|
const likedSongs = social.enrichSongs(
|
||||||
likedSongRows.map(row => songs.findById(row.id)).filter(Boolean),
|
likedSongRows.map(row => songs.findById(row.id)).filter(Boolean),
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
const likedPlaylistRows = social.getLikedPlaylists(profile.id, loggedIn);
|
const likedPlaylistRows = social.getLikedPlaylists(profile.id, loggedIn, isVip);
|
||||||
const likedPlaylists = social.enrichPlaylists(
|
const likedPlaylists = social.enrichPlaylists(
|
||||||
likedPlaylistRows.map(row => playlists.findById(row.id)).filter(Boolean),
|
likedPlaylistRows.map(row => playlists.findById(row.id)).filter(Boolean),
|
||||||
userId
|
userId
|
||||||
|
|
@ -160,7 +172,8 @@ router.get('/profiles/:slug', (req, res, next) => {
|
||||||
router.get('/stream/:id', (req, res, next) => {
|
router.get('/stream/:id', (req, res, next) => {
|
||||||
const song = songs.findById(Number(req.params.id));
|
const song = songs.findById(Number(req.params.id));
|
||||||
if (!song) return next();
|
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);
|
return denyAccess(req, res, next, song.visibility);
|
||||||
}
|
}
|
||||||
const absPath = path.join(mediaDir, song.audioPath);
|
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 = ?');
|
const findByIdStmt = db.prepare('SELECT * FROM playlists WHERE id = ?');
|
||||||
|
|
||||||
// For list queries, effective visibility = max(playlist.visibility, most restrictive track).
|
// 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(`
|
const listGuestStmt = db.prepare(`
|
||||||
SELECT p.* FROM playlists p
|
SELECT p.* FROM playlists p
|
||||||
WHERE p.visibility = 'public'
|
WHERE p.visibility = 'public'
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM playlist_songs ps
|
SELECT 1 FROM playlist_songs ps
|
||||||
JOIN songs s ON s.id = ps.song_id
|
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
|
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(`
|
const listUserStmt = db.prepare(`
|
||||||
SELECT p.* FROM playlists p
|
SELECT p.* FROM playlists p
|
||||||
WHERE p.visibility IN ('public', 'logged_in')
|
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 (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM playlist_songs ps
|
SELECT 1 FROM playlist_songs ps
|
||||||
JOIN songs s ON s.id = ps.song_id
|
JOIN songs s ON s.id = ps.song_id
|
||||||
|
|
@ -36,6 +47,33 @@ const listUserStmt = db.prepare(`
|
||||||
)
|
)
|
||||||
ORDER BY p.created_at DESC
|
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 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 listByUserStmt = db.prepare('SELECT * FROM playlists WHERE created_by = ? ORDER BY created_at DESC');
|
||||||
const featuredGuestStmt = db.prepare(`
|
const featuredGuestStmt = db.prepare(`
|
||||||
|
|
@ -44,7 +82,7 @@ const featuredGuestStmt = db.prepare(`
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM playlist_songs ps
|
SELECT 1 FROM playlist_songs ps
|
||||||
JOIN songs s ON s.id = ps.song_id
|
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 ?
|
ORDER BY RANDOM() LIMIT ?
|
||||||
`);
|
`);
|
||||||
|
|
@ -54,7 +92,7 @@ const featuredUserStmt = db.prepare(`
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM playlist_songs ps
|
SELECT 1 FROM playlist_songs ps
|
||||||
JOIN songs s ON s.id = ps.song_id
|
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 ?
|
ORDER BY RANDOM() LIMIT ?
|
||||||
`);
|
`);
|
||||||
|
|
@ -66,6 +104,9 @@ const effectiveVisibilityStmt = db.prepare(`
|
||||||
WHEN p.visibility = 'private'
|
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')
|
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'
|
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'
|
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')
|
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'
|
THEN 'logged_in'
|
||||||
|
|
@ -198,8 +239,8 @@ function getEffectiveVisibility(id) {
|
||||||
return row ? row.effective_visibility : null;
|
return row ? row.effective_visibility : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function listVisible(loggedIn = false) {
|
function listVisible(loggedIn = false, isVip = false) {
|
||||||
return (loggedIn ? listUserStmt : listGuestStmt).all().map(toView);
|
return (isVip ? listVipStmt : loggedIn ? listUserStmt : listGuestStmt).all().map(toView);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listAll() {
|
function listAll() {
|
||||||
|
|
@ -210,8 +251,8 @@ function listByUser(userId) {
|
||||||
return listByUserStmt.all(userId).map(toView);
|
return listByUserStmt.all(userId).map(toView);
|
||||||
}
|
}
|
||||||
|
|
||||||
function featured(n = 6, loggedIn = false) {
|
function featured(n = 6, loggedIn = false, isVip = false) {
|
||||||
return (loggedIn ? featuredUserStmt : featuredGuestStmt).all(n).map(toView);
|
return (isVip ? featuredVipStmt : loggedIn ? featuredUserStmt : featuredGuestStmt).all(n).map(toView);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTracks(playlistId) {
|
function getTracks(playlistId) {
|
||||||
|
|
@ -245,7 +286,7 @@ const listPublicByUserGuestStmt = db.prepare(`
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM playlist_songs ps
|
SELECT 1 FROM playlist_songs ps
|
||||||
JOIN songs s ON s.id = ps.song_id
|
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
|
ORDER BY p.created_at DESC
|
||||||
`);
|
`);
|
||||||
|
|
@ -255,13 +296,13 @@ const listPublicByUserLoggedStmt = db.prepare(`
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM playlist_songs ps
|
SELECT 1 FROM playlist_songs ps
|
||||||
JOIN songs s ON s.id = ps.song_id
|
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
|
ORDER BY p.created_at DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
function listPublicByUser(userId, loggedIn = false) {
|
function listPublicByUser(userId, loggedIn = false, isVip = false) {
|
||||||
return (loggedIn ? listPublicByUserLoggedStmt : listPublicByUserGuestStmt).all(userId).map(toView);
|
return (isVip ? listPublicByUserVipStmt : loggedIn ? listPublicByUserLoggedStmt : listPublicByUserGuestStmt).all(userId).map(toView);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playlists that contain a given song and are visible to the viewer.
|
// 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
|
ORDER BY p.created_at DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
function getPlaylistsForSong(songId, loggedIn = false) {
|
function getPlaylistsForSong(songId, loggedIn = false, isVip = false) {
|
||||||
return (loggedIn ? forSongUserStmt : forSongGuestStmt).all(songId).map(toView);
|
return (isVip ? forSongVipStmt : loggedIn ? forSongUserStmt : forSongGuestStmt).all(songId).map(toView);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,12 @@ const likedSongsUserStmt = db.prepare(`
|
||||||
WHERE sl.user_id = ? AND s.visibility IN ('public', 'logged_in')
|
WHERE sl.user_id = ? AND s.visibility IN ('public', 'logged_in')
|
||||||
ORDER BY sl.created_at DESC
|
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(`
|
const likedPlaylistsGuestStmt = db.prepare(`
|
||||||
SELECT p.* FROM playlist_likes pl
|
SELECT p.* FROM playlist_likes pl
|
||||||
JOIN playlists p ON p.id = pl.playlist_id
|
JOIN playlists p ON p.id = pl.playlist_id
|
||||||
|
|
@ -121,7 +127,7 @@ const likedPlaylistsGuestStmt = db.prepare(`
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM playlist_songs ps
|
SELECT 1 FROM playlist_songs ps
|
||||||
JOIN songs s ON s.id = ps.song_id
|
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
|
ORDER BY pl.created_at DESC
|
||||||
`);
|
`);
|
||||||
|
|
@ -129,6 +135,17 @@ const likedPlaylistsUserStmt = db.prepare(`
|
||||||
SELECT p.* FROM playlist_likes pl
|
SELECT p.* FROM playlist_likes pl
|
||||||
JOIN playlists p ON p.id = pl.playlist_id
|
JOIN playlists p ON p.id = pl.playlist_id
|
||||||
WHERE pl.user_id = ? AND p.visibility IN ('public', 'logged_in')
|
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 (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM playlist_songs ps
|
SELECT 1 FROM playlist_songs ps
|
||||||
JOIN songs s ON s.id = ps.song_id
|
JOIN songs s ON s.id = ps.song_id
|
||||||
|
|
@ -137,12 +154,12 @@ const likedPlaylistsUserStmt = db.prepare(`
|
||||||
ORDER BY pl.created_at DESC
|
ORDER BY pl.created_at DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
function getLikedSongs(userId, loggedIn = false) {
|
function getLikedSongs(userId, loggedIn = false, isVip = false) {
|
||||||
return (loggedIn ? likedSongsUserStmt : likedSongsGuestStmt).all(userId);
|
return (isVip ? likedSongsVipStmt : loggedIn ? likedSongsUserStmt : likedSongsGuestStmt).all(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLikedPlaylists(userId, loggedIn = false) {
|
function getLikedPlaylists(userId, loggedIn = false, isVip = false) {
|
||||||
return (loggedIn ? likedPlaylistsUserStmt : likedPlaylistsGuestStmt).all(userId);
|
return (isVip ? likedPlaylistsVipStmt : loggedIn ? likedPlaylistsUserStmt : likedPlaylistsGuestStmt).all(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Site-wide recently liked songs (distinct songs, ordered by most recent like from any user).
|
// 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
|
ORDER BY (SELECT MAX(sl2.created_at) FROM song_likes sl2 WHERE sl2.song_id = s.id) DESC
|
||||||
LIMIT ?
|
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) {
|
function getRecentlyLikedSongs(n = 5, loggedIn = false, isVip = false) {
|
||||||
return (loggedIn ? recentlyLikedUserStmt : recentlyLikedGuestStmt).all(n);
|
return (isVip ? recentlyLikedVipStmt : loggedIn ? recentlyLikedUserStmt : recentlyLikedGuestStmt).all(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
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 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)`);
|
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 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');
|
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.
|
// For the "add song to playlist" dropdown: public + logged_in + the user's own private songs.
|
||||||
const listForManagementStmt = db.prepare(`
|
const listForManagementStmt = db.prepare(`
|
||||||
SELECT * FROM songs
|
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
|
ORDER BY title ASC
|
||||||
`);
|
`);
|
||||||
const listGenresStmt = db.prepare(`SELECT DISTINCT genre FROM songs WHERE genre IS NOT NULL AND genre != '' ORDER BY genre 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).
|
// 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.
|
// Supports optional genre and uploadedBy filters for faceted browsing.
|
||||||
function list({ q, genre, uploadedBy, limit = 25, offset = 0, loggedIn = false }) {
|
function list({ q, genre, uploadedBy, limit = 25, offset = 0, loggedIn = false, isVip = false }) {
|
||||||
const vis = loggedIn
|
const vis = isVip
|
||||||
? `visibility IN ('public', 'logged_in')`
|
? `visibility IN ('public', 'logged_in', 'vip')`
|
||||||
: `visibility = 'public'`;
|
: loggedIn
|
||||||
|
? `visibility IN ('public', 'logged_in')`
|
||||||
|
: `visibility = 'public'`;
|
||||||
const conds = [vis];
|
const conds = [vis];
|
||||||
const params = { limit, offset };
|
const params = { limit, offset };
|
||||||
|
|
||||||
|
|
@ -185,23 +192,25 @@ function listForManagement(userId) {
|
||||||
return listForManagementStmt.all(userId).map(toView);
|
return listForManagementStmt.all(userId).map(toView);
|
||||||
}
|
}
|
||||||
|
|
||||||
function recent(n = 8, loggedIn = false) {
|
function recent(n = 8, loggedIn = false, isVip = false) {
|
||||||
return (loggedIn ? recentUserStmt : recentGuestStmt).all(n).map(toView);
|
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 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`);
|
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) {
|
function listPublicByUser(userId, loggedIn = false, isVip = false) {
|
||||||
return (loggedIn ? listPublicByUserLoggedStmt : listPublicByUserGuestStmt).all(userId).map(toView);
|
return (isVip ? listPublicByUserVipStmt : loggedIn ? listPublicByUserLoggedStmt : listPublicByUserGuestStmt).all(userId).map(toView);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listGenres() {
|
function listGenres() {
|
||||||
return listGenresStmt.all().map(r => r.genre);
|
return listGenresStmt.all().map(r => r.genre);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listUploaders(loggedIn = false) {
|
function listUploaders(loggedIn = false, isVip = false) {
|
||||||
const vis = loggedIn ? `IN ('public', 'logged_in')` : `= 'public'`;
|
const vis = isVip
|
||||||
|
? `IN ('public', 'logged_in', 'vip')`
|
||||||
|
: loggedIn ? `IN ('public', 'logged_in')` : `= 'public'`;
|
||||||
return db.prepare(`
|
return db.prepare(`
|
||||||
SELECT DISTINCT u.id, u.display_name
|
SELECT DISTINCT u.id, u.display_name
|
||||||
FROM songs s JOIN users u ON u.id = s.uploaded_by
|
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' : '' %>>
|
<input type="radio" name="visibility" value="logged_in" <%= (!values.visibility || values.visibility === 'logged_in') ? 'checked' : '' %>>
|
||||||
Logged in only — members only
|
Logged in only — members only
|
||||||
</label>
|
</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">
|
<label class="checkbox">
|
||||||
<input type="radio" name="visibility" value="private" <%= (values.visibility === 'private') ? 'checked' : '' %>>
|
<input type="radio" name="visibility" value="private" <%= (values.visibility === 'private') ? 'checked' : '' %>>
|
||||||
Private — only you
|
Private — only you
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@
|
||||||
<td>
|
<td>
|
||||||
<% if (p.visibility === 'logged_in') { %>
|
<% if (p.visibility === 'logged_in') { %>
|
||||||
<span class="badge badge-logged-in">Members</span>
|
<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') { %>
|
<% } else if (p.visibility === 'private') { %>
|
||||||
<span class="badge badge-private">Private</span>
|
<span class="badge badge-private">Private</span>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,12 @@
|
||||||
<input type="radio" name="visibility" value="logged_in" <%= (!values.visibility || values.visibility === 'logged_in') ? 'checked' : '' %>>
|
<input type="radio" name="visibility" value="logged_in" <%= (!values.visibility || values.visibility === 'logged_in') ? 'checked' : '' %>>
|
||||||
Logged in only — members only
|
Logged in only — members only
|
||||||
</label>
|
</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">
|
<label class="checkbox">
|
||||||
<input type="radio" name="visibility" value="private" <%= (values.visibility === 'private') ? 'checked' : '' %>>
|
<input type="radio" name="visibility" value="private" <%= (values.visibility === 'private') ? 'checked' : '' %>>
|
||||||
Private — only you
|
Private — only you
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@
|
||||||
<td>
|
<td>
|
||||||
<% if (s.visibility === 'logged_in') { %>
|
<% if (s.visibility === 'logged_in') { %>
|
||||||
<span class="badge badge-logged-in">Members</span>
|
<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') { %>
|
<% } else if (s.visibility === 'private') { %>
|
||||||
<span class="badge badge-private">Private</span>
|
<span class="badge badge-private">Private</span>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<div class="song-main">
|
<div class="song-main">
|
||||||
<a class="song-title" href="/songs/<%= s.slug %>"><%= s.title %></a>
|
<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 === '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><% } %>
|
<% if (s.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
||||||
<div class="song-subtitle">
|
<div class="song-subtitle">
|
||||||
<span class="muted small"><%= s.artist %><% if (s.album) { %> · <%= s.album %><% } %></span>
|
<span class="muted small"><%= s.artist %><% if (s.album) { %> · <%= s.album %><% } %></span>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title"><%= p.title %>
|
<div class="card-title"><%= p.title %>
|
||||||
<% if (p.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members</span><% } %>
|
<% 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><% } %>
|
<% if (p.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
||||||
</div>
|
</div>
|
||||||
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title"><%= p.title %>
|
<div class="card-title"><%= p.title %>
|
||||||
<% if (p.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members</span><% } %>
|
<% 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><% } %>
|
<% if (p.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
||||||
</div>
|
</div>
|
||||||
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title"><%= p.title %>
|
<div class="card-title"><%= p.title %>
|
||||||
<% if (p.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members</span><% } %>
|
<% 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>
|
</div>
|
||||||
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
|
@ -134,6 +135,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-title"><%= p.title %>
|
<div class="card-title"><%= p.title %>
|
||||||
<% if (p.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members</span><% } %>
|
<% 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>
|
</div>
|
||||||
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
<% if (p.description) { %><div class="muted small"><%= p.description %></div><% } %>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
<div class="song-detail-meta">
|
<div class="song-detail-meta">
|
||||||
<h1><%= song.title %>
|
<h1><%= song.title %>
|
||||||
<% if (song.visibility === 'logged_in') { %><span class="badge badge-logged-in">Members only</span><% } %>
|
<% 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><% } %>
|
<% if (song.visibility === 'private') { %><span class="badge badge-private">Private</span><% } %>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="muted"><%= song.artist %><% if (song.album) { %> · <%= song.album %><% } %></p>
|
<p class="muted"><%= song.artist %><% if (song.album) { %> · <%= song.album %><% } %></p>
|
||||||
|
|
@ -185,6 +186,7 @@
|
||||||
<% } %>
|
<% } %>
|
||||||
<span class="appears-in-title"><%= pl.title %></span>
|
<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 === 'logged_in') { %><span class="badge badge-logged-in">Members only</span><% } %>
|
||||||
|
<% if (pl.visibility === 'vip') { %><span class="badge badge-vip">VIP</span><% } %>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue