Local Feeds
Local (archive-import) feeds: no upstream RSS, episode upload and edits, and bulk directory/upload import (#625)
POST /feeds/{slug}/artwork
Section titled “POST /feeds/{slug}/artwork”Upload cover art for a local, recents or YouTube feed
Local, recents and YouTube feeds only (400 on a subscribed RSS feed, whose artwork comes from the upstream RSS on refresh). JPEG or PNG, detected from the file bytes rather than the client-supplied content type. Bound by the app’s default 10 MB request cap. A response under 1400x1400 still succeeds but carries a warning field, since podcast directories generally expect at least that.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Request body (multipart/form-data, required)
Section titled “Request body (multipart/form-data, required)”file(string (binary), required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Artwork uploaded | object |
| 400 | Not a local, recents or YouTube feed, missing file, or not a JPEG/PNG image | Error |
| 404 | Resource not found | Error |
200 response body
Section titled “200 response body”message(string)artworkUrl(string)warning(string): Present when the image is smaller than 1400x1400 in either dimension.
POST /feeds/{slug}/episodes
Section titled “POST /feeds/{slug}/episodes”Upload a single episode into a local feed
Local feeds only. episode_id is minted as s{season:02d}e{episode:02d}; season defaults to 1, episode defaults to one past the highest existing episode number in that season. 409 if that id already exists; use the bulk import commit with overwrite for that. Widened request cap: audio up to 1 GB, streamed to a temp file on the same volume as the episode’s final storage path, not buffered fully in memory.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Request body (multipart/form-data, required)
Section titled “Request body (multipart/form-data, required)”audio(string (binary), required): .mp3 only, up to 1 GB.season(integer): Default1.episode(integer): Defaults to one past the highest existing episode number in this season.title(string)description(string)publishedAt(string (date-time)): ISO 8601, timezone required. Defaults to the upload time.artwork(string (binary)): Optional episode cover, JPEG or PNG, up to 10 MB.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 201 | Episode created | EpisodeDetail plus object |
| 400 | Not a local feed, no audio field, wrong extension, or unplayable audio | Error |
| 404 | Resource not found | Error |
| 409 | An episode with this season/episode already exists | Error |
| 413 | Upload exceeds the configured local upload size limit | UploadTooLarge |
201 response body
Section titled “201 response body”Includes every field of EpisodeDetail.
episodeNumber(integer)seasonNumber(integer)queued(boolean): Whether this upload was queued for auto-process (only possible when the feed already had at least one episode before this upload).
PATCH /feeds/{slug}/episodes
Section titled “PATCH /feeds/{slug}/episodes”Bulk-edit local episodes
Local feeds only. Every entry is validated before any are applied (atomic): one invalid entry 400s the whole request with nothing written. Up to 500 entries per request. RSS is regenerated once at the end, not per entry.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Request body (application/json, required)
Section titled “Request body (application/json, required)”Array of:
episodeId(string, required): sNNeNN local episode id.
Includes every field of LocalEpisodeEditFields.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Episodes updated | object |
| 400 | Not a local feed, not a list, empty, over 500 entries, duplicate episodeId, or an invalid field on any entry | Error |
| 404 | Feed not found, or an entry names an episodeId that does not exist | Error |
200 response body
Section titled “200 response body”updated(integer)
PATCH /feeds/{slug}/episodes/{episode_id}
Section titled “PATCH /feeds/{slug}/episodes/{episode_id}”Edit a local episode
Local feeds only. season/episode change only the display season_number/episode_number columns; the episode_id minted at upload time never changes.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifierepisode_id(path, string, required): Episode identifier: a 12-character MD5-hex string for a subscribed feed’s episode, or an sNNeNN id (e.g. s01e05) for a local feed’s.
Request body (application/json, required)
Section titled “Request body (application/json, required)”Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Episode updated | EpisodeDetail plus object |
| 400 | Not a local feed, or an invalid field | Error |
| 404 | Resource not found | Error |
200 response body
Section titled “200 response body”Includes every field of EpisodeDetail.
episodeNumber(integer)seasonNumber(integer)
DELETE /feeds/{slug}/episodes/{episode_id}
Section titled “DELETE /feeds/{slug}/episodes/{episode_id}”Delete a local episode
Local feeds only. Hard-deletes the row and its files. Unlike a subscribed feed’s episode delete, there is no upstream to rediscover it from, so it is not reset to ‘discovered’ for a future refresh to pick back up.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifierepisode_id(path, string, required): Episode identifier: a 12-character MD5-hex string for a subscribed feed’s episode, or an sNNeNN id (e.g. s01e05) for a local feed’s.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Episode deleted | object |
| 400 | Not a local feed | Error |
| 404 | Resource not found | Error |
| 409 | Episode is currently processing; cancel it first | Error |
200 response body
Section titled “200 response body”deleted(integer)episodeId(string)
POST /feeds/{slug}/episodes/{episode_id}/artwork
Section titled “POST /feeds/{slug}/episodes/{episode_id}/artwork”Upload cover art for a single local episode
Local feeds only. JPEG or PNG, up to 10 MB, detected from the file bytes.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifierepisode_id(path, string, required): Episode identifier: a 12-character MD5-hex string for a subscribed feed’s episode, or an sNNeNN id (e.g. s01e05) for a local feed’s.
Request body (multipart/form-data, required)
Section titled “Request body (multipart/form-data, required)”file(string (binary), required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Artwork uploaded | object |
| 400 | Not a local feed, missing file, too large, or not a JPEG/PNG image | Error |
| 404 | Resource not found | Error |
200 response body
Section titled “200 response body”message(string)episodeId(string)
POST /feeds/{slug}/import/upload
Section titled “POST /feeds/{slug}/import/upload”Stage archive-import files ahead of scan/commit
Local feeds only. Multipart, repeated field “files”, saved under each file’s original basename into the feed’s staging area (<data>/import-staging/<slug>/) so the sNNeNN naming scheme can be parsed by a later scan. A basename that is empty, a dotfile, over 255 UTF-8 bytes, or contains a path separator/“..”/NUL is rejected per-file rather than failing the whole request. Widened request cap: up to 1 GB total, same as the single-episode upload.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Request body (multipart/form-data, required)
Section titled “Request body (multipart/form-data, required)”files(array of string (binary), required)
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Files staged | object |
| 400 | Not a local feed, or no files field | Error |
| 404 | Resource not found | Error |
| 409 | An import is already running for this feed | Error |
| 413 | Upload exceeds the configured local upload size limit | UploadTooLarge |
200 response body
Section titled “200 response body”staged(array of string): Basenames successfully staged.rejected(array of object)rejected[].file(string)rejected[].reason(string)
POST /feeds/{slug}/import/scan
Section titled “POST /feeds/{slug}/import/scan”Dry-run a bulk archive import
Local feeds only. Scans the staging area and/or the user-managed import directory (<data>/import/<slug>/), applies the sNNeNN naming scheme and any JSON sidecars, synthesizes publish dates for entries that don’t have one, and returns the plan the UI renders for review. Nothing is written to disk or the database by this call.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Request body (application/json)
Section titled “Request body (application/json)”Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Dry-run plan | LocalImportPlan |
| 400 | Not a local feed, or an invalid source/overwrite value | Error |
| 404 | Resource not found | Error |
POST /feeds/{slug}/import/commit
Section titled “POST /feeds/{slug}/import/commit”Commit a previously scanned import plan
Local feeds only. Re-scans the same source(s) server-side and compares the result’s hash against the client-echoed planHash; a mismatch means a file changed since the scan and 409s rather than committing a stale plan. Starts a background job (one import per feed at a time; a second commit while one is running also 409s) and returns immediately; poll import/status for progress. An empty feed’s very first commit never queues anything for auto-process regardless of settings; a later commit into a non-empty feed does, skipping the publish-date recency check.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Request body (application/json, required)
Section titled “Request body (application/json, required)”planHash(string, required): The planHash returned by the most recent import/scan call for this feed and source/overwrite combination.
Includes every field of LocalImportRequest.
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 202 | Import started | object |
| 400 | Not a local feed, missing planHash, or an invalid source/overwrite value | Error |
| 404 | Resource not found | Error |
| 409 | planHash is stale (files changed since scan), an import is already running for this feed, or there is not enough free disk space for the plan’s importable bytes | Error |
202 response body
Section titled “202 response body”message(string)
GET /feeds/{slug}/import/status
Section titled “GET /feeds/{slug}/import/status”Poll a local feed’s background import job
Local feeds only. A slug with no import ever started reads as idle.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Import status | LocalImportStatus |
| 400 | Not a local feed | Error |
| 404 | Resource not found | Error |
DELETE /feeds/{slug}/import/staging
Section titled “DELETE /feeds/{slug}/import/staging”Clear a local feed’s upload staging area
Local feeds only. Empties <data>/import-staging/<slug>/. The UI clears staging itself at the start of every new file selection, so this is mainly for wiping an abandoned upload attempt by hand.
Parameters
Section titled “Parameters”slug(path, string, required): Podcast feed slug identifier
Responses
Section titled “Responses”| Status | Description | Body |
|---|---|---|
| 200 | Staging cleared | object |
| 400 | Not a local feed | Error |
| 404 | Resource not found | Error |
| 409 | An import is already running for this feed | Error |
200 response body
Section titled “200 response body”message(string)