tonhowtf/omniget · error · anyhow::Error

Failed to parse m3u8: neither master nor media playlist

Error message

Failed to parse m3u8: neither master nor media playlist

What it means

After fetching the m3u8 text, download_with_quality tries to interpret it as a master playlist and, failing that, as a media playlist. If neither parse succeeds, it concludes the response is not a usable HLS playlist and bails with this error. This is the library's guard against downloading from URLs that return non-HLS content.

Solutions

  1. Confirm the URL is the real .m3u8 playlist (inspect browser devtools Network tab for the media/manifest request) and use that URL.
  2. Fetch the URL manually (curl with the same headers/referer/User-Agent) and check the body starts with #EXTM3U; if you get HTML, the endpoint needs authentication, different headers, or is blocking the client.
  3. If the stream requires cookies or referer headers, supply them to the downloader so the origin returns the playlist rather than an error page.
  4. Check the m3u8 file for corruption/encoding issues (BOM, HTML entities) if you control the source.

Example fix

// before
hls.download("https://example.com/watch/123", quality, token).await?;
// -> "Failed to parse m3u8: neither master nor media playlist" (page is HTML)
// after: use the playlist URL discovered from the page/devtools
hls.download("https://cdn.example.com/hls/123/index.m3u8", quality, token).await?;
Defensive patterns

Strategy: validation

Validate before calling

let text = reqwest::get(url).await?.text().await?;
let is_playlist = text.trim_start().starts_with("#EXTM3U");
if !is_playlist {
    eprintln!("URL does not return an m3u8 playlist");
} else {
    hls.download(url, quality, token).await?;
}

Type guard

fn looks_like_m3u8(body: &str) -> bool {
    body.trim_start().starts_with("#EXTM3U")
}

Try / catch

match hls.download(url, quality, token).await {
    Err(e) if e.to_string().starts_with("Failed to parse m3u8") => {
        // re-resolve the real playlist URL from the page, then retry once
    },
    other => other?,
}

Prevention

When it happens

Trigger: Calling download()/download_with_quality with a URL whose response body is neither a master (#EXTM3U with #EXT-X-STREAM-INF) nor a media playlist: HTML error/login pages, JSON API responses, plain text, binary content, or an m3u8 so malformed the parsers reject it.

Common situations: Passing a webpage URL instead of the actual playlist URL; the origin serves an anti-bot/Cloudflare interstitial or auth page instead of the playlist; expired stream links returning error bodies with 200 status; CDN serving a captive-portal style response.

Understand the failure class

Related errors


AI-assisted analysis of tonhowtf/omniget@8600b91f42 (2026-09-12). Data as JSON: /api/errors/fbc7a5e5813eda52. Report an issue: GitHub.

Appendix: source

Thrown at src-tauri/omniget-core/src/core/hls_downloader.rs:195

                    .await;
            }
        }

        if parse_media_playlist(m3u8_bytes).is_ok() {
            return self
                .download_media_playlist(
                    m3u8_url,
                    output_path,
                    referer,
                    bytes_tx,
                    cancel_token,
                    max_concurrent,
                    max_retries,
                )
                .await;
        }

        anyhow::bail!("Failed to parse m3u8: neither master nor media playlist")
    }

    async fn fetch_m3u8_with_retry(
        &self,
        url: &str,
        referer: &str,
        max_retries: u32,
    ) -> anyhow::Result<String> {
        if let Some(text) = self.prefetched_for(url) {
            tracing::info!(
                "[hls] using prefetched playlist text ({} bytes)",
                text.len()
            );
            return Ok(text.to_string());
        }

        let mut last_err = None;
        for attempt in 0..max_retries {

View on GitHub (pinned to 8600b91f42)