← Field Notes
Field Notes — 006

15 Episodes. One Session. No API.

June 24, 2026  ·  Hold the Row Podcast  ·  Spotify for Creators
Session Output

Spotify for Creators has no public API for episode uploads. The file picker can't be triggered programmatically — browser security blocks it. There's no CLI, no webhook, no endpoint to hit.

So we built one anyway.

This is the field note for the session where we published all 15 Hold the Row episodes to Spotify in a single run — titles, descriptions, and audio files up to 78MB — using nothing but browser automation and a JavaScript pattern that routes around every restriction the platform puts in front of you.

15
Episodes published
78MB
Largest file
0
Manual uploads
0
API calls

The Constraint

The Chrome MCP tool we use for browser automation has a 10MB limit on file injection. Every Hold the Row audio file is between 30MB and 78MB. Direct upload was off the table.

The native file picker — the one you click to select a file from your computer — can't be opened by JavaScript. That's an intentional browser security boundary. You can't call .click() on a file input and expect a dialog to open.

What you can do is assign a FileList directly to input.files. The browser allows this. Spotify's UI watches for it. So the question becomes: where do you get the file?

The Pattern

The audio files already live on this server at tff365.com/audio/. We added CORS headers to the Caddy config so JavaScript running inside Spotify's browser tab can fetch across origins:

@audio path /audio/* header @audio Access-Control-Allow-Origin * header @audio Access-Control-Allow-Methods "GET, OPTIONS"

Then, from inside Spotify's tab, for each episode:

async function uploadFromUrl(url, filename) { const response = await fetch(url); const blob = await response.blob(); const file = new File([blob], filename, { type: 'audio/mp4' }); const dt = new DataTransfer(); dt.items.add(file); const input = document.querySelector('input[type="file"]'); input.files = dt.files; input.dispatchEvent(new Event('change', { bubbles: true })); return `${(file.size / 1024 / 1024).toFixed(1)}MB loaded`; }

The browser fetches the file from tff365.com, builds it into a File object, wraps it in a DataTransfer, and assigns it to Spotify's hidden file input. Spotify thinks a user just picked a file. The upload begins.

No size limit. No manual interaction. Works on every file we threw at it.

The Description Problem

Spotify's description field is a rich text editor. If you programmatically type into it or set .value the normal way, you get garbled output — the editor's internal state doesn't sync with what you injected.

The fix: toggle the field into raw HTML mode first (there's a small toggle in the UI), then use the native value setter — the one defined on HTMLTextAreaElement.prototype, not the one the React layer has overridden:

const textarea = document.querySelector('textarea'); const setter = Object.getOwnPropertyDescriptor( window.HTMLTextAreaElement.prototype, 'value' ).set; setter.call(textarea, '<p>Your description here.</p>'); textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('change', { bubbles: true }));

React sees the dispatch, syncs its internal state, and the description is clean on the Review page.

Episodes Published

EP Title Size
01The Night TFF365 Got a Heartbeat78.3MB
02Building Revenue One Row at a Time63.2MB
03Coding AVA OS After Losing Kennedy75.2MB
04One Row Starter Kit for Immediate Execution
05Earn from Learning with the Income Ladder
06Stop Treating Financial Advice Like Entertainment38.1MB
07Stop Spinning and Build Income Systems
08The Boxing Blueprint for Financial Freedom
09The Sweet Science of Financial Readiness39.2MB
10Fire Your Willpower and Build Systems68.9MB
11Stop Running Your Money on Willpower42.2MB
12Stop Financial Spinning in Five Days32.8MB
13Engineer Financial Freedom in Five Days
14Complexity is Just Sophisticated Procrastination~30MB
15Chat Thread: Purgatory to Live Production78.1MB

What We Learned

Lesson 01

The constraint is the API, not the UI.

If a platform exposes a UI, you can usually automate it. The file picker isn't the only path into a file input — it's just the one they show you.

Lesson 02

CORS on your own server is a superpower.

Having your assets on infrastructure you control — with headers you can set — means you can use them as a resource from inside any browser tab. tff365.com became the upload source for Spotify.

Lesson 03

React overrides native setters. Bypass them directly.

Most React form fields intercept .value = x and don't fire the right events. Go one level deeper — use the prototype's native setter and dispatch your own events.

Lesson 04

The pattern is reusable.

This works on any web-based platform that accepts input[type="file"] and doesn't lock down its CSP. YouTube Studio, podcast platforms, CMS file uploaders — same pattern, different URL.

The podcast is live. All 15 rows. One session. Hold the row.
▶  Listen on Spotify — Hold the Row