Self-hosted music scrobble server with REST API.
New to scrob? Check out QUICKSTART.md for a step-by-step guide.
Developing? See CLAUDE.md for architecture notes and best practices.
- REST API for scrobble submission and statistics
- Token-based authentication
- PostgreSQL database
- Docker support
- Compatible with last-fm-rs client library
# Build and start
docker-compose up -d
# View logs
docker-compose logs -f
# Stop
docker-compose downThe server will be available at http://localhost:3000.
# Build image
docker build -t scrob .
# Run container
docker run -d \
-p 3000:3000 \
-v $(pwd)/data:/app/data \
--name scrob \
scrob- Rust 1.82+
- PostgreSQL 12+
- sqlx-cli:
cargo install sqlx-cli --no-default-features --features postgres
# Create PostgreSQL database
createdb scrob
# Or with custom user:
# createuser -P scrob
# createdb -O scrob scrob
# Install dependencies
cargo build
# Run migrations
export DATABASE_URL="postgres://localhost/scrob"
cargo sqlx migrate run
# Start server
cargo runSee POSTGRES_SETUP.md for detailed PostgreSQL setup instructions.
DATABASE_URL- Database connection string (default:postgres://localhost/scrob)HOST- Bind address (default:127.0.0.1)PORT- Port number (default:3000)RUST_LOG- Logging level (default:scrob=info)
Example DATABASE_URL formats:
# Local development
postgres://localhost/scrob
# With authentication
postgres://scrob:password@localhost/scrob
# Remote with SSL
postgres://user:pass@host:5432/scrob?sslmode=requireUse the interactive bootstrap script to create a user, login, and get tokens:
./scripts/bootstrap.shThis handles everything for you and outputs the tokens you need.
python3 -c "
import psycopg2
import bcrypt
import time
username = 'alice'
password = 'mypassword'
is_admin = True
hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
timestamp = int(time.time())
conn = psycopg2.connect('dbname=scrob user=scrob')
cur = conn.cursor()
cur.execute(
'INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (%s, %s, %s, %s)',
(username, hash, is_admin, timestamp)
)
conn.commit()
print(f'User {username} created')
"# Requires Python 3 with bcrypt installed
./scripts/create_user.sh alice mypassword true# Login and get token
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "mypassword"}'
# Response: {"token": "...", "username": "alice", "is_admin": false}Use the returned token in the Authorization header for all protected endpoints:
Authorization: Bearer <token>
curl -X POST http://localhost:3000/scrob \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '[{
"artist": "Kendrick Lamar",
"track": "Wesley'\''s Theory",
"album": "To Pimp a Butterfly",
"duration": 287,
"timestamp": 1701619200
}]'curl http://localhost:3000/recent?limit=20 \
-H "Authorization: Bearer <token>"curl http://localhost:3000/top/artists?limit=10 \
-H "Authorization: Bearer <token>"curl http://localhost:3000/top/tracks?limit=10 \
-H "Authorization: Bearer <token>"curl -X POST http://localhost:3000/now \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"artist": "Pink Floyd",
"track": "Time",
"album": "The Dark Side of the Moon"
}'This server is designed to work with the last-fm-rs client library in token mode:
use last_fm_rs::Client;
let client = Client::with_token(
"http://localhost:3000",
"your-api-token"
)?;
// Use as normal
client.update_now_playing(&now_playing).await?;
client.scrobble(&scrobbles).await?;id- Primary keyusername- Unique usernamepassword_hash- Bcrypt password hashis_admin- Admin flagcreated_at- Unix timestamp
id- Primary keyuser_id- Foreign key to userstoken- Unique token stringlabel- Optional label (e.g., "desktop", "phone")created_at- Unix timestamplast_used_at- Unix timestamp (updated on use)revoked- Revocation flag
id- Primary keyuser_id- Foreign key to usersartist- Artist nametrack- Track namealbum- Album name (optional)duration- Duration in seconds (optional)timestamp- When the track was played (Unix timestamp)created_at- When the scrobble was recorded (Unix timestamp)
MIT OR Apache-2.0