A music player for your own music library, kept in an S3-compatible object storage bucket (AWS S3, Contabo, Backblaze B2, Wasabi, MinIO, Cloudflare R2…). It comes in two forms:
The app talks to your bucket directly from the browser. There is no server of its own, and your keys stay on your device.
--endpoint-url.The examples use these variables. Set them to your own values:
export S3BUCKET="music"
export S3ENDPOINT="https://eu2.contabostorage.com"
Configure the AWS CLI with a key that can also write to the bucket
(aws configure). Give the player its own read-only key if your provider
supports one.
The player shows the bucket’s folders as your library and lists the .mp3
files in them. Put the tracks in a folder for the artist and a subfolder for
the album:
music/ ← the bucket
├── ABBA/
│ └── Arrival/
│ ├── 01 Dancing Queen.mp3
│ ├── 02 Knowing Me, Knowing You.mp3
│ └── cover.jpg
├── Miles Davis/
│ └── Kind of Blue/
│ ├── 01 So What.mp3
│ └── cover.jpg
├── Road trip.json ← a playlist
└── Sunday morning.json ← another playlist
Without any other information, the player takes the artist, album and title of
a track from its path, Artist/Album/Title.mp3. More details come from the
object’s metadata. The player reads these keys:
| Metadata key | Meaning |
|---|---|
artist |
Artist |
album |
Album |
title |
Track title (name also works) |
tracknumber |
Track number |
length |
Duration in milliseconds |
year |
Year (recordingtime also works) |
genre |
Genre |
keywords |
Keywords |
image |
Key of the album art in the same bucket, such as ABBA/Arrival/cover.jpg |
S3 stores metadata as x-amz-meta-* headers, which can only hold ASCII text.
The player URL-decodes every value, so URL-encode the values when uploading
(Björk → Bj%C3%B6rk). Plain ASCII values without a % work as they are.
While a track plays, the player shows its album art and takes its color from the art.
A playlist is a JSON file at the root of the bucket. Its file name is the name
shown in the player. url is the key of the track in the bucket. The other
fields of a track are the same as the metadata keys above, written out without
URL-encoding:
{
"title": "Road trip",
"track": [
{
"url": "Miles Davis/Kind of Blue/01 So What.mp3",
"title": "So What",
"artist": "Miles Davis",
"album": "Kind of Blue",
"length": "545000",
"image": "Miles Davis/Kind of Blue/cover.jpg"
},
{
"url": "ABBA/Arrival/01 Dancing Queen.mp3",
"title": "Dancing Queen",
"artist": "ABBA"
}
]
}
To upload one track with its metadata:
aws --endpoint-url "$S3ENDPOINT" s3 cp "01 Dancing Queen.mp3" \
"s3://$S3BUCKET/ABBA/Arrival/01 Dancing Queen.mp3" \
--content-type audio/mpeg \
--metadata "artist=ABBA,album=Arrival,title=Dancing%20Queen,tracknumber=1,year=1976,length=230000,image=ABBA%2FArrival%2Fcover.jpg"
To upload a whole library, use a script that reads each file’s tags. Here is
one that uses ffprobe (from FFmpeg) and Python. Save it
as upload.sh and run it in a folder organized as Artist/Album/Track.mp3. It
uses a cover.jpg next to the tracks as the album art, when there is one:
#!/bin/sh
# Uploads every .mp3 under the current folder, with its tags as metadata
upload() {
file="${1#./}"
tag() { ffprobe -v error -show_entries "format_tags=$1" -of default=nw=1:nk=1 "$file" | head -n 1; }
enc() { python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"; }
ms=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$file" | awk '{printf "%d", $1 * 1000}')
meta="artist=$(enc "$(tag artist)"),album=$(enc "$(tag album)"),title=$(enc "$(tag title)")"
meta="$meta,tracknumber=$(enc "$(tag track)"),year=$(enc "$(tag date)"),genre=$(enc "$(tag genre)"),length=$ms"
cover="$(dirname "$file")/cover.jpg"
[ -f "$cover" ] && meta="$meta,image=$(enc "$cover")"
aws --endpoint-url "$S3ENDPOINT" s3 cp "$file" "s3://$S3BUCKET/$file" \
--content-type audio/mpeg --metadata "$meta"
}
find . -name '*.mp3' | while read -r f; do upload "$f"; done
Upload the album art and playlists with aws s3 cp or aws s3 sync:
aws --endpoint-url "$S3ENDPOINT" s3 sync . "s3://$S3BUCKET" \
--exclude "*" --include "*/cover.jpg"
aws --endpoint-url "$S3ENDPOINT" s3 cp "Road trip.json" "s3://$S3BUCKET/" \
--content-type application/json
The player keeps the metadata of tracks it has seen in the browser. After you change the metadata of a track that’s already uploaded, clear the site data of the player in the browser to see the change.
The browser lets the app read the bucket only if the bucket allows it with a
CORS configuration. This is needed when the app is served from another address
than the bucket, and for the TV app. Save this as cors.json:
{
"CORSRules": [
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": [
"x-amz-meta-artist", "x-amz-meta-album", "x-amz-meta-title",
"x-amz-meta-name", "x-amz-meta-tracknumber", "x-amz-meta-length",
"x-amz-meta-year", "x-amz-meta-recordingtime", "x-amz-meta-genre",
"x-amz-meta-keywords", "x-amz-meta-image"
]
}
]
}
and apply it:
aws --endpoint-url "$S3ENDPOINT" s3api put-bucket-cors --bucket "$S3BUCKET" \
--cors-configuration file://cors.json
Every request is signed with your key, so allowing any origin doesn’t make the
music public. You can replace * with the address of your copy of the app.
The latest version of the app is at
https://audioplayer.ctrldash.app/. It’s built from the main branch
of this repository and published with GitHub Pages. Your keys and music still
go only between your browser and your bucket. Open the address, enter the
settings, and you’re done.
To host a copy of your own, building the app needs Node.js and this repository:
git clone https://github.com/samuelmr/audio-player.git
cd audio-player
npm install
npm run build:pwa
The app is in dist/pwa. Serve its five files over HTTPS from any static
web host.
You can also serve the app from the object storage:
aws --endpoint-url "$S3ENDPOINT" s3api put-object --bucket "$S3BUCKET" --key index.html --content-type text/html --body dist/pwa/index.html
aws --endpoint-url "$S3ENDPOINT" s3api put-object --bucket "$S3BUCKET" --key audioplayer.js --content-type text/javascript --body dist/pwa/audioplayer.js
aws --endpoint-url "$S3ENDPOINT" s3api put-object --bucket "$S3BUCKET" --key sw.js --content-type text/javascript --body dist/pwa/sw.js
aws --endpoint-url "$S3ENDPOINT" s3api put-object --bucket "$S3BUCKET" --key manifest.json --content-type application/manifest+json --body dist/pwa/manifest.json
aws --endpoint-url "$S3ENDPOINT" s3api put-object --bucket "$S3BUCKET" --key play-192.png --content-type image/png --body dist/pwa/play-192.png
These five files have to be publicly readable. Depending on your provider, add
--acl public-read to the commands or make them public in the provider’s
settings. Your music doesn’t have to be public: the app reads it with your key.
The player doesn’t need to be in the same bucket as the music files.
Open the address of the app (or of your index.html) in a browser. On a phone,
you can add it to the home screen (Share → Add to Home Screen on iOS, the
menu → Install app or Add to Home screen on Android) to use it like an app.
The first time, the settings open by themselves. Later, open them with the ⚙ button.
| Setting | What to enter |
|---|---|
| S3 accessKeyId | The access key ID of your key |
| S3 secretAccessKey | The secret of your key |
| S3 endpoint | The address of the S3 service, without the bucket name, such as https://eu2.contabostorage.com or https://s3.eu-north-1.amazonaws.com |
| S3 region | The bucket’s region, such as eu-north-1. If your provider has no regions, us-east-1 usually works |
| S3 bucket | The name of the bucket |
| Player color | The base color of the player. Album art overrides it while a track plays |
Save stores the settings in this browser only. Cancel returns to the saved settings.
To move the settings to another device, use Transfer settings:
A home screen app on iOS has storage of its own, separate from Safari, so it needs the settings too. The settings code contains your secret key, so don’t share it.
index.html#ABBA/Arrival. Opening such an address adds that folder to the
queue.The TV app is not in Samsung’s app store. You build it yourself and install it on your TV in developer mode, which takes some time the first time.
The app needs a Samsung TV with Tizen 6.0 or later, which means models from 2021 on. To check your TV:
The app has been made for 2021 TVs and later, but it hasn’t been tried on every model. If yours is one of them and the app doesn’t work, please open an issue.
git clone, npm install).~/tizen-studio. If it’s elsewhere, set
TIZEN_STUDIO to its folder. In Tizen Studio’s Package Manager, install
from Extension SDK:
ipconfig; on Linux, ip addr).1, 2, 3, 4, 5 with the remote. If the remote has no number
keys, use the number pad that opens from its 123 key or from the screen.Samsung’s guide has pictures of these steps: Connecting the TV and SDK.
The TV runs only apps signed with a certificate that names it.
Samsung’s guide: Creating certificates.
npm run package:tizen
TV_IP=192.168.1.20 npm run install:tv
package:tizen builds the app and signs dist/tizen/CtrlMusic.wgt with the
active certificate profile. install:tv connects to the TV and installs it.
Use your TV’s address instead of 192.168.1.20. Ctrl music is then among the
apps on the TV.
Typing the keys with the remote is slow, so copy them from the web app:
Please open an issue with:
platform_version line of
~/tizen-studio/tools/sdb capability.git rev-parse --short HEAD)npm run package:tizen or npm run install:tvPhotos of the screen help too.
Contributions are welcome: bug reports, fixes, and new features.
You need Node.js (a current LTS version) and Git.
git clone https://github.com/samuelmr/audio-player.git
cd audio-player
npm install
npm run test:setup # once: downloads the Chromium for the end-to-end tests
You don’t need a bucket of your own to work on the app. The end-to-end tests come with a fake S3 bucket and a small music library, which you can also use by hand:
npm run build
node tests/e2e/server.js
Open http://127.0.0.1:4173/pwa/, and enter the settings from SETTINGS in
tests/e2e/library.js. npm start runs a development
server for the web app, which you can point at your own bucket.
The TV app needs Tizen Studio only for packaging it and trying it on a TV.
src/ has the code shared by both apps, and src/platform/ what differs
between thempwa/ has the web app’s HTML template, service worker, manifest and icontizen/ has the TV app’s HTML template and config.xmltests/ has the tests, see belownpm run build builds both apps, into dist/pwa and dist/tizen.
The TV app runs on Tizen 6.0 and later. The oldest of these TVs have
Chromium 76, so Babel compiles the TV app’s script for it, and the stylesheets
avoid what it lacks (such as gap in flexbox, inset and clamp()).
npm run test:build checks this.
Run npm run check before opening a pull request. It builds both apps and
runs every test that doesn’t need Tizen Studio.
| Command | Needs | Tests |
|---|---|---|
npm test |
Node | Unit tests of the source (tests/unit) |
npm run test:build |
Node | The built apps: their files, and that the TV app has nothing Chromium 76 lacks (tests/build) |
npm run test:pwa |
Node, Playwright’s Chromium | The web app in Chromium (tests/e2e/pwa) |
npm run test:tv |
Node, Playwright’s Chromium | The TV app in Chromium, worked with remote control keys (tests/e2e/tv) |
npm run check |
Node, Playwright’s Chromium | All of the above |
npm run test:tizen |
Tizen Studio | Packages and signs the .wgt and checks it; with TV_IP set, installs and launches it on that TV |
npm run check:full |
Tizen Studio | check and test:tizen |
The end-to-end tests need no bucket or keys: tests/e2e/server.js serves the
built apps and a fake S3 bucket with the small library of
tests/e2e/library.js. The TV app gets stand-ins for the Tizen APIs it uses.
Chromium can’t be as old as the oldest TVs, so the checks of test:build
stand in for running on them.
Left to be tried by hand on the devices: playback on a real TV and its remote, a Tizen 6 TV, a home screen app on iOS in the background, and scanning a QR code with a camera.
npm run check, and npm run check:full too if you changed the TV app
and have Tizen Studio.main. Describe what you changed and why, how
you tested it, and on which devices or browsers you tried it.For bigger changes, open an issue first to talk about the idea.
Every branch pushed to this repository is built, tested and published in a
folder of its own: my-test-branch is at
https://audioplayer.ctrldash.app/my-test-branch/, and feature/search at
/feature-search/ (slashes become dashes). Deleting the branch removes the
folder. main is at the root of the site.
The workflow, .github/workflows/pages.yml,
also works in a fork: in your fork’s Settings → Pages, choose Deploy from
a branch and the gh-pages branch after the first push has created it. Your
copies are then at https://<your-user>.github.io/audio-player/.
The copies share the browser’s storage with the app at the root of the same
site. They see its settings, offline playlists and metadata cache. A branch
that raises the version of the IndexedDB database in src/db.js leaves older
versions of the app unable to open it in that browser, so try such changes in
a private window or another browser profile.
The dependencies are kept at their latest versions. If an update breaks something, fix the code rather than pinning an older version.
The Unlicense: this is free and unencumbered software released into the public domain.