Files
bitvid/js/playlistView.js
T
thePR0M3TH3ANandClaude Opus 4.8 2a543c3ba9 playlists(#37): playlist view fetches its videos (fix deep-link showing recent feed)
On a DIRECT load of a playlist URL the video cache is empty, so the view
resolved zero videos and the shared #videoList kept showing the default recent
feed. The view now fetches the referenced creators' videos from relays
(nostrService.fetchVideosByAuthors) before resolving, so a cold/deep-linked
playlist renders its ACTUAL videos in order. Resolution runs against the fetched
pool + the active cache; the grid is always rendered (even empty) so it can't
fall through to the recent feed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 14:48:03 -04:00

165 lines
4.9 KiB
JavaScript

// js/playlistView.js
//
// The playlist view (#37): #view=playlist&pubkey=<hex>&id=<playlist id>. Fetches
// the playlist, resolves its ordered `a` refs to the app's active video objects,
// and renders them into the shared #videoList grid via app.renderVideoList
// (preserveOrder so the playlist's order is kept, not re-sorted by date — #21).
// Gated by FEATURE_PLAYLISTS.
import { nostrClient } from "./nostrClientFacade.js";
import { fetchPlaylist } from "./playlists/playlistFacade.js";
import { buildVideoAddressPointer } from "./utils/videoPointer.js";
import { safeEncodeNpub } from "./utils/nostrHelpers.js";
import { FEATURE_PLAYLISTS } from "./constants.js";
import { devLogger } from "./utils/logger.js";
function readParams() {
const params = new URLSearchParams(
typeof window !== "undefined" && window.location
? window.location.hash.slice(1)
: "",
);
return {
pubkey: (params.get("pubkey") || "").trim().toLowerCase(),
id: (params.get("id") || "").trim(),
};
}
// Resolve the playlist's items to video objects, in playlist order, from the
// given pool. Unresolved refs (video missing) are skipped — the count line
// reflects how many resolved.
function resolveOrderedVideos(playlist, videoPool) {
const byCoordinate = new Map();
const byEventId = new Map();
for (const video of videoPool) {
const coordinate = buildVideoAddressPointer(video);
if (coordinate && !byCoordinate.has(coordinate)) {
byCoordinate.set(coordinate, video);
}
if (video && typeof video.id === "string" && video.id) {
byEventId.set(video.id, video);
}
}
const ordered = [];
for (const item of playlist.items) {
const video =
item.type === "a"
? byCoordinate.get(item.value)
: byEventId.get(item.value);
if (video) {
ordered.push(video);
}
}
return ordered;
}
// The videos to resolve against: the referenced creators' videos (fetched from
// relays so a DIRECT load / cold cache still works) plus whatever's already
// active. Without the fetch, a deep-linked playlist resolves nothing and the
// shared grid shows the default feed instead.
async function gatherVideoPool(playlist, app) {
const authors = [
...new Set(
playlist.items
.filter((item) => item.type === "a")
.map((item) => item.value.split(":")[1])
.filter(Boolean),
),
];
let fetched = [];
if (authors.length && typeof app?.nostrService?.fetchVideosByAuthors === "function") {
try {
fetched = (await app.nostrService.fetchVideosByAuthors(authors)) || [];
} catch (error) {
devLogger.warn("[playlistView] Failed to fetch playlist videos:", error);
}
}
let active = [];
try {
active = nostrClient.getActiveVideos() || [];
} catch (error) {
active = [];
}
return [...fetched, ...active];
}
function setText(id, text) {
const el = document.getElementById(id);
if (el) {
el.textContent = text;
}
}
export async function initPlaylistView({ getApp } = {}) {
const app = typeof getApp === "function" ? getApp() : null;
const { pubkey, id } = readParams();
if (!FEATURE_PLAYLISTS) {
setText("playlistTitle", "Playlists are not enabled.");
return;
}
if (!pubkey || !id) {
setText("playlistTitle", "Playlist not found.");
return;
}
// Point the back link at the creator's channel.
const back = document.getElementById("playlistBackLink");
const npub = safeEncodeNpub(pubkey);
if (back && npub) {
back.setAttribute("href", `#view=channel-profile&npub=${npub}`);
}
let playlist = null;
try {
playlist = await fetchPlaylist(pubkey, id);
} catch (error) {
devLogger.warn("[playlistView] Failed to fetch playlist:", error);
}
if (!playlist) {
setText("playlistTitle", "Playlist not found.");
setText("playlistMeta", "");
return;
}
setText("playlistTitle", playlist.title);
const pool = await gatherVideoPool(playlist, app);
const videos = resolveOrderedVideos(playlist, pool);
const total = playlist.items.length;
setText(
"playlistMeta",
videos.length === total
? `${total} ${total === 1 ? "video" : "videos"}`
: `${videos.length} of ${total} videos available`,
);
// Always render into the shared grid — even an empty playlist must OVERRIDE
// the default feed that populates #videoList, so a deep-linked playlist never
// shows unrelated recent videos.
if (app && typeof app.mountVideoListView === "function") {
app.mountVideoListView({ includeTags: false });
}
if (app && typeof app.renderVideoList === "function") {
try {
await app.renderVideoList({
videos,
metadata: {
reason: "playlist",
preserveOrder: true,
emptyStateMessage: "None of this playlist's videos could be loaded.",
},
});
} catch (error) {
devLogger.warn("[playlistView] Failed to render playlist videos:", error);
}
}
}
export default initPlaylistView;