#!/usr/bin/env python3 """ Cozypaw Hospital — freesound.org batch audio downloader Downloads all placeholder audio files (0-byte .ogg) and replaces them with 128 kbps HQ OGG previews from freesound.org. Prerequisites: pip install requests API key setup (free, ~2 min): 1. freesound.org → login → click your username → "API credentials" 2. Click "Apply for an API key" 3. App name: "Cozypaw Download Script", Description: "Personal game project" 4. Copy the "Api key" value (the long string) into API_KEY below. Usage: python docs/download_audio.py Quality note: This script downloads the "preview-hq-ogg" (128 kbps OGG Vorbis). For a mobile children's game this is indistinguishable from lossless. Original-quality downloads require full OAuth2 — not worth the hassle. """ import os import sys import requests from pathlib import Path # ── Fill in your API key here ────────────────────────────────────────────────── API_KEY = "XLXzH6xQJbt5HQjLx7kQwfDSB9MTFawMTsAFhRFG" # e.g. "aB3dEfGhIjKlMnOpQrStUvWx" # ────────────────────────────────────────────────────────────────────────────── REPO_ROOT = Path(__file__).parent.parent BASE_URL = "https://freesound.org/apiv2" # Files with confirmed freesound IDs — downloaded by ID, no searching needed. # CC-BY entries are marked; they need an attribution line in docs/credits-audio.md KNOWN_IDS: dict[str, tuple[int, str]] = { # path id license # ── Sprint 19 — music ────────────────────────────────────────────────────── "assets/audio/music/floor_0.ogg": (725019, "CC0"), "assets/audio/music/floor_1.ogg": (387588, "CC0"), "assets/audio/music/floor_2.ogg": (684511, "CC0"), "assets/audio/music/floor_3.ogg": (723913, "CC0"), # ── Sprint 19 — SFX ─────────────────────────────────────────────────────── "assets/audio/sfx/chest_tap.ogg": (679772, "CC0"), "assets/audio/sfx/item_spawn.ogg": (683096, "CC0"), "assets/audio/sfx/item_drag_start.ogg": (411177, "CC0"), "assets/audio/sfx/item_drop_hand.ogg": (448086, "CC0"), "assets/audio/sfx/item_drop_outfit.ogg": (161415, "CC-BY 3.0"), # needs attribution "assets/audio/sfx/item_return_chest.ogg": (740266, "CC0"), "assets/audio/sfx/item_drop_floor.ogg": (449955, "CC0"), # ── Sprint 21 — interactive object SFX ──────────────────────────────────── "assets/audio/sfx/xray_scan.ogg": (614030, "CC0"), # Machine beep.wav — INHIVE.NEWERA "assets/audio/sfx/tea_pour.ogg": (116396, "CC0"), # liquid-pour.mp3 — shakala1 "assets/audio/sfx/cradle_rock.ogg": (216877, "CC0"), # Slow gentle squeaky wooden door — CastIronCarousel "assets/audio/sfx/gift_open.ogg": (676625, "CC0"), # Rip 8 - Long — NearTheAtmoshphere "assets/audio/sfx/ambulance_siren.ogg": (536773, "CC0"), # Siren.ogg — egomassive "assets/audio/sfx/delivery_cheer.ogg": (717771, "CC0"), # victory chime — 1bob "assets/audio/sfx/object_tap.ogg": (817506, "CC0"), # Soft Interface 01 Tap — tonymadethatt # ── Sprint 22 — character & ambient SFX ─────────────────────────────────── "assets/audio/sfx/ultrasound_heartbeat.ogg":(463202, "CC0"), # one_beep.wav — Kenneth_Cooney "assets/audio/sfx/character_pickup.ogg": (789840, "CC0"), # Whoosh Short — FartCTO "assets/audio/sfx/character_place.ogg": (653910, "CC0"), # soft-hit.wav — Krokulator "assets/audio/sfx/character_tap.ogg": (776443, "CC0"), # pop out, bubble — chaferwitt } # All files now have confirmed IDs in KNOWN_IDS above. SEARCH_QUERIES: dict[str, tuple[str, float]] = {} # ── Helpers ──────────────────────────────────────────────────────────────────── def _is_placeholder(path: Path) -> bool: """Returns True if the file is missing or 0-byte (i.e. still a placeholder).""" return not path.exists() or path.stat().st_size == 0 def _get_sound_info(sound_id: int) -> dict | None: url = f"{BASE_URL}/sounds/{sound_id}/" r = requests.get(url, params={ "fields": "id,name,previews,license,username,duration", "token": API_KEY, }, timeout=15) if r.status_code != 200: print(f" ✗ API error {r.status_code} for ID {sound_id}") return None return r.json() def _search_sound(query: str, max_duration: float) -> dict | None: r = requests.get(f"{BASE_URL}/search/text/", params={ "query": query, "filter": f'license:"Creative Commons 0" duration:[0 TO {max_duration}]', "fields": "id,name,previews,license,username,duration", "sort": "score", "page_size": 5, "token": API_KEY, }, timeout=15) if r.status_code != 200: print(f" ✗ Search API error {r.status_code} for query '{query}'") return None results = r.json().get("results", []) if not results: print(f" ✗ No results for '{query}' under {max_duration}s") return None return results[0] def _download_preview(info: dict, dest: Path) -> bool: ogg_url = info.get("previews", {}).get("preview-hq-ogg") if not ogg_url: print(f" ✗ No HQ OGG preview URL in response") return False r = requests.get(ogg_url, timeout=30) if r.status_code != 200: print(f" ✗ CDN download failed ({r.status_code}): {ogg_url}") return False dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes(r.content) return True def _record_attribution(path: str, info: dict, license_str: str) -> None: credits_file = REPO_ROOT / "docs" / "credits-audio.md" line = ( f"| `{path}` | {info['name']} | {info['username']} " f"| {license_str} | https://freesound.org/s/{info['id']}/ |\n" ) if not credits_file.exists(): credits_file.write_text( "# Audio Credits\n\nCC-BY files require attribution.\n\n" "| File | Title | Author | License | URL |\n" "|---|---|---|---|---|\n" ) content = credits_file.read_text() if f"/{info['id']}/" not in content: with credits_file.open("a") as f: f.write(line) print(f" → Attribution recorded in docs/credits-audio.md") # ── Main ─────────────────────────────────────────────────────────────────────── def main() -> None: if not API_KEY: print("ERROR: API_KEY is empty.") print("Get your free key at: https://freesound.org/apiv2/apply/") print("Then fill in API_KEY at the top of this script.") sys.exit(1) skipped = [] succeeded = [] failed = [] # ── Known IDs ────────────────────────────────────────────────────────────── print(f"\n{'─'*60}") print("Downloading files with known freesound IDs …") print(f"{'─'*60}") for rel_path, (sound_id, license_str) in KNOWN_IDS.items(): dest = REPO_ROOT / rel_path if not _is_placeholder(dest): print(f" ✓ skip {rel_path} (already downloaded)") skipped.append(rel_path) continue print(f" ↓ {rel_path} (ID {sound_id})") info = _get_sound_info(sound_id) if info is None: failed.append(rel_path) continue if _download_preview(info, dest): size_kb = dest.stat().st_size // 1024 print(f" ✓ {info['name']} by {info['username']}" f" [{size_kb} KB, {info['duration']:.1f}s, {license_str}]") succeeded.append(rel_path) if "CC-BY" in license_str: _record_attribution(rel_path, info, license_str) else: failed.append(rel_path) # ── Search queries ───────────────────────────────────────────────────────── print(f"\n{'─'*60}") print("Searching and downloading remaining SFX …") print(f"{'─'*60}") for rel_path, (query, max_dur) in SEARCH_QUERIES.items(): dest = REPO_ROOT / rel_path if not _is_placeholder(dest): print(f" ✓ skip {rel_path} (already downloaded)") skipped.append(rel_path) continue print(f" ↓ {rel_path} (search: '{query}', max {max_dur}s)") info = _search_sound(query, max_dur) if info is None: failed.append(rel_path) continue if _download_preview(info, dest): size_kb = dest.stat().st_size // 1024 print(f" ✓ {info['name']} by {info['username']}" f" [{size_kb} KB, {info['duration']:.1f}s, {info['license']}]") succeeded.append(rel_path) else: failed.append(rel_path) # ── Summary ──────────────────────────────────────────────────────────────── print(f"\n{'─'*60}") print(f"Done. ✓ {len(succeeded)} downloaded" f" · ↷ {len(skipped)} skipped" f" · ✗ {len(failed)} failed") if failed: print("\nFailed files (fix manually):") for f in failed: print(f" {f}") print(f"{'─'*60}\n") if __name__ == "__main__": main()