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
- 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
- Check whether the library needs updating for a changed API schema (media.source.url moved)
- Confirm the episode actually has downloadable audio (open it in the app/website); skip text/video-only episodes
- 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
- Check episodes have audio in the app before scripting downloads
- Log the raw episode payload once per run to detect API schema drift early
- Keep the CLI updated when Xiaoyuzhou changes its API response shape
- Skip freshly published episodes until media processing completes
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
- PARSE_ERROR
- Bilibili creator comparison omitted metric ${definition.key}
- hf paper returned malformed JSON: ${err?.message ?? err}
- Suno feed lookup returned malformed clip identity
- EMPTY_RESPONSE
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/795a550676936125.
Report an issue: GitHub.