aperivue/fulltext-retrieval
Batch download open-access PDFs by DOI using legitimate OA APIs (Unpaywall, PMC, OpenAlex, Crossref). Optional PDF→Markdown conversion for token-efficient LLM analysis.
npx skills add https://github.com/Aperivue/medsci-skills --skill fulltext-retrieval
Batch download open-access full-text PDFs from a DOI list using legitimate OA APIs only.
DOI → arXiv (10.48550/arXiv.* DOIs) → Unpaywall → PMC (Europe PMC / OA FTP / web) → OpenAlex → Crossref → landing page
Each DOI goes through these sources in order until a valid PDF (≥10 KB, %PDF- header) is found. arXiv DOIs (10.48550/arXiv.2401.01234, version suffixes, old-style hep-th/9901001, or a bare arXiv: id) resolve directly to the arXiv PDF first.
# Prepare a DOI list (one per line)
cat > dois.txt << 'EOF'
10.1007/s00330-010-1783-x
10.1002/mp.12524
10.1148/radiol.13131265
EOF
# Run
python fetch_oa.py dois.txt --output pdfs/ --email [email protected]
# Verbose mode for debugging
python fetch_oa.py dois.txt -o pdfs/ -e [email protected] --verbose
Plain text — one DOI per line:
10.1007/s00330-010-1783-x
10.1002/mp.12524
TSV / CSV with header — must contain a DOI column; optional PMID and Title columns:
ID Title DOI PMID Year
1 Some paper 10.1007/s00330-010-1783-x 20628747 2010
Markdown table — a pipe table with a DOI column also works:
| DOI | PMID | Title |
|-----|------|-------|
| 10.1007/s00330-010-1783-x | 20628747 | Some paper |
When a PMID is available, the PMC lookup is more reliable (PMID → PMCID conversion). When a Title column is present, downloaded PDFs get a best-effort title cross-check (see *Retrieval report* below).
PMC web pages may block automated downloads with JavaScript proof-of-work challenges. This tool uses three fallback methods:
PMCID="PMC9733600"
curl -sLo output.pdf \
"https://europepmc.org/backend/ptpmcrender.fcgi?accid=${PMCID}&blobtype=pdf"
curl -s "https://www.ncbi.nlm.nih.gov/pmc/utils/oa/oa.fcgi?id=${PMCID}" | \
grep -oE 'href="[^"]*\.pdf"' | head -1 | \
sed 's/href="//;s/"//' | xargs curl -sLo output.pdf
# Works with both DOI and PMID
curl -s "https://www.ncbi.nlm.nih.gov/pmc/utils/idconv/v1.0/?ids=${DOI}&format=json" | \
python3 -c "import sys,json; print(json.load(sys.stdin)['records'][0].get('pmcid',''))"
{DOI_safe}.pdf (slashes replaced with underscores)pdfs/retrieval_report.json — structured per-DOI report (see below)manual_needed.txt — DOIs that could not be retrieved via OA--report)Every run writes a structured report (default <output>/retrieval_report.json,
override with --report PATH):
{
"schema_version": 1,
"generated_by": "fetch_oa.py",
"counts": {"total": 10, "retrieved": 6, "not_retrieved": 4, "title_mismatch": 1},
"items": [
{"doi": "10.1007/...", "pmid": "20628747", "title": "...",
"status": "oa", "source": "unpaywall", "file": "10.1007_....pdf",
"size_bytes": 482113, "title_match": "match"}
]
}
status ∈ arxiv | oa | pmc | skip | fail; source names the resolver that succeeded.title_match ∈ match | mismatch | unavailable (tri-state). It is best-effort:it needs a Title column and pdftotext (poppler). When either is missing it is
unavailable; a mismatch is flagged for review and never auto-rejects a PDF
(guards against a publisher serving a wrong/redirect PDF that still passes the %PDF- check).
OA-only resolvers miss paywalled-but-licensed papers. To attach full text **inside
Zotero** at a much higher yield, use references/find_available_pdf.js — a user-run
snippet for Zotero's *Tools → Developer → Run JavaScript*. It triggers Zotero's own
addAvailablePDF / addAvailablePDFs and therefore reuses your OpenURL resolver /
institutional proxy config; **no credentials, proxy hosts, or institutional identifiers
are hard-coded or leave your Zotero client**. The no-code equivalent is right-click →
"Find Available PDF".
This path is user-initiated and depends on your live Zotero session, so its results
are recorded manually (not reproducible CI evidence). /lit-sync Phase 2.7 orchestrates
both routes (disk OA via this script + in-library via the snippet) and reconciles them in
a report.
| Source | Rate Limit | Notes |
|--------|-----------|-------|
| Unpaywall | 100 req/sec | Email required |
| NCBI PMC | 3 req/sec without API key | Add &api_key= for higher limits |
| OpenAlex | 100k req/day | Polite pool with email in User-Agent |
| Crossref | 50 req/sec with email | Plus service with mailto: in UA |
| Europe PMC | No documented limit | Be polite, ≤1 req/sec recommended |
The script uses 0.3–0.5 second delays between requests.
After downloading PDFs, convert them to LLM-friendly Markdown for token-efficient repeated analysis. Uses pymupdf4llm — optimized for academic papers with two-column layout handling and table preservation.
# Install (one-time)
pip install pymupdf4llm
# Convert all PDFs in a directory
python pdf_to_md.py pdfs/
# Convert with verbose output
python pdf_to_md.py pdfs/ -v
# Custom output directory
python pdf_to_md.py pdfs/ -o markdown/
# First 10 pages only (useful for long supplements)
python pdf_to_md.py pdfs/ --pages 0-9
# Overwrite existing conversions
python pdf_to_md.py pdfs/ --force
# Step 1: Download PDFs
python fetch_oa.py dois.txt -o pdfs/ -e [email protected]
# Step 2: Convert to Markdown (only successful downloads)
python pdf_to_md.py pdfs/ -v
After conversion, .md files sit alongside .pdf files. Claude Code can then use Read for full content or Grep for targeted extraction — significantly more token-efficient than re-reading PDFs.
| Scenario | Recommendation |
|----------|---------------|
| Screening/triage (read once) | Skip — read PDF directly |
| Data extraction from k≥5 studies | Convert — repeated reads save tokens |
| Meta-analysis full pipeline | Convert — papers referenced across multiple phases |
| Single paper deep review | Optional — marginal benefit |
lines_strict strategy (preserves grid-line tables accurately)pdf_to_md.py requires pymupdf4llm (AGPL-3.0). This is an optional dependency — fetch_oa.py remains stdlib-only with zero external dependencies. The AGPL license applies to pymupdf4llm itself, not to this skill.
Take aperivue/fulltext-retrieval from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip.
Without those the skill loads but fails at the first command.