2  destroy  the  platform

Blandcamp

Git Music Server

U  must  becum  the  platform

Introducing...

Blandcamp is a music store that you run yourself. A release arrives as a git push, and the store streams the tracks and sells the downloads.

Payments land in your own Stripe account, and all data is kept on a machine that only you own. One binary and one data dir is the whole install, and the HTML, CSS and JS are yours to edit.

Just like Wargames, the control panel is a terminal you connect to over SSH.

It can run on pretty much anything... the old family computer you haven't turned on in years, the laptop with the broken screen you still keep around, or a Pi Zero as a dedicated device.

The Blandcamp store with a release open, and the terminal dashboard over it

View live instance

$35

Linux/Mac/Pi

Run on Pi

Blandcamp on Pi 5

Be your own datacentre.

You are the only shareholder, there are no board meetings.

Blandcamp can be run on many types of Raspberry Pi, from Pi Zero to Pi 5. No monitor, no keyboard — connect over wifi or Ethernet. We recommened the Pi 5 or Pi Zero 2W.

Blandcamp supports the Waveshare e-ink hat. The display cycles through stats such as stream counts, sales data, country popularity.

Install on Pi

Simplest install to get running, feel free to overcomplicate.

Flash an SD card with Pi Lite OS, whichever version your Pi supports, and enable SSH. The instructions below assume the user you created was called diana and the device was called blandcamp. The scp line takes the arm64 build, so swap in the file for your Pi.

# copy the build to your Pi
scp blandcamp-linux-arm64.tar.gz [email protected]:
# after sshing in
sudo apt-get update
sudo apt-get install -y ffmpeg

# unpack it and run the TUI
tar xzf blandcamp-linux-arm64.tar.gz && cd blandcamp
./blandcamp

By default, blandcamp listens on port :2468, so on your own network it answers at http://blandcamp.local:2468. The reverse proxy or the router comes next; set either up carefully.

Pi builds

Piimagedownloadfile
Pi Zero, Zero W, Pi 132-bitLinux (ARMv6 / Pi Zero)blandcamp-linux-arm6
Pi 2, Pi 3, Pi 432-bitLinux (ARM / Raspberry Pi 2+)blandcamp-linux-arm
Zero 2 W, Pi 3, Pi 4, Pi 564-bitLinux (ARM64 / Raspberry Pi)blandcamp-linux-arm64

Case for Pi Zero 2

Case for Pi Zero 2W with e-ink hat.

Pi will snap fit! No supports required.

Download STL

Zero

Docs

Commands

blandcamp            the dashboard
blandcamp serve      the web store and git transport, in the foreground
blandcamp templates  write the default templates to <data>/templates
blandcamp create     create a release and print its push instructions
blandcamp artist     create a non-admin artist

--data DIR           data dir (default: $BLANDCAMP_DATA, else ./data)

Requirements

  • A server or computer with SSH: an old desktop, a Pi 5, a VPS
  • Git and ffmpeg installed, to push releases and make the stream
  • A custom domain
  • A Stripe account, to sell downloads
  • Enough git knowledge to push a repo

Interface

The Blandcamp site is read-only, and everything on it comes from a file you can edit.

Templates

One file per page. layout.html is the frame around them: the head, the header, the stylesheet links. Each page says what goes in the middle.

index.html
The front page, a grid of every published release and the server's readme.
repo.html
One release. The cover, the player, the readme, the files, the commits, the replies.
file.html
One source file, highlighted.
cart.html
The basket, with a discount code field and the mailing list opt-in.
receipt.html
An order and its download links.
list.html
The page a mailing list confirmation or unsubscribe link lands on.
profile.html
The fediverse profile: a handle, the counts, the releases.
share.html
The embed a release is posted as. Its own document, no layout.

Fonts, stylesheets and modules sit in a public/ dir beside the pages, served from /public/. A module is imported by name, so adding one means adding that name to layout.html.

There are 47 themes from the hundredrabbits collection, and an svg dropped in themes/ under the data dir joins them. Pick one in the TUI and every open tab repaints where it stands.

Your own files

Three files in the data dir are yours to drop in.

profile.png, .jpg or .gif
The account's picture, served to the fediverse. One image covers the store and its artists.
favicon.png, .svg or .ico
The tab icon.
readme.md
The store's own readme, under the releases on the front page.

Without a picture or an icon, the store draws its own mark.

Editing the templates

The default templates are built in. To take them over, run the binary against your data dir:

blandcamp templates

That writes the templates and the assets into <data>/templates, and the store uses that copy from then on. Edit a file and reload the page to see it. Restart blandcamp serve once, after the first export.

blandcamp templates overwrites whatever is there, so keep your own edits somewhere else as well. New sheets go in public/css, new modules in public/js inside it, linked from layout.html.

The palette is a block of CSS variables at the top of layout.html, and everything else is an ordinary file. Nothing is compiled between an edit and the page.

Terminal

Run blandcamp over SSH and you are in the dashboard. There is no web admin. esc leaves a screen; esc on the dashboard quits.

