c465e518e5
New endpoint: GET /api/activity/{id}/download/{bas|original|gpx}
- bas: streams the BAS detail JSON as an attachment
- original: streams the original FIT or GPX file from originals/
- gpx: generates a GPX from the timeseries (always available when GPS exists)
download_disabled flag stored in sidecar (edits/{id}.md), propagated to
the merged BAS detail JSON. When set, only the owner can download.
Backend: ops.py writes flag to sidecar; merge.py propagates it to detail
JSON; download.py implements the endpoint; server.py registers the router.
Frontend: EditDrawer gets a "No download" toggle button; ActivityDetail
shows a Download section (hidden when disabled and viewer is not the owner).
97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
"""Pure write operations used by both the single-user edit server and the
|
|
multi-user serve server.
|
|
|
|
No FastAPI, no globals — all context is passed as explicit arguments.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
# ── Shared constants (imported by edit/server.py and serve/server.py) ─────────
|
|
|
|
from bincio.extract.sport import SUB_SPORTS as _SUB_SPORTS
|
|
|
|
SPORTS = ["cycling", "running", "hiking", "walking", "swimming", "skiing", "other"]
|
|
_VALID_SUB_SPORTS = {v for vs in _SUB_SPORTS.values() for v in vs}
|
|
STAT_PANELS = ["elevation", "speed", "heart_rate", "cadence", "power"]
|
|
VALID_ACTIVITY_ID = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9\-]{0,250}$')
|
|
|
|
|
|
def apply_sidecar_edit(activity_id: str, payload: dict[str, Any], data_dir: Path) -> None:
|
|
"""Write a sidecar .md file and trigger merge_all().
|
|
|
|
Args:
|
|
activity_id: Validated activity ID (caller must validate).
|
|
payload: Dict with optional keys: title, sport, gear, description,
|
|
highlight, private, hide_stats.
|
|
data_dir: Per-user data directory (contains activities/, edits/).
|
|
"""
|
|
edits_dir = data_dir / "edits"
|
|
edits_dir.mkdir(exist_ok=True)
|
|
sidecar_path = edits_dir / f"{activity_id}.md"
|
|
|
|
lines: list[str] = []
|
|
if payload.get("title"):
|
|
lines.append(f"title: {json.dumps(payload['title'])}")
|
|
if payload.get("sport") and payload["sport"] in SPORTS and payload["sport"] != "other":
|
|
lines.append(f"sport: {payload['sport']}")
|
|
if payload.get("sub_sport") and payload["sub_sport"] in _VALID_SUB_SPORTS:
|
|
lines.append(f"sub_sport: {payload['sub_sport']}")
|
|
if payload.get("gear"):
|
|
lines.append(f"gear: {json.dumps(payload['gear'])}")
|
|
if payload.get("highlight"):
|
|
lines.append("highlight: true")
|
|
if payload.get("private"):
|
|
lines.append("private: true")
|
|
hide = [s for s in (payload.get("hide_stats") or []) if s in STAT_PANELS]
|
|
if hide:
|
|
lines.append(f"hide_stats: [{', '.join(hide)}]")
|
|
if payload.get("download_disabled"):
|
|
lines.append("download_disabled: true")
|
|
|
|
description = (payload.get("description") or "").strip()
|
|
|
|
content = "---\n" + "\n".join(lines) + "\n---\n"
|
|
if description:
|
|
content += "\n" + description + "\n"
|
|
|
|
sidecar_path.write_text(content, encoding="utf-8")
|
|
|
|
from bincio.render.merge import merge_one
|
|
merge_one(data_dir, activity_id)
|
|
|
|
|
|
def run_strava_sync(
|
|
data_dir: Path,
|
|
client_id: str,
|
|
client_secret: str,
|
|
originals_dir: Optional[Path] = None,
|
|
) -> dict[str, Any]:
|
|
"""Fetch new Strava activities and write them into data_dir.
|
|
|
|
Args:
|
|
data_dir: Per-user data directory.
|
|
client_id: Strava OAuth client ID.
|
|
client_secret: Strava OAuth client secret.
|
|
originals_dir: If set, raw Strava API data (meta + streams) is saved here
|
|
as JSON files for potential future reprocessing.
|
|
|
|
Returns:
|
|
Dict with keys: ok, imported, skipped, error_count, errors.
|
|
|
|
Raises:
|
|
RuntimeError: If Strava credentials are missing or API calls fail.
|
|
"""
|
|
from bincio.extract.ingest import strava_sync as _strava_sync
|
|
from bincio.render.merge import merge_all
|
|
|
|
result = _strava_sync(data_dir, client_id, client_secret, originals_dir=originals_dir)
|
|
if result["imported"]:
|
|
merge_all(data_dir)
|
|
|
|
return result
|