feat: bundle Free-Exercise-DB + name matcher (step 1)

Adds the static exercise reference data (~870 entries, public
domain, source: github.com/yuhonas/free-exercise-db) plus a
conservative name matcher. New endpoint:

  GET /api/exercises/lookup?name=<name>
    → {"match": {"name", "primary_muscles", "secondary_muscles",
                 "equipment", "category", "level", ...}}
    → {"match": null}  when nothing plausibly matches.

Matcher tiers (priority order):
  1. exact (case-insensitive)
  2. compressed exact ("Pull-ups" → "Pullups")
  3. compressed substring, with a guard: single-token generics
     like "Bench"/"Squat" return null instead of misleading the
     user — the planned alias table will handle these properly.
  4. token-overlap with ≥50% coverage of the user's tokens.

UI integration ("Trains: chest · shoulders") comes in step 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Danny 2026-05-24 11:11:34 +02:00
parent 9e50686983
commit ebd0016a62
4 changed files with 22824 additions and 0 deletions

View file

@ -18,6 +18,7 @@ from urllib.parse import parse_qs
from aiohttp import web
from db import init_db, save_workout, get_workouts, get_workout_count, get_stats_sql, delete_workout, update_workout, export_workouts, get_user_workout_number, get_all_exercise_names, log_event, get_settings, update_settings, get_last_exercise
import exercise_db
from parser import parse_workout, format_workout
logging.basicConfig(
@ -293,6 +294,15 @@ async def api_get_last_exercise(request: web.Request):
return web.json_response({"last": last})
@require_auth
async def api_lookup_exercise(request: web.Request):
"""Look up an exercise in the static Free-Exercise-DB reference data."""
name = request.query.get("name", "").strip()
if not name:
return web.json_response({"error": "Missing name"}, status=400)
return web.json_response({"match": exercise_db.lookup(name)})
@require_auth
async def api_get_stats(request: web.Request):
"""Return summary stats for the user."""
@ -377,6 +387,7 @@ def create_app() -> web.Application:
app.router.add_delete("/api/workouts/{workout_id}", api_delete_workout)
app.router.add_get("/api/exercises", api_get_exercise_names)
app.router.add_get("/api/exercises/last", api_get_last_exercise)
app.router.add_get("/api/exercises/lookup", api_lookup_exercise)
app.router.add_get("/api/stats", api_get_stats)
app.router.add_get("/api/export/json", api_export_json)
app.router.add_get("/api/export/csv", api_export_csv)