{"record":{"id":"e22dc8d451349398","repo":"jackwener/OpenCLI","slug":"xiaoyuzhou-history-row-rownumber-is-missing-pod","errorCode":null,"errorMessage":"Xiaoyuzhou history row ${rowNumber} is missing podcast or finished state","messagePattern":"Xiaoyuzhou history row (.+?) is missing podcast or finished state","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaoyuzhou/history.js","lineNumber":93,"sourceCode":"        throw new CommandExecutionError('Xiaoyuzhou history returned an unexpected response shape');\n    }\n    if (next == null || next === '') return { entries, next: null };\n    if (typeof next !== 'string' || !next.trim()) {\n        throw new CommandExecutionError('Xiaoyuzhou history returned an invalid loadMoreKey');\n    }\n    if (entries.length === 0) {\n        throw new CommandExecutionError('Xiaoyuzhou history returned an empty page with a continuation cursor');\n    }\n    return { entries, next: next.trim() };\n}\n\nfunction parseHistoryEpisode(entry, rowNumber) {\n    if (!isRecord(entry) || !isRecord(entry.episode)) {\n        throw new CommandExecutionError(`Xiaoyuzhou history row ${rowNumber} is malformed; expected episode metadata`);\n    }\n    const episode = entry.episode;\n    if (!isRecord(episode.podcast) || typeof episode.isFinished !== 'boolean') {\n        throw new CommandExecutionError(`Xiaoyuzhou history row ${rowNumber} is missing podcast or finished state`);\n    }\n    return {\n        eid: requiredId(episode.eid, `eid in row ${rowNumber}`),\n        pid: requiredId(episode.pid, `pid in row ${rowNumber}`),\n        title: requiredString(episode.title, `title in row ${rowNumber}`),\n        podcast: requiredString(episode.podcast.title, `podcast title in row ${rowNumber}`),\n        durationSec: optionalSeconds(episode.duration, `duration in row ${rowNumber}`, { positive: true }),\n        pubDate: optionalIsoTime(episode.pubDate, `pubDate in row ${rowNumber}`, { required: true }),\n        finished: episode.isFinished,\n    };\n}\n\nfunction parseProgressRows(response, episodes) {\n    if (!Array.isArray(response?.data)) {\n        throw new CommandExecutionError('Xiaoyuzhou playback progress returned an unexpected response shape');\n    }\n    const requested = new Map(episodes.map((episode) => [episode.eid, episode]));\n    const progressById = new Map();","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaoyuzhou/history.js#L75-L111","documentation":"After confirming the row has an episode object, parseHistoryEpisode requires `episode.podcast` to be a record (its title is read later) and `episode.isFinished` to be a boolean. Missing either means the library cannot render the podcast column or the finished state, so it throws CommandExecutionError.","triggerScenarios":"A history row's episode object omits `podcast` (or it is null/non-object) or omits `isFinished` (or it is 0/1/'true' instead of a boolean), during `xiaoyuzhou history` parsing.","commonSituations":"Podcast unpublished/deleted so its metadata is dropped; API version change renames isFinished or switches it to an integer flag; partial responses from a flaky network layer; old fixtures.","solutions":["Retry — if the API intermittently returns partial episode objects, a fresh run may be complete.","Update the CLI/library to match any API field rename or type change (e.g. isFinished becoming an int).","Inspect the raw episode object to confirm whether podcast metadata or isFinished is absent vs. retyped.","Check the specific episode/podcast in the app — unpublished podcasts cause missing metadata rows.","Patch the parser (or report upstream) to coerce 0/1 isFinished or handle missing podcast titles if the API legitimately allows them."],"exampleFix":"// before\n{ \"episode\": { \"eid\": \"...\", \"isFinished\": 1 } }\n// after\n{ \"episode\": { \"eid\": \"...\", \"podcast\": { \"title\": \"P\" }, \"isFinished\": true } }","handlingStrategy":"type-guard","validationCode":"function hasPodcastAndFinished(ep) { return ep !== null && typeof ep === 'object' && ep.podcast !== null && typeof ep.podcast === 'object' && typeof ep.isFinished === 'boolean'; }","typeGuard":"function isWellFormedEpisode(ep) { return typeof ep === 'object' && ep !== null && typeof ep.podcast === 'object' && ep.podcast !== null && typeof ep.isFinished === 'boolean'; }","tryCatchPattern":"try {\n  const rows = await runHistory();\n} catch (e) {\n  if (/missing podcast or finished state/.test(e.message)) {\n    // retry, or fall back to rows without finished-state filtering\n  } else throw e;\n}","preventionTips":["Keep the CLI updated for field renames/type changes in the episode schema","Avoid fetching history immediately after deleting podcasts/episodes from the account","Validate fixtures against the current episode schema before replaying"],"tags":["api-response","schema-validation","xiaoyuzhou"],"backgroundTag":"missing-required-api-field","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}