calesthio/OpenMontage · error · RuntimeError

No preview URL available for sound {sound.get('id')} ({sound

Error message

No preview URL available for sound {sound.get('id')} ({sound.get('name')})

What it means

Raised by freesound_music._download when a search result's previews object contains neither preview-hq-mp3 nor preview-lq-mp3. Freesound download URLs only exist for sounds whose license/premium state permits previews; the tool downloads the HQ MP3 preview rather than the original file, so a result without preview URLs is undownloadable.

Source

Thrown at tools/audio/freesound_music.py:210

        request = urllib.request.Request(
            url,
            headers={"User-Agent": "OpenMontage/0.1 (music acquisition tool)"},
        )

        with urllib.request.urlopen(request, timeout=30) as response:
            data = json.loads(response.read().decode("utf-8"))

        results = data.get("results", [])
        return results

    def _download(self, sound: dict, inputs: dict[str, Any], api_key: str) -> Path:
        """Download the HQ MP3 preview of a Freesound sound."""
        previews = sound.get("previews", {})
        # Prefer the HQ MP3 preview; fall back to LQ MP3
        audio_url = previews.get("preview-hq-mp3") or previews.get("preview-lq-mp3")

        if not audio_url:
            raise RuntimeError(
                f"No preview URL available for sound {sound.get('id')} ({sound.get('name')})"
            )

        # Build output path
        sound_name = sound.get("name", f"freesound_{sound.get('id', 'unknown')}")
        safe_name = "".join(c if c.isalnum() or c in "._- " else "_" for c in sound_name)
        default_filename = f"freesound_{sound.get('id')}_{safe_name}.mp3"
        output_path = Path(inputs.get("output_path", default_filename))
        output_path.parent.mkdir(parents=True, exist_ok=True)

        request = urllib.request.Request(
            audio_url,
            headers={"User-Agent": "OpenMontage/0.1 (music acquisition tool)"},
        )

        with urllib.request.urlopen(request, timeout=60) as response:
            output_path.write_bytes(response.read())

View on GitHub (pinned to 95e1c3d0ab)

Solutions

  1. Filter search results client-side: prefer items where result['previews'].get('preview-hq-mp3') is truthy
  2. Fall back to the next search result instead of failing the whole job
  3. Request additional fields in the search query so previews are populated

Example fix

// before
sound = results[0]  # may lack previews

// after
sound = next(r for r in results if r.get("previews", {}).get("preview-hq-mp3") or r.get("previews", {}).get("preview-lq-mp3"))
Defensive patterns

Strategy: validation

Validate before calling

def downloadable(sound: dict) -> bool:
    p = sound.get("previews") or {}
    return bool(p.get("preview-hq-mp3") or p.get("preview-lq-mp3"))

results = [r for r in results if downloadable(r)]

Type guard

def has_preview(sound: dict) -> bool:
    previews = sound.get("previews")
    return isinstance(previews, dict) and bool(
        previews.get("preview-hq-mp3") or previews.get("preview-lq-mp3")
    )

Prevention

When it happens

Trigger: Search returns a premium-only or delisted sound whose previews dict is empty; result objects from a stale/paginated search response missing the previews field.

Common situations: Picking the first search hit without checking its previews; sounds uploaded with original-only licensing; API response shape differences between search endpoints.

Related errors


AI-assisted analysis of calesthio/OpenMontage@95e1c3d0ab (2026-08-15). Data as JSON: /api/errors/cb8691b4b2feda03. Report an issue: GitHub.