Posting a dump was a single form where the important choices were the easiest
to miss. It is now three panels: link or file, why & where, playlists.
Composition:
- No more URL/File toggle. An empty panel offers both ways in at once and the
kind follows what you actually did; a file dropped anywhere in the modal is
accepted, not just on the zone.
- Categories and visibility get their own panel instead of a disclosure that
read as optional, and the primary button stays "Next" until they've been
seen. Visibility carries a real label now.
- The draft (link, title, why, categories, visibility) is mirrored to
localStorage on every change and restored on reopen, so Escape or a stray
backdrop click costs nothing. Only an attached file can't be restored, so
that is the one case that asks before closing.
- URL dumps can carry a poster-supplied title instead of being stuck with
whatever the page scraped, editable right under the preview.
- Multipart uploads go through XHR so there is a real progress bar and a
percentage on the button, rather than 50 MB of silence.
Duplicates:
- New dumps.url_canonical column (+ index, backfilled by 0013) holding a lossy
key that ignores scheme, www., trailing slashes, tracking parameters and
YouTube share shapes. GET /api/dumps/by-url reads it, and the create form
warns "already dumped by X" while it fetches the preview. Never blocking.
Fixes:
- /api/preview now reports whether the page was actually reached: a failed
fetch still yields a hostname-only stub, so a dead link and a page without
metadata used to render identically.
- The Web Share Target never worked. The manifest posts to "/", but the index
redirect dropped the query string, so every Android share landed on the feed
with nothing pre-filled.
- File dumps no longer take the extension into their title.
- The link field no longer autofocuses on touch, where it raised a keyboard
over the modal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiAPtJZeCYYk8rKehtLUQU
The dump modal's window-level paste listener looked at the clipboard files
before it looked at the event target, so an image pasted into the "Why?"
field was claimed twice: the editor uploaded it as an inline attachment,
and the modal switched itself to file mode with that same image as the
dump. Writing a URL dump with an illustrated description was impossible.
The target guard that already existed for text pastes now runs first and
covers files too — fields own their paste, and the URL input keeps its own
handler for the image-to-file-dump shortcut. Pasting onto the modal body,
where nothing else is listening, still starts a file dump.
Card teasers are line-clamped boxes, which an embedded image blows apart.
Inline markdown — the mode the dump and journal cards already ask for —
now drops img nodes, so a description image shows up on the detail page
and nowhere else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiAPtJZeCYYk8rKehtLUQU
Re-resolving a stream is a network round-trip, and nothing checked that the
player still belonged to that item once it landed. Picking another dump —
or closing the player — while a resolve was in flight let the stale result
swap the queue back; from onError, which resolves with autoplay, it would
also start playing over whatever was chosen instead. A generation counter,
bumped by playQueue/advanceTo/stop, now drops any result that no longer owns
the player, with a separate counter owning the resolving flag so a
superseded resolve can't clear a newer one's spinner.
A track that stays dead after a re-resolve is one track, not one album: the
cooldown path now steps to the next entry and only falls back to the iframe
on the last one. A failed resolve still goes straight to the embed, since
that's an album-wide failure rather than a single bad URL. And a track that
has vanished from the release, or lost its streamable flag to Bandcamp's
free-play cap, no longer resumes track 1 at the dead track's offset — the
findIndex miss resets the offset instead of seeking past the end of another
track.
Clicking the playing row rewound it but never resumed: seekTo only moves
currentTime, so re-selecting a finished or paused track looked like a
no-op. It now resumes as well. Same idea in the compact rich-content card,
whose button advertises "Pause" while active but re-played on click — in
native mode that meant a fresh /api/bandcamp/tracks and a restart from
track 1. It pauses for streams now; embeds have no transport of their own
and keep restarting, as before.
playRichContent reports whether playback actually started, so a native-mode
Bandcamp page with no streams and no stored embedUrl — an artist root, a
/music index, a preorder — is no longer a dead click: the journal card
navigates to the dump as it used to, and the rich-content card opens the
source page.
Also: the tralbum fetch releases the response body on its two error paths.
The endpoint is unauthenticated, so random /track/<slug> URLs would leak a
connection per request while also defeating the cache.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E56F55N7m5GGKKEwoYFJtH
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
The hot/new feeds go through FilePreview, which asks for
GET /api/thumbnails/:dumpId unconditionally on a video mime and lets VideoThumb
fall back to an icon if it 404s. The journal instead only recognised image files
and dump.thumbnailMime — and that field maps to custom_thumbnail_mime, i.e. a
thumbnail somebody uploaded by hand. Nothing on the Dump object advertises the
ffmpeg still the route generates on demand, so hasThumbnail() returned false,
mode degraded to text, and the card rendered a 🎬.
hasThumbnail() and JournalCard's thumbnail resolution now both treat a video
file as art, resolving to the same route. Since hasThumbnail() also gates grid
footprints, video dumps can now claim feature/tall/wide slots — the mosaic has
noticeably more image cards as a result, which is the point.
ThumbnailPlaceholder gains `glyph` and `seed`: a host without ffmpeg gets a 404
and lands on the placeholder, where the mime emoji says more about the dump than
an initial taken from its filename, and the hue is seeded from the filename
since there's no hostname to hash.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0011 tested for "favicon" anywhere in the path, so mirtitles.org's declared
og:image — /wp-content/uploads/2022/04/mir-logo-favicon.png — was moved into
faviconUrl and its thumbnailUrl cleared. Without a thumbnail, hasThumbnail() is
false and the journal mosaic demoted the dump from an image card to a
pull-quote. A blanket .svg match did the same to real artwork (tanibis.net's
header.svg).
0012 judges an icon by its filename rather than the whole path: named
favicon…/apple-touch-icon…/icon…, living in an icons/ directory, or ending in
.ico. That spares mir-logo-favicon.png and also catches icon_SEARCH.png, which
0011 missed. It runs in both directions, and is deliberately narrow when
reversing — a value is only promoted back to thumbnailUrl if it matched 0011's
rule but not this one, so rows written by a normal fetch aren't disturbed.
On the dev database: 4 thumbnails restored, 1 icon reclassified, real favicons
left alone, and a second pass rewrites nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Thumbnails were failing in two different ways that both ended as an empty box.
Cloudflare hotlink protection answers a cross-site Referer with 403, so images
we had extracted correctly (dles.aukspot.com's og:image among them) never
rendered — every onError handler set display:none and swallowed it. The new
Thumbnail component loads with referrerPolicy="no-referrer", retries once
through /api/proxy-image for hosts that reject an empty referrer too, and only
then falls back to a placeholder.
Separately, the extraction cascade ended at the page's icon and then a guessed
/favicon.ico, so thumbnailUrl was almost never empty — just a 16x16 icon
cover-cropped into a 128x72 box. It now stops at real artwork, with faviconUrl
and accentColor (theme-color / msapplication-TileColor / mask-icon) as their own
fields. An absent thumbnailUrl finally means "no artwork", which is what makes
the placeholder possible: the site's own color mixed into the theme surface,
with its favicon centered on it, or its initial. Contrast holds for any
third-party color by construction rather than by luminance math, so nyt only
has to set --thumb-tint-strength to 0% to stay monochrome and geocities only
has to raise it. Missing accents fall back to a stable hostname-derived hue, so
rows saved before this get a tint with no backfill.
Migration 0011 reclassifies favicon-shaped thumbnailUrls on existing dumps.
The journal mosaic keeps its pull-quote and text fallbacks — the placeholder
appears there only to repair a broken image.
Also fixed: refresh silently overwrote good metadata with a failure stub, the
refresh button swallowed every error, refresh never broadcast the update,
extractBestIcon ranked SVG icons below 16x16 PNGs, and shared links with no
artwork carried no og:image at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>