mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Network wiring on top of the pure playlistService core: - fetchCreatorPlaylists(pubkey) — lists kind-30082 by author via the L1 SubscriptionManager, collapses to newest-per-`d` (NIP-33 replaceable), drops empty playlists from the listing (a cleared playlist republishes empty), sorts newest-first. - fetchPlaylist(pubkey, id) — newest matching event, parsed. - publishPlaylist(playlist) — build + sign + publish via nostrClient.signAndPublishEvent (active signer), replacing the same `d`. All entry points take an injectable client for hermetic testing. tests/playlist-facade.test.mjs (5): newest-per-id + empty/other-author drop + sort, includeEmpty, blank/no-relay guards, fetch-one newest, publish emits a kind-30082 event with the right d/title/ordered a-refs under the "playlist" context. Mock client; no relays/signer. Lint clean; 5/5. Co-Authored-By: Claude Opus 4.8 <[email protected]>
157 lines
4.6 KiB
JavaScript
157 lines
4.6 KiB
JavaScript
// js/playlists/playlistFacade.js
|
|
//
|
|
// Network wiring for creator playlists (#37): fetch a creator's playlists / one
|
|
// playlist, and publish (build + sign + publish) a playlist. Kept apart from the
|
|
// pure playlistService core so that core stays hermetic. Reads route through the
|
|
// L1 SubscriptionManager (relay caps + in-flight dedupe), matching the rest of
|
|
// bitvid's read paths; writes go through nostrClient.signAndPublishEvent (active
|
|
// signer). All entry points take an injectable `client` for testing.
|
|
|
|
import { nostrClient } from "../nostrClientFacade.js";
|
|
import {
|
|
PLAYLIST_KIND,
|
|
parsePlaylistEvent,
|
|
buildPlaylist,
|
|
} from "./playlistService.js";
|
|
import { devLogger } from "../utils/logger.js";
|
|
|
|
const LIST_LIMIT = 200;
|
|
|
|
function getManagerAndRelays(client) {
|
|
const manager =
|
|
typeof client?.getSubscriptionManager === "function"
|
|
? client.getSubscriptionManager()
|
|
: null;
|
|
const relays = Array.isArray(client?.relays) ? client.relays : [];
|
|
return { manager, relays };
|
|
}
|
|
|
|
// Collapse to the newest event per `d` tag (NIP-33 replaceable): a flaky relay
|
|
// list can return an old + new copy of the same playlist, so keep only the
|
|
// freshest per id.
|
|
function newestPerDTag(events) {
|
|
const byD = new Map();
|
|
for (const event of Array.isArray(events) ? events : []) {
|
|
if (!event || !Array.isArray(event.tags)) {
|
|
continue;
|
|
}
|
|
const dTag = event.tags.find(
|
|
(t) => Array.isArray(t) && t[0] === "d" && typeof t[1] === "string",
|
|
);
|
|
const id = dTag ? dTag[1] : "";
|
|
if (!id) {
|
|
continue;
|
|
}
|
|
const existing = byD.get(id);
|
|
if (
|
|
!existing ||
|
|
(Number(event.created_at) || 0) > (Number(existing.created_at) || 0)
|
|
) {
|
|
byD.set(id, event);
|
|
}
|
|
}
|
|
return [...byD.values()];
|
|
}
|
|
|
|
/**
|
|
* Fetch a creator's playlists, parsed, newest-per-id, sorted newest-first.
|
|
* Empty playlists are dropped from the listing by default (a cleared playlist
|
|
* republishes as an empty replaceable event) — pass includeEmpty to keep them.
|
|
* @param {string} pubkey
|
|
* @param {{ client?: any, includeEmpty?: boolean }} [opts]
|
|
* @returns {Promise<Array>} parsed playlists
|
|
*/
|
|
export async function fetchCreatorPlaylists(
|
|
pubkey,
|
|
{ client = nostrClient, includeEmpty = false } = {},
|
|
) {
|
|
const author =
|
|
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
|
|
if (!author) {
|
|
return [];
|
|
}
|
|
const { manager, relays } = getManagerAndRelays(client);
|
|
if (!manager || typeof manager.list !== "function" || !relays.length) {
|
|
return [];
|
|
}
|
|
|
|
let events = [];
|
|
try {
|
|
events = await manager.list({
|
|
relays,
|
|
filters: [{ kinds: [PLAYLIST_KIND], authors: [author], limit: LIST_LIMIT }],
|
|
});
|
|
} catch (error) {
|
|
devLogger.warn("[playlists] Failed to list creator playlists:", error);
|
|
return [];
|
|
}
|
|
|
|
return newestPerDTag(events)
|
|
.map(parsePlaylistEvent)
|
|
.filter(Boolean)
|
|
.filter((playlist) => includeEmpty || playlist.items.length > 0)
|
|
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
}
|
|
|
|
/**
|
|
* Fetch a single playlist by (pubkey, id). Returns the newest matching event
|
|
* parsed, or null.
|
|
* @param {string} pubkey
|
|
* @param {string} id
|
|
* @param {{ client?: any }} [opts]
|
|
* @returns {Promise<Object|null>}
|
|
*/
|
|
export async function fetchPlaylist(pubkey, id, { client = nostrClient } = {}) {
|
|
const author =
|
|
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
|
|
const dTag = typeof id === "string" ? id.trim() : "";
|
|
if (!author || !dTag) {
|
|
return null;
|
|
}
|
|
const { manager, relays } = getManagerAndRelays(client);
|
|
if (!manager || typeof manager.list !== "function" || !relays.length) {
|
|
return null;
|
|
}
|
|
|
|
let events = [];
|
|
try {
|
|
events = await manager.list({
|
|
relays,
|
|
filters: [
|
|
{
|
|
kinds: [PLAYLIST_KIND],
|
|
authors: [author],
|
|
"#d": [dTag],
|
|
limit: 20,
|
|
},
|
|
],
|
|
});
|
|
} catch (error) {
|
|
devLogger.warn("[playlists] Failed to fetch playlist:", error);
|
|
return null;
|
|
}
|
|
|
|
const newest = newestPerDTag(events)[0];
|
|
return newest ? parsePlaylistEvent(newest) : null;
|
|
}
|
|
|
|
/**
|
|
* Build + sign + publish a playlist (replaces the same `d` on relays, NIP-33).
|
|
* @param {Object} playlist { pubkey, id, title, description, image, items }
|
|
* @param {{ client?: any }} [opts]
|
|
* @returns {Promise<Object>} the signed event
|
|
*/
|
|
export async function publishPlaylist(playlist, { client = nostrClient } = {}) {
|
|
const event = buildPlaylist(playlist);
|
|
const result = await client.signAndPublishEvent(event, {
|
|
context: "playlist",
|
|
});
|
|
return result?.signedEvent || null;
|
|
}
|
|
|
|
export default {
|
|
fetchCreatorPlaylists,
|
|
fetchPlaylist,
|
|
publishPlaylist,
|
|
};
|