{"record":{"id":"0ea5bfb0c4027c7c","repo":"jackwener/OpenCLI","slug":"xiaoyuzhou-playback-progress-pid-did-not-match-his","errorCode":null,"errorMessage":"Xiaoyuzhou playback progress pid did not match history eid ${eid}","messagePattern":"Xiaoyuzhou playback progress pid did not match history eid (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/xiaoyuzhou/history.js","lineNumber":126,"sourceCode":"        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();\n    for (const [index, row] of response.data.entries()) {\n        if (!isRecord(row)) {\n            throw new CommandExecutionError(`Xiaoyuzhou playback progress row ${index + 1} is malformed`);\n        }\n        const eid = requiredId(row.eid, `progress eid in row ${index + 1}`);\n        const episode = requested.get(eid);\n        if (!episode) {\n            throw new CommandExecutionError(`Xiaoyuzhou playback progress returned unrequested eid ${eid}`);\n        }\n        if (progressById.has(eid)) {\n            throw new CommandExecutionError(`Xiaoyuzhou playback progress returned duplicate eid ${eid}`);\n        }\n        const pid = requiredId(row.pid, `progress pid in row ${index + 1}`);\n        if (pid !== episode.pid) {\n            throw new CommandExecutionError(`Xiaoyuzhou playback progress pid did not match history eid ${eid}`);\n        }\n        const progressSec = optionalSeconds(row.progress, `progress in row ${index + 1}`);\n        if (progressSec !== null && episode.durationSec !== null && progressSec > episode.durationSec) {\n            throw new CommandExecutionError(`Xiaoyuzhou playback progress exceeded duration for eid ${eid}`);\n        }\n        progressById.set(eid, {\n            progressSec,\n            playedAt: optionalIsoTime(row.playedAt, `playedAt in row ${index + 1}`),\n        });\n    }\n    for (const episode of episodes) {\n        if (!progressById.has(episode.eid)) {\n            throw new CommandExecutionError(\n                `Xiaoyuzhou playback progress omitted requested eid ${episode.eid}; the history join is incomplete`,\n            );\n        }\n    }\n    return progressById;","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/xiaoyuzhou/history.js#L108-L144","documentation":"After resolving each progress row's eid to a requested episode, parseProgressRows verifies the row's pid (podcast id) equals the pid of the matching history episode. A mismatch means the progress row and the history entry describe different podcasts, so the join is inconsistent and the library refuses to produce misleading data.","triggerScenarios":"The progress API returns a pid for an eid that differs from the pid recorded in the history entry for that same eid — typically after an upstream API change, an eid/pid field swap, or stale/mixed responses from different endpoints.","commonSituations":"Server changes the meaning or casing of pid/eid fields; a proxy serves responses captured at different times; custom code mutates episode objects (e.g. remapping pids) before parsing; version skew between history and progress endpoints.","solutions":["Log the offending eid, the progress pid, and the history episode pid and compare against a raw API response.","Re-fetch history and progress in one session so both responses come from the same API version.","Remove any local transformation of episode.pid before passing episodes into parseProgressRows.","If the API genuinely changed field semantics, update the parsing code to the new contract."],"exampleFix":"// before\nepisodes.push({ eid: entry.eid, pid: entry.podcast.id.toUpperCase(), ... });\nreturn parseProgressRows(rows, episodes);\n// after\nepisodes.push({ eid: entry.eid, pid: entry.podcast.id, ... }); // keep pid exactly as returned\nreturn parseProgressRows(rows, episodes);","handlingStrategy":"validation","validationCode":"for (const r of progressRows) {\n  const ep = episodesById.get(r.eid);\n  if (ep && r.pid !== ep.pid) throw new Error(`pid mismatch for ${r.eid}: ${r.pid} != ${ep.pid}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  const progress = progressById(episodes);\n} catch (e) {\n  if (e instanceof CommandExecutionError && /pid did not match/.test(e.message)) {\n    console.error('History/progress join mismatch:', e.message);\n  } else throw e;\n}","preventionTips":["Keep pid/eid values exactly as returned by the API; never transform them.","Fetch history and progress in the same session so versions match.","Compare pids in fixtures against live responses after API updates."],"tags":["api","data-integrity","join-mismatch","schema"],"backgroundTag":"join-key-mismatch","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}