# Changelog of Discogs Marketplace Scraper (`devilscrapes/discogs-sold-price`) Actor

- **URL**: https://apify.com/devilscrapes/discogs-sold-price/changelog.md
- **Full Actor documentation**: https://apify.com/devilscrapes/discogs-sold-price.md

## Changelog — discogs-sold-price

### \[0.5.0] — 2026-09-24

#### Fixed

- **REQ-19 — a blocked listings fetch no longer masquerades as success.**
  Reproduced live on cloud run `9g8zU9OvQHN3UQYrt` (release 249504, "Never
  Gonna Give You Up"): `www.discogs.com/sell/release/249504` 403'd
  Cloudflare on both attempts (the known `BUYPROXIES94952`-datacenter block
  from `CHANGELOG` 0.3.0), while an independent check against
  `api.discogs.com/marketplace/stats/249504` — a surface Cloudflare does
  not challenge — showed `num_for_sale: 121`. The release genuinely has
  121 live listings; the run still emitted 1 stats row, 0 listing rows,
  and exited SUCCEEDED, billing the customer for the paid-for "marketplace
  listings" half of the product while silently delivering none of it.
  `main.py` now cross-checks every blocked listings fetch against
  ground-truth `num_for_sale`: a confirmed positive count (or an
  undetermined count, if the stats fetch also fails) fails the run loud
  with a status message naming the blocked release_id(s); a confirmed `0`
  is reported plainly as a genuinely empty marketplace and still succeeds.
  See `docs/specs/discogs-sold-price/spec.md` REQ-19.
- `parsers/api.py` `parse_marketplace_stats` no longer coerces a real
  `num_for_sale: 0` to `None` — the reused `_coerce_optional_int` treated
  `0` as "not meaningful" (correct for `year`/`master_id`, wrong here),
  which made a genuinely-empty marketplace indistinguishable from "we
  don't know." New `extract_num_for_sale()` preserves the distinction and
  is what REQ-19's cross-check relies on.
- Widened `tests/fixtures/input.qa.json` (`maxPagesPerRelease` 1 → 2) so
  cloud QA exercises the pagination branch of the listings path, not just
  a single page — the prior fixture passed QA on the stats row alone
  while the listings half returned nothing (see
  `reference-prefill-decides-qa-coverage`).

### \[0.4.0] — 2026-09-16

#### Fixed

- `api_client.py` `_get_with_retry` and `html_client.py` `_get_text_with_retry`
  now catch `curl_cffi.requests.exceptions.RequestException` (connection
  reset, proxy connect failure, TLS error, timeout) around `session.get()`
  and retry it with the same backoff schedule as a `5xx`, converting it to
  the documented `RuntimeError` on exhaustion. Previously a transport-level
  failure — most likely on the shared `BUYPROXIES94952` datacenter proxy
  pool — propagated **raw**, past every `except RuntimeError` guard in
  `main.py`, crashing the whole Actor run instead of being isolated to one
  release/page. `failure_rate_delta.py` caught this live: 40% of 5 fresh
  customer runs failed on 2026-09-16 against a 15% 30-day smear, while our
  own `/acts/{id}/runs` history was empty (all failures were customer
  runs) — exactly the shape a per-request transport exception produces
  under real, higher-volume traffic that a single-request cloud QA run
  rarely triggers.

### \[0.3.0] — 2026-06-10

#### Fixed

- Reduce `html_client.py` `MAX_RETRIES` from 5 to 2 and `BACKOFF_CAP_S` from 30 s
  to 10 s. Discogs' `www.discogs.com` Cloudflare policy now issues a managed
  JS challenge to Apify datacenter IPs that cannot be resolved by HTTP-only
  impersonation. The old retry schedule wasted 60+ seconds per request batch,
  causing customer runs to TIMEOUT before emitting any data. Two retries fail
  fast (~6 s) so stats rows (fetched from the unaffected `api.discogs.com`
  surface) are still emitted and the run exits SUCCEEDED.
- Add `html_blocked` flag threading through `_run` / `_process_release` /
  `_emit_listing_rows` so the final status message clearly distinguishes
  "HTML blocked, stats only" from a genuine full-success.
- Rotate browser TLS fingerprint per run: `BROWSER_PROFILES` tuple
  (`chrome131`, `chrome124`, `firefox147`, `safari180`) selected randomly at
  session creation, per ADR-0002.

### \[0.2.0] — 2026-05-20

#### Fixed

- Add `prefill` to the discriminating input field so Apify's automated daily QA receives a runnable payload. Empty-input runs were tripping the Pydantic `model_validate` XOR/required-field check inside 100 ms, which after three consecutive days flagged the Actor "Under maintenance" and unlisted it from the Apify Store.

### \[0.0.2] — 2026-05-17

- Default `useProxy=true` — Cloudflare 403s un-proxied Apify datacenter
  IPs on the marketplace HTML surface (caught only by cloud QA).
- HTML client treats `403` as retriable (alongside `408 / 429 / 503`)
  so a fresh proxy session can be tried between attempts.

### \[0.0.1] — 2026-05-16

Initial public release.

- Two-source pipeline: Discogs JSON REST API (release metadata + marketplace
  stats + search) plus public marketplace HTML (per-listing asking prices,
  seller, condition, ships-from).
- Pydantic v2 input + output models (ADR-0004); single `ResultRow` schema
  with a `row_type` discriminator distinguishes per-listing rows from
  per-release stats rows.
- `curl-cffi` `chrome131` impersonation + one-shot Discogs homepage warm-up
  passes Cloudflare cleanly (verified live 2026-05-16, no JS challenge).
- Discogs REST API requires a custom `User-Agent`
  (`DevilScrapes/0.1 (+https://apify.com/DevilScrapes)`) — Discogs rejects
  default curl-cffi UAs with 403.
- Conservative pacing (`INTER_REQUEST_SLEEP_S = 1.5`, ~40 req/min) under
  Discogs' 60 req/min API limit and 25 req/min HTML limit.
- PPE: `actor-start` $0.05, `result-row` $0.005 — ~$5.05 per 1,000 rows.
- Input XOR: exactly one of `releaseIds` or `searchQuery` must be set
  (validated before any network call).
- Per-release fail isolation; only an entirely empty dataset triggers a
  non-zero exit.
