Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions backend/app/tests/scripts/test_sync_place_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import importlib.util
import sys
from pathlib import Path

ROOT_DIR = Path(__file__).resolve().parents[4]
SCRIPT_PATH = ROOT_DIR / "scripts" / "sync_place_audio.py"


def load_audio_sync_module():
spec = importlib.util.spec_from_file_location("sync_place_audio", SCRIPT_PATH)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module


def place_payload(slug="rynek-wroclaw", *, cover_audio=None, preview_items=None):
return {
"city_id": "wroclaw",
"slug": slug,
"title": slug.replace("-", " ").title(),
"cover_photo": {
"id": f"photo-{slug}",
"public_path": f"/media/photos/{slug}.jpg",
"audio": cover_audio,
},
"preview_items": preview_items or [],
}


def test_build_audio_jobs_prefers_cover_photo_and_matches_slug_file():
module = load_audio_sync_module()

jobs = module.build_audio_jobs(
[place_payload("rynek-wroclaw")],
{"rynek-wroclaw": Path("research-exports/place-audio/wroclaw/rynek-wroclaw.mp3")},
)

assert len(jobs) == 1
assert jobs[0].status == "ready"
assert jobs[0].target_kind == "photo"
assert jobs[0].target_id == "photo-rynek-wroclaw"
assert jobs[0].upload_path == "/api/admin/photos/photo-rynek-wroclaw/audio"


def test_build_audio_jobs_skips_places_that_already_have_audio_unless_forced():
module = load_audio_sync_module()
place = place_payload("rynek-wroclaw", cover_audio={"public_path": "/media/photos/audio.mp3"})

skipped_jobs = module.build_audio_jobs(
[place],
{"rynek-wroclaw": Path("research-exports/place-audio/wroclaw/rynek-wroclaw.mp3")},
)
forced_jobs = module.build_audio_jobs(
[place],
{"rynek-wroclaw": Path("research-exports/place-audio/wroclaw/rynek-wroclaw.mp3")},
force=True,
)

assert skipped_jobs[0].status == "already_has_audio"
assert forced_jobs[0].status == "ready"
assert forced_jobs[0].target_id == "photo-rynek-wroclaw"


def test_build_audio_jobs_falls_back_to_memory_when_place_has_no_photo():
module = load_audio_sync_module()
place = {
"city_id": "wroclaw",
"slug": "memory-place",
"title": "Memory Place",
"cover_photo": None,
"preview_items": [
{
"kind": "memory",
"id": "memory-1",
"public_path": "/media/memories/memory-place.jpg",
"audio": None,
}
],
}

jobs = module.build_audio_jobs(
[place],
{"memory-place": Path("research-exports/place-audio/wroclaw/memory-place.flac")},
)

assert jobs[0].status == "ready"
assert jobs[0].target_kind == "memory"
assert jobs[0].upload_path == "/api/admin/memories/memory-1/audio"


def test_build_audio_jobs_reports_missing_duplicate_and_targetless_cases():
module = load_audio_sync_module()

missing = place_payload("missing-audio")
targetless = {
"city_id": "wroclaw",
"slug": "targetless",
"title": "Targetless",
"cover_photo": None,
"preview_items": [],
}
duplicate = place_payload("duplicate")

jobs = module.build_audio_jobs(
[missing, targetless, duplicate],
{},
{"duplicate": [Path("duplicate.mp3"), Path("duplicate.flac")]},
)

assert {job.slug: job.status for job in jobs} == {
"missing-audio": "missing_audio_file",
"targetless": "no_media_target",
"duplicate": "duplicate_audio_file",
}
52 changes: 52 additions & 0 deletions docs/place-audio-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Place Audio Workflow

PhotoMap nie ma osobnego bytu `place_audio`. Dźwięk miejsca jest publicznie odtwarzany jako załącznik audio do reprezentatywnego medium widocznego na mapie: najpierw cover photo, potem pierwsze zdjęcie z galerii, a na końcu pierwsza pamiątka.

Do masowego dopinania nagrań użyj `scripts/sync_place_audio.py`. Skrypt pobiera aktualne miejsca z publicznego endpointu mapy, sprawdza, które mają już audio, dopasowuje lokalne pliki po slugach miejsc i w trybie `--apply` wgrywa je przez admin API.

## Przygotowanie plików

Trzymaj robocze nagrania poza commitem, najlepiej w ignorowanym katalogu `research-exports/`:

```bash
mkdir -p research-exports/place-audio/wroclaw
cp ~/Nagrania/rynek.mp3 research-exports/place-audio/wroclaw/rynek-wroclaw.mp3
cp ~/Nagrania/ostrow.mp3 research-exports/place-audio/wroclaw/ostrow-tumski.mp3
```

Nazwa pliku musi być slugiem miejsca z mapy. Obsługiwane rozszerzenia: `.mp3`, `.m4a`, `.flac`.

## Plan bez wgrywania

```bash
python scripts/sync_place_audio.py \
--base-url https://photomap.pl \
--city wroclaw \
--audio-dir research-exports/place-audio/wroclaw \
--output research-exports/place-audio/wroclaw-plan.json
```

Najważniejsze statusy:

- `already_has_audio` — miejsce ma już audio w coverze albo podglądzie mapy.
- `missing_audio_file` — miejsce jest na mapie, ale brakuje pliku `{slug}.mp3/.m4a/.flac`.
- `ready` — plik jest znaleziony i można go wgrać.
- `no_media_target` — miejsce nie ma covera, zdjęcia ani pamiątki, do których można przypiąć audio.
- `duplicate_audio_file` — w katalogu jest więcej niż jeden plik dla tego samego sluga.

## Wgrywanie do PhotoMap

```bash
PHOTOMAP_ADMIN_TOKEN="..." python scripts/sync_place_audio.py \
--apply \
--base-url https://photomap.pl \
--city wroclaw \
--audio-dir research-exports/place-audio/wroclaw \
--output research-exports/place-audio/wroclaw-upload.json
```

Skrypt wymaga tokena w `PHOTOMAP_ADMIN_TOKEN`, bo używa adminowych endpointów audio. Domyślnie pomija miejsca, które już mają publiczne audio. `--force` podmieni audio także tam, gdzie jest już nagranie.

## Po wgraniu

Sprawdź publiczną mapę i otwórz medium miejsca. Marker albo kafelek galerii z audio powinien mieć oznaczenie audio, a modal zdjęcia/pamiątki powinien pokazać kontrolkę odtwarzania. Przełącznik `Audio` na mapie uruchamia delikatny ambient autoplay tylko po decyzji użytkownika.
Loading
Loading