Every list works the same: up and down move, enter selects, esc goes back. Typing jumps to the first entry with that prefix, and each menu remembers where the cursor was.

Setup

On a fresh store the dashboard opens on Create Admin User instead of the menu. The admin is the way in, and cannot be deleted later.

Admin name
Your name. The slug is proposed from it: enter accepts, or type another.
Password
You type it twice and never see it.
Server Name
The store's name on the site.
Currency
The currency for prices, from Stripe's list.

Blandcamp

The dashboard. The header shows the site URL, and, with federation on, the store's handle and follower count.

Create Release
Start a new release.
Edit Release
Pick one you already made. Hidden until there is one.
Sales: N
The purchases. Hidden until there is one.
Subscribers: N
The mailing list. Hidden until there is one.
Artists: N
The artists that may push. The admin is not counted.
Comments: N
Comments on every release, and how many are new. Always in the list, 0 included.
Settings
The server, payments, mail, the fediverse, and the store-wide choices.

esc quits.

Create release

Asks for a title, then shows the slug and takes enter or a new one. That makes the release's git repo, prints its push instructions, and waits.

git remote add origin https://your.store/push/<slug>
git push origin main

A fresh repo gets the git init lines too. The wait counts seconds, and esc skips it. When a push lands it prints the tracks read from their tags, then the file tree, and drops into the release editor.

Release editor

One release, headed title (slug).

Title: X
The name on the page. The slug stays: it is the URL and the git remote.
Artists: none
Who may push to it. The artist name the page shows comes from the same artists.
Cover: auto
Auto uses a cover.* file from the release. Pick an image to override it.
Published: yes/no
Publishing also announces it to followers, once per release.
Tracks: N Locked
Locks tracks, counting them beside the label once one is. A locked one keeps its place in the player and does not play.
Price: Free
Set it on the price screen.
Streaming: default (192 kbps)
The rate this release streams at, without moving the store-wide one. Default rides it.
Password: none
A shared password to read it. Empty clears it.
Plays: N
Plays and listeners per track, over the last two weeks.
Comments: N
Comments on this release, hidden ones included.
Codes: N · N used
Discount codes for this release, spent or waiting.
Theme: default (Autism)
A palette for this release's page alone. Default rides the store's.
Preview
A 24h link to show it before publishing. Remake it to revoke. In the list while the release is unpublished.
Delete
Deletes the release and takes down the post about it.

Price

Name Your Price or Fixed, then the amount in whole cents. A floor may be zero; a fixed price may not.

Tracks

Every track with its state beside it, [locked] on the ones that are, and Unlock All or Lock All above. Picking one flips it.

Locking keeps a track out of the stream and leaves it in the player, so a couple can be heard before anyone buys and the rest are kept. The download carries every track either way.

Codes

A code takes money off one release, or covers it whole, and each one works once. Generate asks what they are worth, how many, and when they lapse. List Unspent appears once there is one, and prints the ones nobody has used.

Buyers type one into the cart, under the total.

Artists

The artists that may push to the git server. The header names the admin, which is not in the list.

Add Artist
A login for a new artist, who starts with no releases.
an artist
Give it a title, set push access, reset its password, or delete it.

Deleting one leaves you on the list, not on the artist that is gone.

An artist has no picture of its own: the store's stands for all of them.

Push access

The catalogue with this artist's access beside each release, as slug: on or slug: off. Selecting one toggles it. The same list is reachable from a release's own menu, under Artists.

Comments

Every comment the store holds, newest first, or one release's when opened from its menu. A comment that arrived since you last looked is prefixed *.

Mark All as Read (N new)
Clears the unread marks.
a comment
The handle and a preview of the comment.

One comment opens on who wrote it and the whole text, then offers Reply, Hide (or Show), Block <host>, and Delete.

Sales

The header is the purchase count and the revenue. A row is date, amount, release and email, with a free download marked free.

Export CSV
Every sale to a CSV in data/exports.
a sale
One sale and the link to hand over if the buyer lost it.

The link carries the buyer's token, so the export is a credential: keep it off the web.

Subscribers

The header is who is on the list and how many are waiting to confirm. A row is the day, the state, and the address.

Export CSV
Every address with its state and its dates, to a CSV in data/exports.
an address
One address and where it stands: on the list, waiting, or gone.

The list is seen and exported here alone: the store the public reads has no admin routes to hang it on.

Settings

Server
The name, the readme, the domain, the proxy, the port, your password.
Payments
The currency and the Stripe keys.
Mail
The SMTP the store sends from, and the mail to the list.
Fediverse
The store's handle, its profile, its followers.
Push to Create: on/off
Whether a push to a slug nobody owns makes a release. Off, releases are made here, and an artist pushes only to what they were granted.
Bitrate: N kbps
The stream quality: 16 to 320 kbps, 192 unless you say otherwise.
Theme: X
The palettes on offer, the built-in ones and your own. Moving down the list previews each one.

Server

