Skip to content

About

PrintLyrics finds songs through LRCLIB and turns their lyrics into shareable, print-optimized lyric sheets.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

59 Commits

Folders and files

Repository files navigation

PrintLyrics

PrintLyrics finds songs through LRCLIB and turns their lyrics into shareable, print-optimized lyric sheets. Manual lyric entry is also supported. It is a Rails 8.1 application built with Hotwire, Tailwind CSS, SQLite, and a small amount of Stimulus for preview controls.

Requirements

  • Ruby 4.0.5
  • SQLite 3

Setup

bin/setup
bin/dev

The application runs at http://localhost:3000.

Tests

bin/rails test
bin/rubocop
bin/brakeman --no-pager
bin/rails assets:precompile

Song Search

Song search uses the public LRCLIB API. Selecting a result fills the editable form without creating a database record. A record is persisted only after the user generates the print page.

Songbooks

A generated page can be extended into a songbook: an ordered, token-addressed set of lyric sheets that prints as one job. A songbook keeps the generated-page contract — unlisted link, no account, and excluded from indexing.

Retention

Generated pages and songbooks expire 180 days after their last visit, and visits renew the retention window. Visiting a songbook renews it and every song in it. Nothing purges expired rows on its own: cleanup is the explicit task below, which the kamal purge alias runs against production.

bin/rails lyrics:purge_expired

User feedback

Visitors can describe what they were printing and what got in the way, from the prompt under a search that found nothing, from the line under a sheet they just generated, or from the /feedback page. Submissions are stored in the feedbacks table and read with:

bin/rails feedback:demand   # the songs asked for, most requested first
bin/rails feedback:list     # newest first, 50 by default
LIMIT=200 bin/rails feedback:list

feedback:demand is where a review starts: the same title typed twice counts as one request, and each row carries its surface, whether it came with a note, and when it was last asked for. Every row also carries the answer the visitor chose (reason, from Feedback::REASONS) and how many sheets the visit had already made (visit_sheet_count) — a request with no sheet behind it is a visitor who left empty-handed. The kamal feedback and kamal feedback-demand aliases run those two tasks against the deployed container, which is where production submissions are read.

A stored submission is also reported to the site's own analytics service as Feedback Submitted, carrying the song query, the note, the surface it came from, and the answer, so the demand can be read beside the counts around it. That report is Umami's alone — never Plausible's — and the reply address is not part of it. The measurement contract owns the rule and the event's properties.

The form is gated by Cloudflare Turnstile. The site key and the accepted hostnames are public and live in config/deploy.yml; the widget secret is a credential, with the rest of this project's secrets:

TURNSTILE_SITE_KEY=...      # config/deploy.yml, public
TURNSTILE_HOSTNAMES=...     # config/deploy.yml, public; never localhost in production
bin/rails credentials:edit  # turnstile.secret_key, unlocked by RAILS_MASTER_KEY

A submission is stored only when Cloudflare confirms the token for the feedback action on one of TURNSTILE_HOSTNAMES. A declined token, a token minted for another surface or host, an unreachable siteverify, or a host missing any of these values is refused: an unjudged challenge is not a pass, so the form stays closed until the deployment is configured.

Cloudflare's test keys cannot satisfy those checks: their reply carries no action and reports hostname: "example.com", so a test-key submission is refused. To exercise the happy path locally, add localhost and 127.0.0.1 to the widget's domains in the Cloudflare dashboard and run with the real sitekey and TURNSTILE_HOSTNAMES=localhost,127.0.0.1.

Deployment

The Dockerfile builds assets without Node.js. Kamal deploys one web container to the existing Hetzner host, with SQLite stored on a persistent Docker volume. Deployments require:

RAILS_MASTER_KEY=...
KAMAL_REGISTRY_PASSWORD=...

Analytics runs beside that container as two Kamal accessories — Umami and its PostgreSQL database. A deploy neither starts, stops, nor updates them; booting them for the first time needs two more secrets:

UMAMI_DB_PASSWORD=...   # openssl rand -hex 24
UMAMI_APP_SECRET=...    # openssl rand -hex 32

An ordinary deploy does not need those two, and the runbook linked under Operations owns the setup.

The printlyrics-prod GitHub environment also requires KAMAL_SSH_KEY. CI runs for pull requests and pushes to main. Release Please maintains a release pull request and updates version.txt; merging that pull request publishes a release and dispatches the production deployment. The application shows that version in its footer.

Operations

Use the organic search operations runbook to configure Search Console, boot the self-hosted Umami analytics service and its MCP endpoint, run the two-week parallel measurement beside Plausible, record launch baselines, and run the 30- and 90-day reviews. The measurement contract holds the event vocabulary, how a surface reports an event, and what must never reach analytics.

About

PrintLyrics finds songs through LRCLIB and turns their lyrics into shareable, print-optimized lyric sheets.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages