v3: native bandcamp playback and a queue in the global player
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 43s

The player now holds a queue instead of a single item, so albums play
through and single tracks share the same code path. Adds a "stream" item
kind for expiring, remotely-hosted sources: Bandcamp signs its mp3 URLs
for ~24h, so an item carries the page it came from and re-resolves itself
on error or on a stale restored session, falling back to the iframe embed
if that fails too.

Gated behind GERBEUR_BANDCAMP_PLAYER (default "embed"), injected into a
meta tag the same way as site-name/site-emoji.

Also: player header gains artwork and a subtitle, volume persists across
queue advances and reloads, and cross-origin streams get a plain seek bar
rather than a waveform that can never decode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SbtjNsT5wvuABnfqhegZEJ
This commit is contained in:
khannurien
2026-08-22 12:14:28 +00:00
parent eb323a8ba8
commit 2606ef4ccc
30 changed files with 1531 additions and 225 deletions

View File

@@ -1,4 +1,3 @@
import { useContext } from "react";
import { Link, useNavigate } from "react-router";
import { Plural, Trans } from "@lingui/react/macro";
import type { Dump } from "../model.ts";
@@ -11,7 +10,10 @@ import { VoteButton } from "./VoteButton.tsx";
import { Markdown } from "./Markdown.tsx";
import { Tooltip } from "./Tooltip.tsx";
import Thumbnail from "./Thumbnail.tsx";
import { PlayerContext } from "../contexts/PlayerContext.ts";
import {
canPlayRichContent,
usePlayRichContent,
} from "../hooks/usePlayRichContent.ts";
export type { JournalShape };
@@ -32,7 +34,7 @@ export function JournalCard(
) {
const navigate = useNavigate();
const { token } = useAuth();
const { play } = useContext(PlayerContext);
const { playRichContent } = usePlayRichContent();
const unread = !isOwner && isRecent(dump.createdAt) &&
!isDumpVisited(dump.id);
@@ -71,7 +73,13 @@ export function JournalCard(
})()
: "🔗";
const embedUrl = dump.richContent?.embedUrl;
const richContent = dump.richContent;
// In native mode a Bandcamp page is playable even with no stored embedUrl.
// The card's own thumbnail (dump upload or provider) becomes the player's
// header artwork.
const playable = richContent && canPlayRichContent(richContent)
? { ...richContent, thumbnailUrl: thumbnailUrl ?? undefined }
: null;
const titleLink = (
<Link
@@ -142,15 +150,8 @@ export function JournalCard(
return (
<li
className={className}
onClick={embedUrl
? () =>
play({
kind: "embed",
embedUrl,
title: dump.richContent?.title,
type: dump.richContent?.type ?? "unknown",
dumpHref: dumpUrl(dump),
})
onClick={playable
? () => void playRichContent(playable, dumpUrl(dump))
: handleNavigate}
>
<div className="journal-card-image">
@@ -167,7 +168,7 @@ export function JournalCard(
siteName: dump.richContent?.siteName,
}}
/>
{embedUrl && (
{playable && (
<span className="rich-content-play-overlay" aria-hidden="true">
</span>