241 lines
11 KiB
JavaScript
241 lines
11 KiB
JavaScript
const { db } = require('../../db');
|
||
const notifications = require('./notifications');
|
||
|
||
// ── Lookup statements for notification context ───────────────────────────────
|
||
const songOwnerStmt = db.prepare('SELECT uploaded_by, title, slug FROM songs WHERE id = ?');
|
||
const playlistOwnerStmt = db.prepare('SELECT created_by, title, slug FROM playlists WHERE id = ?');
|
||
const userNameStmt = db.prepare('SELECT display_name FROM users WHERE id = ?');
|
||
|
||
function getActorName(actorId) {
|
||
return actorId ? (userNameStmt.get(actorId)?.display_name || null) : null;
|
||
}
|
||
|
||
// ── Song statements ──────────────────────────────────────────────────────────
|
||
const songLikeCountStmt = db.prepare('SELECT COUNT(*) AS c FROM song_likes WHERE song_id = ?');
|
||
const songFavCountStmt = db.prepare('SELECT COUNT(*) AS c FROM song_favorites WHERE song_id = ?');
|
||
const songUserLikedStmt = db.prepare('SELECT 1 FROM song_likes WHERE user_id = ? AND song_id = ?');
|
||
const songUserFavoritedStmt = db.prepare('SELECT 1 FROM song_favorites WHERE user_id = ? AND song_id = ?');
|
||
const insertSongLike = db.prepare('INSERT OR IGNORE INTO song_likes (user_id, song_id) VALUES (?, ?)');
|
||
const deleteSongLike = db.prepare('DELETE FROM song_likes WHERE user_id = ? AND song_id = ?');
|
||
const insertSongFav = db.prepare('INSERT OR IGNORE INTO song_favorites (user_id, song_id) VALUES (?, ?)');
|
||
const deleteSongFav = db.prepare('DELETE FROM song_favorites WHERE user_id = ? AND song_id = ?');
|
||
|
||
// ── Playlist statements ──────────────────────────────────────────────────────
|
||
const plLikeCountStmt = db.prepare('SELECT COUNT(*) AS c FROM playlist_likes WHERE playlist_id = ?');
|
||
const plFavCountStmt = db.prepare('SELECT COUNT(*) AS c FROM playlist_favorites WHERE playlist_id = ?');
|
||
const plUserLikedStmt = db.prepare('SELECT 1 FROM playlist_likes WHERE user_id = ? AND playlist_id = ?');
|
||
const plUserFavoritedStmt = db.prepare('SELECT 1 FROM playlist_favorites WHERE user_id = ? AND playlist_id = ?');
|
||
const insertPlLike = db.prepare('INSERT OR IGNORE INTO playlist_likes (user_id, playlist_id) VALUES (?, ?)');
|
||
const deletePlLike = db.prepare('DELETE FROM playlist_likes WHERE user_id = ? AND playlist_id = ?');
|
||
const insertPlFav = db.prepare('INSERT OR IGNORE INTO playlist_favorites (user_id, playlist_id) VALUES (?, ?)');
|
||
const deletePlFav = db.prepare('DELETE FROM playlist_favorites WHERE user_id = ? AND playlist_id = ?');
|
||
|
||
// ── Toggle functions ─────────────────────────────────────────────────────────
|
||
function toggleSongLike(userId, songId) {
|
||
const exists = songUserLikedStmt.get(userId, songId);
|
||
if (exists) {
|
||
deleteSongLike.run(userId, songId);
|
||
} else {
|
||
insertSongLike.run(userId, songId);
|
||
const song = songOwnerStmt.get(songId);
|
||
if (song) notifications.create({ userId: song.uploaded_by, actorId: userId, actorName: getActorName(userId), action: 'like', entityType: 'song', entityId: songId, entityTitle: song.title, entitySlug: song.slug });
|
||
}
|
||
return { liked: !exists, count: songLikeCountStmt.get(songId).c };
|
||
}
|
||
|
||
function toggleSongFavorite(userId, songId) {
|
||
const exists = songUserFavoritedStmt.get(userId, songId);
|
||
if (exists) {
|
||
deleteSongFav.run(userId, songId);
|
||
} else {
|
||
insertSongFav.run(userId, songId);
|
||
const song = songOwnerStmt.get(songId);
|
||
if (song) notifications.create({ userId: song.uploaded_by, actorId: userId, actorName: getActorName(userId), action: 'favorite', entityType: 'song', entityId: songId, entityTitle: song.title, entitySlug: song.slug });
|
||
}
|
||
return { favorited: !exists, count: songFavCountStmt.get(songId).c };
|
||
}
|
||
|
||
function togglePlaylistLike(userId, playlistId) {
|
||
const exists = plUserLikedStmt.get(userId, playlistId);
|
||
if (exists) {
|
||
deletePlLike.run(userId, playlistId);
|
||
} else {
|
||
insertPlLike.run(userId, playlistId);
|
||
const pl = playlistOwnerStmt.get(playlistId);
|
||
if (pl) notifications.create({ userId: pl.created_by, actorId: userId, actorName: getActorName(userId), action: 'like', entityType: 'playlist', entityId: playlistId, entityTitle: pl.title, entitySlug: pl.slug });
|
||
}
|
||
return { liked: !exists, count: plLikeCountStmt.get(playlistId).c };
|
||
}
|
||
|
||
function togglePlaylistFavorite(userId, playlistId) {
|
||
const exists = plUserFavoritedStmt.get(userId, playlistId);
|
||
if (exists) {
|
||
deletePlFav.run(userId, playlistId);
|
||
} else {
|
||
insertPlFav.run(userId, playlistId);
|
||
const pl = playlistOwnerStmt.get(playlistId);
|
||
if (pl) notifications.create({ userId: pl.created_by, actorId: userId, actorName: getActorName(userId), action: 'favorite', entityType: 'playlist', entityId: playlistId, entityTitle: pl.title, entitySlug: pl.slug });
|
||
}
|
||
return { favorited: !exists, count: plFavCountStmt.get(playlistId).c };
|
||
}
|
||
|
||
// ── Enrichment helpers ───────────────────────────────────────────────────────
|
||
// Mutates each item in-place, adding likeCount/favoriteCount/userLiked/userFavorited.
|
||
function enrichSongs(songs, userId = null) {
|
||
for (const s of songs) {
|
||
s.likeCount = songLikeCountStmt.get(s.id).c;
|
||
s.favoriteCount = songFavCountStmt.get(s.id).c;
|
||
s.userLiked = userId ? !!songUserLikedStmt.get(userId, s.id) : false;
|
||
s.userFavorited = userId ? !!songUserFavoritedStmt.get(userId, s.id) : false;
|
||
}
|
||
return songs;
|
||
}
|
||
|
||
function enrichPlaylists(playlists, userId = null) {
|
||
for (const p of playlists) {
|
||
p.likeCount = plLikeCountStmt.get(p.id).c;
|
||
p.favoriteCount = plFavCountStmt.get(p.id).c;
|
||
p.userLiked = userId ? !!plUserLikedStmt.get(userId, p.id) : false;
|
||
p.userFavorited = userId ? !!plUserFavoritedStmt.get(userId, p.id) : false;
|
||
}
|
||
return playlists;
|
||
}
|
||
|
||
// ── Profile liked/favorited queries ─────────────────────────────────────────
|
||
const likedSongsGuestStmt = db.prepare(`
|
||
SELECT s.* FROM song_likes sl
|
||
JOIN songs s ON s.id = sl.song_id
|
||
WHERE sl.user_id = ? AND s.visibility = 'public'
|
||
ORDER BY sl.created_at DESC
|
||
`);
|
||
const likedSongsUserStmt = 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')
|
||
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
|
||
WHERE pl.user_id = ? AND 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', 'vip', 'private')
|
||
)
|
||
ORDER BY pl.created_at DESC
|
||
`);
|
||
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
|
||
WHERE ps.playlist_id = p.id AND s.visibility = 'private'
|
||
)
|
||
ORDER BY pl.created_at DESC
|
||
`);
|
||
|
||
function getLikedSongs(userId, loggedIn = false, isVip = false) {
|
||
return (isVip ? likedSongsVipStmt : loggedIn ? likedSongsUserStmt : likedSongsGuestStmt).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).
|
||
const recentlyLikedGuestStmt = db.prepare(`
|
||
SELECT s.* FROM songs s
|
||
WHERE s.visibility = 'public'
|
||
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 ?
|
||
`);
|
||
const recentlyLikedUserStmt = db.prepare(`
|
||
SELECT s.* FROM songs s
|
||
WHERE s.visibility IN ('public', 'logged_in')
|
||
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 ?
|
||
`);
|
||
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, isVip = false) {
|
||
return (isVip ? recentlyLikedVipStmt : loggedIn ? recentlyLikedUserStmt : recentlyLikedGuestStmt).all(n);
|
||
}
|
||
|
||
// ── Most popular songs (scored by likes + 2×favorites) ───────────────────────
|
||
const mostPopularGuestStmt = db.prepare(`
|
||
SELECT s.*,
|
||
(COUNT(DISTINCT sl.user_id) + 2 * COUNT(DISTINCT sf.user_id)) AS score
|
||
FROM songs s
|
||
LEFT JOIN song_likes sl ON sl.song_id = s.id
|
||
LEFT JOIN song_favorites sf ON sf.song_id = s.id
|
||
WHERE s.visibility = 'public'
|
||
GROUP BY s.id
|
||
HAVING score > 0
|
||
ORDER BY score DESC, s.created_at DESC
|
||
LIMIT ?
|
||
`);
|
||
const mostPopularUserStmt = db.prepare(`
|
||
SELECT s.*,
|
||
(COUNT(DISTINCT sl.user_id) + 2 * COUNT(DISTINCT sf.user_id)) AS score
|
||
FROM songs s
|
||
LEFT JOIN song_likes sl ON sl.song_id = s.id
|
||
LEFT JOIN song_favorites sf ON sf.song_id = s.id
|
||
WHERE s.visibility IN ('public', 'logged_in')
|
||
GROUP BY s.id
|
||
HAVING score > 0
|
||
ORDER BY score DESC, s.created_at DESC
|
||
LIMIT ?
|
||
`);
|
||
const mostPopularVipStmt = db.prepare(`
|
||
SELECT s.*,
|
||
(COUNT(DISTINCT sl.user_id) + 2 * COUNT(DISTINCT sf.user_id)) AS score
|
||
FROM songs s
|
||
LEFT JOIN song_likes sl ON sl.song_id = s.id
|
||
LEFT JOIN song_favorites sf ON sf.song_id = s.id
|
||
WHERE s.visibility IN ('public', 'logged_in', 'vip')
|
||
GROUP BY s.id
|
||
HAVING score > 0
|
||
ORDER BY score DESC, s.created_at DESC
|
||
LIMIT ?
|
||
`);
|
||
|
||
function getMostPopular(n = 20, loggedIn = false, isVip = false) {
|
||
return (isVip ? mostPopularVipStmt : loggedIn ? mostPopularUserStmt : mostPopularGuestStmt).all(n);
|
||
}
|
||
|
||
module.exports = {
|
||
toggleSongLike, toggleSongFavorite,
|
||
togglePlaylistLike, togglePlaylistFavorite,
|
||
enrichSongs, enrichPlaylists,
|
||
getLikedSongs, getLikedPlaylists,
|
||
getRecentlyLikedSongs, getMostPopular,
|
||
};
|