{"record":{"id":"ab7e46b22850c66d","repo":"tonhowtf/omniget","slug":"o-arquivo-baixado-n-o-parece-o-export-do-goodreads","errorCode":null,"errorMessage":"o arquivo baixado não parece o export do Goodreads","messagePattern":"o arquivo baixado não parece o export do Goodreads","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/omniget-core/src/core/tools/lists/goodreads.rs","lineNumber":326,"sourceCode":"                \"o Goodreads ainda não gerou o CSV depois de {}s. Deixe goodreads.com/review/import aberto, espere e rode de novo — o link fica salvo lá\",\n                limit.as_secs()\n            ));\n        }\n        report(\n            p,\n            TOOL_ID,\n            \"progress\",\n            started.elapsed().as_secs(),\n            Some(limit.as_secs()),\n            Some(\"esperando o Goodreads gerar o CSV\".into()),\n        );\n        tokio::time::sleep(Duration::from_secs(10)).await;\n    }\n\n    let (bytes, _) = f.get_bytes(&csv_url).await?;\n    let csv = String::from_utf8_lossy(&bytes).to_string();\n    if !csv.to_lowercase().contains(\"title\") {\n        return Err(anyhow!(\n            \"o arquivo baixado não parece o export do Goodreads\"\n        ));\n    }\n    Ok((csv, started.elapsed().as_secs()))\n}\n\n/// A página devolvida quando a sessão não vale: o Goodreads responde 200 com\n/// a tela de entrada em vez de mandar para outro lugar.\npub fn is_signed_out(html: &str) -> bool {\n    let has_form = html.contains(\"review_porter\") || html.contains(\"js-LibraryExport\");\n    !has_form && (html.contains(\"/user/sign_in\") || html.contains(\"/user/new\"))\n}\n\n// ── Prateleira na web ───────────────────────────────────────────────────\n\npub fn shelf_url(user: &str, shelf: &str, page: u32) -> String {\n    format!(\n        \"https://www.goodreads.com/review/list/{}?shelf={}&per_page=100&page={}&print=true\",","sourceCodeStart":308,"sourceCodeEnd":344,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/omniget-core/src/core/tools/lists/goodreads.rs#L308-L344","documentation":"This error is thrown by trigger_and_wait in the Goodreads export tool after it downloads the CSV generated by Goodreads' import/export pipeline. The tool polls until the export is ready, fetches the file, and sanity-checks that the body contains the string 'title' (a required column header in a Goodreads library export). If the downloaded text does not look like a Goodreads CSV, it assumes something else came back (HTML error page, login page, empty export) and throws this anyhow error.","triggerScenarios":"Calling run without a local CSV path so trigger_and_wait waits for Goodreads to generate the export, then f.get_bytes(&csv_url) returns content whose lowercase form lacks 'title' — e.g. Goodreads returned an HTML/login/error page at the export URL, the export job produced an empty or partial file, or the poll loop timed out and grabbed a not-yet-ready resource.","commonSituations":"Goodreads export job never completed but the URL was fetched anyway; the session cookies expired so Goodreads served a sign-in page; Goodreads changed its export format or column names; network middleware (proxy/CAPTCHA page) intercepted the download.","solutions":["Check that the Goodreads session cookies are valid: re-capture cookies from goodreads.com in the browser extension and pass them via opts.session_netscape.","Trigger the export manually at goodreads.com/review/import and download goodreads_library_export.csv, then pass its path via opts.csv_path so the download/poll path is skipped entirely.","Increase opts.delay_ms / re-run to give Goodreads more time to finish generating the export before the fetch.","Inspect what was actually downloaded (log the first bytes of csv) to see if it is an HTML page or an empty file and adjust accordingly."],"exampleFix":"// before: rely solely on polling the generated export\nlet (csv, secs) = trigger_and_wait(&f, opts, &p).await?;\n// after: prefer an existing local export when present\nif let Some(path) = opts.csv_path.as_deref().map(str::trim) {\n    let csv = std::fs::read_to_string(path)?;\n    ...\n} else {\n    let (csv, secs) = trigger_and_wait(&f, opts, &p).await?;\n    ...\n}","handlingStrategy":"validation","validationCode":"if let Some(path) = &opts.csv_path {\n    let head = std::fs::read_to_string(path)?;\n    anyhow::ensure!(head.to_lowercase().contains(\"title\"), \"arquivo não parece export do Goodreads\");\n}","typeGuard":"fn looks_like_goodreads_csv(body: &str) -> bool {\n    body.to_lowercase().contains(\"title\")\n}","tryCatchPattern":"match tool::run(&opts, &p).await {\n    Ok(res) => println!(\"export ok: {}\", res.output_dir),\n    Err(e) if e.to_string().contains(\"export do Goodreads\") => {\n        eprintln!(\"Goodreads devolveu algo inesperado; re-capture os cookies ou gere o CSV manualmente\");\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Keep a manually downloaded goodreads_library_export.csv handy and pass csv_path to skip the fragile polling path","Re-capture Goodreads cookies right before running; expired sessions yield HTML instead of CSV","Give the export time to finish (larger delay_ms) before the tool fetches the file","Log the first bytes of any failed download to distinguish HTML pages from empty files"],"tags":["goodreads","http","validation","export"],"backgroundTag":"unexpected-response-shape","analyzedSha":"8600b91f4246848bac346874daa9e61c1fc5677a","analyzedAt":"2026-09-12T14:29:19.317Z","contentChangedAt":"2026-09-12T14:29:19.317Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}