v3: added content slugs, fixed real-time updates in client, added @mentions across the app, added new file selector and drop zone
This commit is contained in:
@@ -22,19 +22,23 @@ import {
|
||||
broadcastPlaylistDumpsUpdated,
|
||||
broadcastPlaylistUpdated,
|
||||
} from "./ws-service.ts";
|
||||
import { notifyPlaylistFollowersNewDump } from "./notification-service.ts";
|
||||
import {
|
||||
notifyMentions,
|
||||
notifyPlaylistFollowersNewDump,
|
||||
} from "./notification-service.ts";
|
||||
import { makeSlug, UUID_RE } from "../lib/slugify.ts";
|
||||
|
||||
const DUMP_SELECT_COLS =
|
||||
"id, kind, title, comment, user_id, created_at, url, rich_content, file_name, file_mime, file_size, vote_count, is_private";
|
||||
"id, kind, title, slug, comment, user_id, created_at, url, rich_content, file_name, file_mime, file_size, vote_count, is_private";
|
||||
|
||||
const PLAYLIST_SELECT = `p.*, u.username as owner_username,
|
||||
(SELECT COUNT(*) FROM playlist_dumps pd WHERE pd.playlist_id = p.id) as dump_count
|
||||
FROM playlists p LEFT JOIN users u ON u.id = p.user_id`;
|
||||
|
||||
function getPlaylistById(playlistId: string): Playlist {
|
||||
const row = db.prepare(
|
||||
`SELECT ${PLAYLIST_SELECT} WHERE p.id = ?;`,
|
||||
).get(playlistId);
|
||||
function getPlaylistById(idOrSlug: string): Playlist {
|
||||
const row = UUID_RE.test(idOrSlug)
|
||||
? db.prepare(`SELECT ${PLAYLIST_SELECT} WHERE p.id = ?;`).get(idOrSlug)
|
||||
: db.prepare(`SELECT ${PLAYLIST_SELECT} WHERE p.slug = ?;`).get(idOrSlug);
|
||||
if (!row || !isPlaylistRow(row)) {
|
||||
throw new APIException(APIErrorCode.NOT_FOUND, 404, "Playlist not found");
|
||||
}
|
||||
@@ -47,13 +51,15 @@ export function createPlaylist(
|
||||
): Playlist {
|
||||
const id = crypto.randomUUID();
|
||||
const createdAt = new Date();
|
||||
const slug = makeSlug(req.title, id);
|
||||
db.prepare(
|
||||
`INSERT INTO playlists (id, user_id, title, description, is_public, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?);`,
|
||||
`INSERT INTO playlists (id, user_id, title, slug, description, is_public, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);`,
|
||||
).run(
|
||||
id,
|
||||
userId,
|
||||
req.title,
|
||||
slug,
|
||||
req.description ?? null,
|
||||
req.isPublic ? 1 : 0,
|
||||
createdAt.toISOString(),
|
||||
@@ -62,10 +68,12 @@ export function createPlaylist(
|
||||
id,
|
||||
userId,
|
||||
title: req.title,
|
||||
slug,
|
||||
description: req.description,
|
||||
isPublic: req.isPublic,
|
||||
createdAt,
|
||||
};
|
||||
if (req.description) notifyMentions(userId, req.description, "playlist", id, req.title);
|
||||
broadcastPlaylistCreated(playlist);
|
||||
return playlist;
|
||||
}
|
||||
@@ -91,7 +99,7 @@ export function getPlaylist(
|
||||
WHERE pd.playlist_id = ?
|
||||
AND (d.is_private = 0 OR d.user_id = ?)
|
||||
ORDER BY pd.position ASC;`,
|
||||
).all(playlistId, requestingUserId ?? "");
|
||||
).all(playlist.id, requestingUserId ?? "");
|
||||
|
||||
const dumps: Dump[] = rows.filter(isDumpRow).map(dumpRowToApi);
|
||||
// Owners always see their own private dumps; strip them for non-owners regardless
|
||||
@@ -145,16 +153,21 @@ export function updatePlaylist(
|
||||
? req.isPublic
|
||||
: playlist.isPublic;
|
||||
|
||||
const now = new Date();
|
||||
const newSlug = makeSlug(newTitle, playlist.id);
|
||||
db.prepare(
|
||||
`UPDATE playlists SET title = ?, description = ?, is_public = ? WHERE id = ?;`,
|
||||
).run(newTitle, newDescription, newIsPublic ? 1 : 0, playlistId);
|
||||
`UPDATE playlists SET title = ?, slug = ?, description = ?, is_public = ?, updated_at = ? WHERE id = ?;`,
|
||||
).run(newTitle, newSlug, newDescription, newIsPublic ? 1 : 0, now.toISOString(), playlist.id);
|
||||
|
||||
const updated: Playlist = {
|
||||
...playlist,
|
||||
title: newTitle,
|
||||
slug: newSlug,
|
||||
description: newDescription ?? undefined,
|
||||
isPublic: newIsPublic,
|
||||
updatedAt: now,
|
||||
};
|
||||
if (newDescription) notifyMentions(requestingUserId, newDescription, "playlist", playlist.id, newTitle);
|
||||
broadcastPlaylistUpdated(updated);
|
||||
return updated;
|
||||
}
|
||||
@@ -169,8 +182,8 @@ export function deletePlaylist(
|
||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
||||
}
|
||||
|
||||
db.prepare(`DELETE FROM playlists WHERE id = ?;`).run(playlistId);
|
||||
broadcastPlaylistDeleted(playlistId, playlist.userId, playlist.isPublic);
|
||||
db.prepare(`DELETE FROM playlists WHERE id = ?;`).run(playlist.id);
|
||||
broadcastPlaylistDeleted(playlist.id, playlist.userId, playlist.isPublic);
|
||||
}
|
||||
|
||||
export function setPlaylistImage(
|
||||
@@ -184,9 +197,9 @@ export function setPlaylistImage(
|
||||
}
|
||||
db.prepare(`UPDATE playlists SET image_mime = ? WHERE id = ?;`).run(
|
||||
imageMime,
|
||||
playlistId,
|
||||
playlist.id,
|
||||
);
|
||||
const updated = getPlaylistById(playlistId);
|
||||
const updated = getPlaylistById(playlist.id);
|
||||
broadcastPlaylistUpdated(updated);
|
||||
return updated;
|
||||
}
|
||||
@@ -204,7 +217,7 @@ export function addDumpToPlaylist(
|
||||
|
||||
const minRow = db.prepare(
|
||||
`SELECT MIN(position) as min_pos FROM playlist_dumps WHERE playlist_id = ?;`,
|
||||
).get(playlistId) as { min_pos: number | null } | undefined;
|
||||
).get(playlist.id) as { min_pos: number | null } | undefined;
|
||||
|
||||
const nextPos = (minRow?.min_pos ?? 1) - 1;
|
||||
const addedAt = new Date().toISOString();
|
||||
@@ -213,7 +226,7 @@ export function addDumpToPlaylist(
|
||||
db.prepare(
|
||||
`INSERT INTO playlist_dumps (playlist_id, dump_id, position, added_at)
|
||||
VALUES (?, ?, ?, ?);`,
|
||||
).run(playlistId, dumpId, nextPos, addedAt);
|
||||
).run(playlist.id, dumpId, nextPos, addedAt);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes("UNIQUE") || msg.includes("unique")) {
|
||||
@@ -226,7 +239,7 @@ export function addDumpToPlaylist(
|
||||
throw err;
|
||||
}
|
||||
|
||||
const dumpIds = getCurrentDumpIds(playlistId);
|
||||
const dumpIds = getCurrentDumpIds(playlist.id);
|
||||
broadcastPlaylistDumpsUpdated(playlist, dumpIds);
|
||||
|
||||
if (playlist.isPublic) {
|
||||
@@ -235,7 +248,7 @@ export function addDumpToPlaylist(
|
||||
) as { title: string } | undefined;
|
||||
if (dumpRow) {
|
||||
notifyPlaylistFollowersNewDump(
|
||||
playlistId,
|
||||
playlist.id,
|
||||
playlist.title,
|
||||
dumpId,
|
||||
dumpRow.title,
|
||||
@@ -257,9 +270,9 @@ export function removeDumpFromPlaylist(
|
||||
|
||||
db.prepare(
|
||||
`DELETE FROM playlist_dumps WHERE playlist_id = ? AND dump_id = ?;`,
|
||||
).run(playlistId, dumpId);
|
||||
).run(playlist.id, dumpId);
|
||||
|
||||
const dumpIds = getCurrentDumpIds(playlistId);
|
||||
const dumpIds = getCurrentDumpIds(playlist.id);
|
||||
broadcastPlaylistDumpsUpdated(playlist, dumpIds);
|
||||
}
|
||||
|
||||
@@ -274,7 +287,7 @@ export function reorderPlaylist(
|
||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 403, "Forbidden");
|
||||
}
|
||||
|
||||
const currentIds = getCurrentDumpIds(playlistId);
|
||||
const currentIds = getCurrentDumpIds(playlist.id);
|
||||
const currentSet = new Set(currentIds);
|
||||
const newSet = new Set(dumpIds);
|
||||
|
||||
@@ -293,7 +306,7 @@ export function reorderPlaylist(
|
||||
`UPDATE playlist_dumps SET position = ? WHERE playlist_id = ? AND dump_id = ?;`,
|
||||
);
|
||||
for (let i = 0; i < dumpIds.length; i++) {
|
||||
update.run(i, playlistId, dumpIds[i]);
|
||||
update.run(i, playlist.id, dumpIds[i]);
|
||||
}
|
||||
|
||||
broadcastPlaylistDumpsUpdated(playlist, dumpIds);
|
||||
|
||||
Reference in New Issue
Block a user