A single-page app for searching and viewing a large personal sheet music library stored as PDFs on Google Drive.
A FastAPI server proxies Google Drive PDF requests to avoid CORS issues, serves the static SPA, and exposes the catalog as an API endpoint. The app itself is a vanilla JS SPA using pdf.js from CDN, with shared logic extracted into app/lib.js.
| File | Purpose |
|---|---|
catalog.json |
The song index — hand-editable, append-friendly |
server.py |
FastAPI server: serves the SPA, proxies Drive PDFs |
app/index.html |
SPA with search + pdf.js viewer + fullscreen mode |
app/lib.js |
Shared JS module: search, rendering, fullscreen logic |
ingestion/ingest.py |
CLI tool for parsing indexes into the catalog |
ingestion/parsers/pdf_parser.py |
Positional column parser using pdfplumber |
ingestion/parsers/csv_parser.py |
Flexible CSV import with column alias mapping |
make install # Create venv and install server + test dependencies
make install-ingestion # Also install ingestion dependencies
npm install # Install JS test dependencies (jsdom)The server supports two modes for fetching PDFs from Google Drive:
This lets the server access files shared with the service account — no public links needed.
- Go to the Google Cloud Console
- Create a project (or reuse an existing one)
- Enable the Google Drive API (APIs & Services → Enable APIs)
- Go to IAM & Admin → Service Accounts and create a service account
- Create a key for the service account (JSON format) and save it as
credentials.jsonin the project root (this file is gitignored) - Share your Google Drive folder(s) with the service account's email address (it looks like
name@project-id.iam.gserviceaccount.com)
The server detects credentials.json at startup and uses the Drive API with Bearer auth automatically.
If no credentials.json is present, the server falls back to fetching PDFs via public download URLs. Each Drive file must be shared as "anyone with the link can view." Some files may also require a resourceKey, which is passed via the catalog and forwarded as an X-Goog-Drive-Resource-Keys header.
The content-serving endpoints (/api/pdf/{id} and /drive/file) require a shared
access token, since the server typically binds to all interfaces on the LAN. Set it
one of two ways (both gitignored):
export SHEET_MUSIC_TOKEN="some-long-random-string" # env var, or:
echo "some-long-random-string" > token.txt # file in the project rootGenerate one with python3 -c "import secrets; print(secrets.token_urlsafe(24))".
Requests authenticate with an Authorization: Bearer <token> header or a
?token=<token> query parameter. The web app prompts for the token the first time
you open a PDF and remembers it in the browser's localStorage. If no token is
configured server-side, the protected endpoints refuse all requests (503) —
they fail closed. The catalog endpoint (song titles only) and the static app
remain open.
Instead of splitting lines on whitespace (which breaks for multi-word titles like "All Of Me"), it uses pdfplumber's character position data:
- Groups characters by y-coordinate into lines
- Groups characters within each line into tokens by x-gaps (>4pt gap = new token)
- Identifies the page number as the last all-digit token
- Splits title from book name at the largest x-gap between remaining tokens — this is the natural column gutter between left-aligned titles and center-aligned book names
# Dry-run first to check parsing quality
python ingestion/ingest.py pdf --file your-index.pdf --source "master-index" --dry-run
# Ingest for real
python ingestion/ingest.py pdf --file your-index.pdf --source "master-index"
# Import from CSV
python ingestion/ingest.py csv --file extra-songs.csv --source "manual-csv"
# Re-ingest a source (removes old entries first)
python ingestion/ingest.py pdf --file your-index.pdf --source "master-index" --replace
# Remove all entries from a source
python ingestion/ingest.py remove --source "master-index"
# List ingestion sources and counts
python ingestion/ingest.py sources
# Add or update a volume using a Google Drive share link
python ingestion/ingest.py add-volume --id Realbk1 --name "The Real Book Vol 1" \
--url "https://drive.google.com/file/d/1aBcDeFg.../view?resourcekey=0-abc123" \
--offset 5The --url flag accepts a full Google Drive share link and extracts both the file ID and resource key automatically.
make serve # Starts uvicorn on port 7001Then open http://localhost:7001.
make test # Run all tests (Python + JS)
make test-py # Python tests only (pytest)
make test-js # JS tests only (node --test)catalog.json has two top-level keys:
volumes— keyed by volume ID slug, each with:name,driveFileId,resourceKey,pageOffset,notessongs— flat array, each with:title,composer,arranger,volumeId,nominalPage,source,addedAt
The page offset formula: actual_pdf_page = nominalPage + pageOffset. If a book's "page 1" is PDF page 6, set pageOffset: 5.
The resourceKey field is only needed when accessing files via public links (Option 2 above). Files shared with the service account do not require it.
After ingesting your index, you need to:
- Add each volume with
ingest.py add-volume --id ... --name ... --url ... - Set the correct
pageOffsetfor each volume - Either share Drive folders with the service account, or ensure files are publicly shared