v4: a playlist's dump count is of the dumps the viewer may see — it counted every dump in the playlist, so a card said "3 dumps" over a page listing two, and told everyone how many private dumps a playlist held. The lists (/playlists, a profile's playlists and followed ones, the search, a dump's playlists, the add-to-playlist panel) now count by isMemberVisibleTo's rule, in SQL as VISIBLE_DUMP_COUNT (lib/playlist-members.ts): the public dumps, and for the owner their own private ones. GET /api/playlists and /api/users/:key/follow/playlists read the optional sign-in for it. The community page and the digests count the public dumps, their figures being the same for every reader. Live, playlist_created and playlist_updated carry each socket's own count: broadcastChange builds the owner's message and the others' apart. playlist_dumps_updated loses its dumpCount, a client counting the dumps it is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-10 08:25:24 +00:00
parent ffc071af4c
commit 28f910b00e
18 changed files with 273 additions and 94 deletions

View File

@@ -146,6 +146,72 @@ test("a public playlist hides its private dumps from everyone but their owner",
);
});
test("a playlist's dumpCount is of the dumps the viewer may see", async () => {
const owner = await createUser();
const author = await createUser();
const pl = insertPlaylist({ userId: owner.id, title: "Counted banana" });
const pub = insertDump({ userId: owner.id });
const mine = insertDump({ userId: owner.id, isPrivate: true });
// Someone else's, gone private after it was added: its owner no longer
// sees it there, and neither does its poster.
const theirs = insertDump({ userId: author.id, isPrivate: true });
for (const [i, d] of [pub, mine, theirs].entries()) {
db.prepare(
`INSERT INTO playlist_dumps (playlist_id, dump_id, position, added_at)
VALUES (?, ?, ?, ?);`,
).run(pl.id, d.id, i, new Date().toISOString());
}
await put(`/api/me/follow/playlists/${pl.id}`, { auth: author.auth });
const lists = [
"/api/playlists",
`/api/users/@${owner.username}/playlists`,
`/api/users/@${author.username}/follow/playlists`,
];
for (
const [auth, expected] of [
[owner.auth, 2],
[author.auth, 1],
[undefined, 1],
] as const
) {
for (const url of lists) {
const { items } = (await get<PaginatedData<Playlist>>(url, { auth }))
.data;
assertEquals(items.map((p) => p.dumpCount), [expected], url);
}
assertEquals(
(await get<{ playlists: Playlist[] }>("/api/search?q=banana", { auth }))
.data.playlists.map((p) => p.dumpCount),
[expected],
);
assertEquals(
(await get<Playlist[]>(`/api/dumps/${pub.id}/playlists`, { auth })).data
.map((p) => p.dumpCount),
[expected],
);
const page = await get<PlaylistWithDumps>(`/api/playlists/${pl.id}`, {
auth,
});
assertEquals(page.data.dumpCount, expected);
assertEquals(ids(page).length, expected);
}
assertEquals(
(await get<PlaylistMembership[]>(`/api/me/playlists?contains=${pub.id}`, {
auth: owner.auth,
})).data.map((m) => [m.hasDump, m.playlist.dumpCount]),
[[true, 2]],
);
// An edit answers with the editor's count.
assertEquals(
(await put<Playlist>(`/api/playlists/${pl.id}`, {
auth: owner.auth,
json: { title: "Counted" },
})).data.dumpCount,
2,
);
});
test("reorder: must name exactly the current members", async () => {
const owner = await createUser();
const { id } = insertPlaylist({ userId: owner.id });