# Publish an album on Sound.fish

Sound.fish gives an album a page of its own at `https://sound.fish/@handle/album`: a waveform-and-spectrogram player, hearts, comments, and play counts. Every track keeps its revisions in order, listeners hear the current one, and the player embeds on any site. Publishing needs a Hraness account with a username; listening needs no account.

## Create and manage an album

1. Sign in and choose **Upload**, or open [sound.fish/audio/albums/new](https://sound.fish/audio/albums/new). Give the album a title. The slug defaults to the title and becomes the page address `/@handle/slug`.
2. On the manage page, add tracks by uploading MP3, WAV, or FLAC files. Each file is checked after it arrives; the track appears once it is ready.
3. Add a cover, a description, and details. The preview player above the form plays the album exactly as listeners will hear it, with plays, hearts, and comments switched off.
4. Choose **Publish**. The album page, your artist page at `/@handle`, and the embed go live. **Unpublish** returns the album to a draft that only you can open.

Albums start as drafts. A draft has no public page, no embed, and collects no plays, hearts, or comments. The manage page shows a usage line (`albums 1/1 · tracks 3/10 · storage 120 MiB/2 GiB`) from the same account read the CLI's `whoami` prints.

## Tracks and revisions

Upload a file to an existing track to add a revision. Revisions are numbered in order (`v1`, `v2`, …) and dated by UTC calendar day, so a comment left on an earlier version reads `v1 · Sep 14`. The current revision is the one listeners hear; promote any earlier or later revision at any time. A revision may carry a note and details that only you see. A file you already stored is not uploaded again.

Reorder tracks with **Up** and **Down**. Deleting a track or an album removes its page, frees its slug, and releases its storage; free accounts keep one album, so deleting makes room for another.

## Descriptions and details

Descriptions are plain text; blank lines separate paragraphs. They appear under the album title and inside the active track's lane. Details are up to 32 key–value pairs (keys are lowercase letters, digits, dots, dashes, or underscores; values up to 1,000 characters) rendered as a "Details" list on the album page and in the track lane. The embed shows neither.

## Formats and limits

Sound.fish is free to use, with these limits per account:

| Limit | Free plan |
| --- | ---: |
| Albums | 1 |
| Tracks per album | 10 |
| Revisions per track | 25 |
| Track file | 200 MiB |
| Track duration | 1 s to 20 min |
| Cover image | 12 MiB, 200–8,000 px per edge |
| Storage | 2 GiB, counting uploads in progress |
| Changes | 600 per rolling hour; 8 uploads in progress at once |

- **Audio:** MP3 (`audio/mpeg`), WAV (`audio/wav` or `audio/x-wav`; 16-, 24-, 32-bit PCM or 32-bit float, 8,000–192,000 Hz), or FLAC (`audio/flac` or `audio/x-flac`), mono or stereo. Duration is subject to the 200 MiB cap (16-bit/44.1 kHz stereo WAV ≈ 19.8 min, 24-bit/48 kHz ≈ 12 min; MP3 and FLAC reach 20 min). Compressed WAV encodings, 8-bit and 64-bit float samples, and RF64 files are refused as unsupported media.
- **Covers:** PNG, JPEG, or WebP. Sound.fish strips metadata and derives a 1,400 px cover and a 400 px thumbnail.
- **Text:** titles up to 120 characters; slugs of 1–64 lowercase letters, digits, and dashes (`new`, `manage`, `embed`, `edit`, `media`, and `cover` are reserved); descriptions up to 4,000 characters; revision notes up to 2,000; comments up to 2,000.

When a limit is reached, the manage page, the CLI, and the API show the same sentence, for example "Free accounts publish one album. Delete the existing album to create another." There is no paid plan and no upgrade link. `GET https://sound.fish/api/audio/v1/capabilities` publishes the accepted formats and every limit.

## Artist profile

Album, artist, and embed pages show the name and avatar from your published Hraness profile, and comments show your handle. Edit the profile at [account.hraness.com/account](https://account.hraness.com/account); when it is not published, pages show `@handle` alone. If another account later claims your handle at Hraness, Sound.fish releases it from your albums once that account signs in at sound.fish: the artist page and the album, track, and embed addresses under that handle then belong to the new holder, and your albums stop answering at them. Sign in at sound.fish to refresh your handle; your albums stay published and return at `/@your-current-handle/slug`, and an embed copied under the old handle must be copied again. Album and track ids keep working meanwhile.

## Listening, hearts, and comments

Anyone can listen without an account. A play counts once a listener has heard enough of a track; Sound.fish sets a first-party listener cookie (`__Host-soundfish-listener`) or, inside an embed, returns a listener id to the player so repeated plays are not counted twice. Hearts and comments need a Hraness sign-in on the album page. A comment can be pinned to a moment in a track and records which revision was current when it was written.

## Embed the player

A published album has an embed at `https://sound.fish/embed/@handle/slug`; add `?track=<track-slug>` to start on one track and `t=83` or `t=1m23s` to start at a moment. Album and track pages accept the same parameters, so a shared link opens on the right track at the right second without autoplaying. The player's share control copies such links. Copy the `<iframe>` from the manage page or print it with `soundfish audio embed <album> --html`:

```html
<iframe src="https://sound.fish/embed/@handle/slug" width="100%" height="488" title="Album by @handle" loading="lazy" allow="autoplay; clipboard-write" style="border:0"></iframe>
```

The embed shows the transport, waveform, track list, counts, and a read-only preview of the active track's latest three comments; hearting or opening the comments goes to the album page in a new tab. The height is an estimate from the track count. The page posts `{ "type": "soundfish:resize", "height": <px> }` to its parent window whenever its content height changes, so a host page can listen for that message and resize the frame. Once a listener switches tracks it also posts `{ "type": "soundfish:location", "track": "<track-slug>" }`, so the host can mirror `?track=` in its own address bar. Allow `clipboard-write` on the frame so the share control can copy links.

## Markdown pages

Every artist and album page is also served as Markdown for agents and readers: append `.md` (`https://sound.fish/@handle/slug.md`) or request the page with `Accept: text/markdown`. The Markdown carries the title, artist, description, details table, track list with durations and page URLs, and the embed URL.

## Publish from the command line

The `soundfish audio` verbs publish from a terminal or a coding agent. Create an API token at [sound.fish/settings/tokens](https://sound.fish/settings/tokens) (it is shown once), then run `soundfish audio login`, which prints that address and reads the token with echo off; in a script, pipe it in with `--token-stdin`. Tokens look like `sfk_` followed by 40 letters or digits and are stored under `$XDG_STATE_HOME/hraness/soundfish/credentials.json` in a directory only you can read. Revoke a token at the same settings page.

```sh
soundfish audio login [--token-stdin]
soundfish audio logout
soundfish audio whoami
soundfish audio albums list|create|show|update|publish|unpublish|delete|stats
soundfish audio tracks list|add|update|remove|reorder
soundfish audio upload <album> <file ...> [--track <track>] [--title <text>] [--slug <slug>] [--note <text>] [--no-promote]
soundfish audio revisions list|promote
soundfish audio art set <album> <image>
soundfish audio embed <album> [--html]
soundfish audio status <assetId>
```

An album is named by `alb_<id>`, `@handle/<slug>`, or one of your own slugs; a track by `trk_<id>` or its slug. `upload` hashes each file, asks the API for an upload, streams the bytes to storage, waits for inspection, and attaches the result: without `--track` it creates a track from the file name (or `--title`) and refuses to replace an existing track silently (exit 4 with the hint to use `--track`); with `--track` it adds the next revision and promotes it unless `--no-promote` is given. `--track`, `--title`, `--slug`, and `--note` require exactly one file. For a draft album, every successful upload prints the manage page address on standard error so you can listen before publishing.

`update` verbs accept `--description-file <path|->`, repeatable `--metadata key=value`, and `--unset-metadata <key>`. Every verb accepts `--format json`, mutating verbs accept `--dry-run`, and `--api-url` (or `SOUNDFISH_API_URL`) selects the API origin. Exit codes: 0 success, 1 network failure or a 5xx response, 2 usage, 3 invalid local input, 4 conflict or plan limit, 5 rejected by the API. Standard output carries only results; progress and hints go to standard error. The [CLI audio reference](https://sound.fish/skills/soundfish/reference/audio.md) in the Soundfish skill repeats this procedure for agents.

## HTTP API

Every route lives under `https://sound.fish/api/audio/v1/`, accepts and returns JSON (request bodies up to 64 KiB), and answers a failure as `{ "error": { "code", "message", "retryable" }, "requestId" }` with codes `invalid_request`, `sign_in_required`, `forbidden`, `not_found`, `conflict`, `payload_too_large`, `unsupported_media`, `plan_limit`, `rate_limited`, and `unavailable`. A method the route does not implement answers 405 with the same envelope (code `invalid_request`), an `Allow` header listing the accepted methods, and `cache-control: no-store`. Authenticate with `Authorization: Bearer sfk_…`. Uploads are a three-step contract: `POST /uploads` with the file's SHA-256 and size returns a presigned `PUT`, the client sends the bytes there, and `POST /uploads/{assetId}/complete` inspects the file and reports `ready` or a failure code. Public reads need no token: `GET /capabilities`, `GET /public/albums`, `GET /public/albums/@handle/slug`, and `GET /public/artists/@handle`. The [OpenAPI description](https://sound.fish/openapi.json) lists every operation and schema.
