jackwener/OpenCLI · error · CliError

PARSE_ERROR

PARSE_ERROR

Error message

Audio URL not found in episode payload

What it means

Thrown by the download command (clis/xiaoyuzhou/download.js:34) as a CliError with code PARSE_ERROR when the episode payload fetched from /v1/episode/get exists but does not expose an audio URL at media.source.url. The library requires that exact nested path to start the audio download; a schema drift or missing media block makes the download impossible.

Source

Thrown at clis/xiaoyuzhou/download.js:34

    browser: false,
    args: [
        { name: 'id', positional: true, required: true, help: 'Episode ID (eid from podcast-episodes output)' },
        { name: 'output', default: './xiaoyuzhou-downloads', help: 'Output directory' },
    ],
    columns: ['title', 'podcast', 'status', 'size', 'file'],
    func: async (args) => {
        const credentials = loadXiaoyuzhouCredentials();
        const response = await requestXiaoyuzhouJson('/v1/episode/get', {
            query: { eid: args.id },
            credentials,
        });
        const ep = response.data;
        if (!ep) {
            throw new CliError('NOT_FOUND', 'Episode not found', 'Please check the ID');
        }
        const audioUrl = ep.media?.source?.url;
        if (!audioUrl) {
            throw new CliError('PARSE_ERROR', 'Audio URL not found in episode payload', 'Episode payload does not expose media.source.url');
        }
        const output = String(args.output || './xiaoyuzhou-downloads');
        const ext = path.extname(new URL(audioUrl).pathname) || '.mp3';
        const title = String(ep.title || 'episode');
        const filename = `${args.id}_${sanitizeFilename(title, 80) || 'episode'}${ext}`;
        const outputDir = path.join(output, String(args.id));
        fs.mkdirSync(outputDir, { recursive: true });
        const destPath = path.join(outputDir, filename);
        const result = await httpDownload(audioUrl, destPath, {
            timeout: 60000,
        });
        return [{
                title,
                podcast: ep.podcast?.title || '',
                status: result.success ? 'success' : 'failed',
                size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
                file: result.success ? destPath : '-',
            }];

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Dump the raw episode payload and locate where the audio URL now lives, then adjust the access path in download.js if you control it
  2. Check whether the library needs updating for a changed API schema (media.source.url moved)
  3. Confirm the episode actually has downloadable audio (open it in the app/website); skip text/video-only episodes
  4. Retry later if the episode was just published — media processing may not be complete

Example fix

// before
const audioUrl = ep.media?.source?.url;
// after
const audioUrl = ep.media?.source?.url ?? ep.media?.url ?? ep.audio?.url;
Defensive patterns

Strategy: type-guard

Validate before calling

function episodeHasAudio(ep) {
  return Boolean(ep && typeof ep === 'object' && typeof ep.media?.source?.url === 'string' && ep.media.source.url.length > 0);
}
// call the API, then: if (!episodeHasAudio(response.data)) skip download;

Type guard

function hasAudioUrl(ep) {
  return typeof ep?.media?.source?.url === 'string' && ep.media.source.url.length > 0;
}

Try / catch

try {
  await cli.download({ id });
} catch (error) {
  if (error.code === 'PARSE_ERROR') {
    console.error('Episode payload lacks media.source.url — episode may have no audio or the API schema changed');
  } else throw error;
}

Prevention

When it happens

Trigger: The API returns an episode object whose ep.media?.source?.url is undefined/null: episodes with no audio stream (text-only or video-only), API schema changes moving the audio URL, or partial/degraded API responses missing the media object.

Common situations: Xiaoyuzhou updates their API response shape so media.source.url moves (version drift); trying to download a video episode or an episode still processing with no audio attached; an intermediary/proxy stripping fields from the response.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/795a550676936125. Report an issue: GitHub.