MikeNGarrett/play-later

Browse new album releases on Spotify and save them for later — an Rdio-style new-release browser. Slim 4 + PHP 8.4 rebuild.

★ 2Forks 0PHPGitHub ↗Compare

README

Play Later

Browse newly released albums on Spotify and save them for later — an Rdio-style new-release browser. Albums are ingested nightly from Spotify's tag:new search, enriched with track counts, artwork, and popularity, and presented by release week (new music drops Friday). Log in with Spotify to add a whole album's tracks to your "Play Later" playlist with one click.

This is the modernized rewrite of Play-Later (now archived; treat it as the behavior reference, not running code).

Stack: PHP ^8.3, Slim 4, Twig, MariaDB 11.4, Phinx migrations, Symfony Console/Cache/Lock, nginx + php-fpm + supercronic under Docker Compose.

Local development

Requirements: Docker (Compose v2) and PHP 8.3+ with Composer on the host.

cp .env.example .env                                  # fill in Spotify credentials + DB_PASS
cp compose.override.yaml.example compose.override.yaml
composer install
docker compose up -d
docker compose exec app vendor/bin/phinx migrate

The site is at http://localhost:8080. MariaDB is exposed on localhost:3306 for GUI clients (dev override only).

Spotify credentials: use the grandfathered Spotify app (rotate the secret in the developer dashboard if needed — never create a new app, new client IDs lose grandfathered API access). The redirect URI for dev is http://localhost:8080/spotify/.

Seeding: a fresh database is empty. Either restore a production dump (gunzip -c dump.sql.gz | docker compose exec -T db mariadb -u... play_later), run app:migrate:legacy against a copy of the legacy database, or just run the ingest + enrich commands below and wait — you'll have the current week's releases within a few minutes.

Console commands

All commands run through bin/console (inside the app or cron container: docker compose exec app php bin/console <command>). Production schedule lives in docker/cron/crontab, executed by supercronic in the cron container (TZ America/New_York).

Command Schedule What it does
app:ingest:spotify-tag-new 00:05 daily Discover new releases via Spotify tag:new search
app:enrich:albums --limit=500 01:05 daily Fill in track counts, artwork, genres for new rows
app:refresh:popularity 02:05 daily Refresh popularity scores for recent releases
app:enrich:albums --backfill --budget=3000 03:15 daily Drain the legacy-archive enrichment queue (remove once drained)
scripts/backup.sh 03:30 daily DB dump + prune + optional offsite copy
app:cache:prune 04:00 Sunday Prune expired filesystem cache entries
app:migrate:legacy manual, once Import the legacy database into the new schema

Each cron job pings its healthchecks.io URL (see HEALTHCHECKS_* in .env.example); leave those empty in dev to disable pings.

Deploys & secrets

Pushing to master runs .github/workflows/deploy.yml: checks (cs-fixer, phpstan, phpunit) → composer install --no-dev → rsync to /srv/play-later/releases/<sha>/ → phinx migrate → flip the /srv/play-later/current symlink → reload php-fpm → smoke-check https://play-later.com/, rolling the symlink back on failure. Branch pushes and PRs run .github/workflows/ci.yml only.

GitHub secrets: SSH_PRIVATE_KEY (deploy user), DEPLOY_HOST.

The application .env lives only on the server at /srv/play-later/shared/.env (symlinked into each release) and is never in git. To rotate a secret (Spotify client secret, DB password): update the provider/database, edit the server .env, and docker compose up -d --force-recreate app cron — no deploy needed. (restart does not re-read env_file; only recreate does.) TLS is a Cloudflare Origin CA cert mounted from docker/nginx/certs/ (gitignored); Cloudflare fronts the public edge.

The shared var/ directory on the server (/srv/play-later/shared/var) must be owned by uid 82 (www-data) — both php-fpm and the cron jobs run as that user: chown -R 82:82 /srv/play-later/shared/var.

Backups & restore

scripts/backup.sh (nightly via cron container) writes var/backups/play_later_YYYY-MM-DD.sql.gz, prunes dumps older than 7 days, copies offsite when RCLONE_REMOTE is set (e.g. a Backblaze B2 remote), and pings HEALTHCHECKS_BACKUP_URL.

scripts/restore-test.sh is the monthly restore drill: it boots a throwaway MariaDB container, imports the latest dump, and asserts the row count and newest release date look sane. Run it — a backup that has never been restored is a hope, not a backup.

Contributors

MikeNGarrett

Issues