Server Name
The store's name on the site.
Server Readme
The store's own readme, from readme.md in the data dir, shown on the front page.
Domain
The public hostname, with no https:// and no port. Local names get plain HTTP.
Proxy
The addresses a proxy comes from, or off. Only a peer in that list may name the client address; Cloudflare is recognised by its own addresses and needs no entry.
Port
The port it listens on. A running server keeps the old one until restarted.
Password
Change your password.
Release Notice: on/off
The store asks whether there is a newer build, once a day, and the header carries it as v1.4 -> v1.5 when there is. Nothing but the version moves. Check Now asks on the spot.

Payments

The store is the payee: every payment goes to the admin's Stripe, and the checkout never asks who made a release. Run it as a label and you settle with your artists yourself: every row names its release, and the export is the ledger to add up from.

The webhook URL is printed to paste into Stripe's dashboard. Each key reads set or not set and is never shown: paste a new one, or enter to keep.

Currency
The currency for prices and checkout.
Secret Key
From Stripe's API keys page. A restricted one is enough.
Publishable Key
Unused: buyers land on Stripe's own page, so it can stay empty.
Webhook Secret
From the webhook you point here, added by hand under Developers, then Webhooks. Subscribe it to checkout.session.completed, and to checkout.session.async_payment_succeeded if you want the slow methods covered. Anything else is ignored. Without it, an order is only safe if the buyer stays on the page.

The secret key can be a restricted key. The store makes three calls, all on Checkout Sessions: create one when a buyer checks out, then read the session and its line items when the order lands. Nothing else is asked of Stripe, so Checkout Sessions write is the whole permission.

Mail

Where the store sends from. A buyer gets their download link by mail when an order lands, and publishing a release mails the list. The header carries the subscriber count.

Host / Port / Username / Password
Your SMTP account. Port 587 unless yours differs.
From
The address the mail comes from.
Send Test
Renders the purchase mail for an address you name, then sends it.
Email About a Release...
The release mail to the list, for one that went out before mail was set up.

One plain text file per message, in mail/: confirm.txt, purchase.txt, release.txt. blandcamp templates writes them out, so they are yours to edit.

The list is built from two opt-ins: the cart, and the free download form. An address that arrives any other way is mailed a confirmation first, and joins on the click. Every mail carries its own unsubscribe link, so it goes out one address at a time.

Fediverse

The server posts from one handle, and every artist posts from their own. Follow the server from Mastodon and the whole catalogue arrives; follow an artist and only their releases do. An artist's handle comes from their slug, so giving them a title does not move it.

A release page prints the handle to follow for that release: the artist holding it, or the server when nobody does. Replies land on whoever posted it.

The header carries the handle, and the delivery queue when it has anything in it.

State: off
Turns it on. Warns first if you have no public domain.
Handle
Your handle. Renaming it loses your followers, so it asks first.
Bio: auto
The text on your profile. Basic HTML.
Replies: on
Accept replies to releases.
Followers: N
Who follows. Pick one to remove it.
Blocked: N
Domains you blocked. Pick one to unblock.

Preparing a release

Make flacs from your source and keep them in one folder. A cover.png or cover.jpg beside them becomes the release's cover.

Tag the tracks before you push. The tags are the track list: title, artist and album are read from the files, and the player's order is the track number, so untagged files land at the end. Kid3 is free and fills a folder in one pass. Any tagger does.

The repo carries more than audio. A text file in it opens on its own page, highlighted, so a readme or the source of a patch sits beside the tracks. A binary is listed and not printed.

What you push is what buyers get: the stream is an mp3 made at the store's bitrate, and the download is the folder exactly as you pushed it, names and tree and all.

Hosting

The store is one binary and one data dir. How you run them is up to you.

  • The data dir is the whole store: bland.db, the release repos, the art and audio it makes, the themes and templates you add. Copy that dir and the backup is done.
  • FFmpeg makes each stream on demand. Without it the downloads still work and the player has nothing to play.
  • The suggested shape is a reverse proxy in front, with Caddy doing the TLS and its renewals. Name it in Settings, or every play and free download reads as one visitor.
  • A Cloudflare tunnel gets the store online from a machine with no public address, and nothing is forwarded. It is a proxy like any other, so name what it arrives from in Settings.
  • A static IP from your ISP is the other way to be reachable, and the reverse proxy still sits in front. A home address that moves takes the name in Domain with it.
  • Nothing here needs a bot filter. If the crawlers ever bother you, Anubis or go-away can sit in front, and the Stripe webhook, the fediverse and git have to stay out of the challenge: a server cannot answer one.
  • Set the domain before you publish. The fediverse handle, the mail links and the push instructions read from it.

Support

Setup questions and bug reports go to irc.auti.sm.

Mail works too: [email protected]. So does Mastodon.

Bought it and lost the link? Email us.

License

Blandcamp is nonfree software. The licence covers one instance. The binary is yours to run, not to pass on or resell. We accept no responsibility for your self-hosting setup.

It comes as it is, with no warranty. Questions get answered in the irc channel.

It's now safe to turn off your computer.