Youtube-DL but for product pages.
Project description
product-dl / product-ui / list-dl
This repository contains three packages:
product-dl— "youtube-dl but for product pages". Given a URL it detects the right provider, scrapes the product page, and returns a normalisedParsedProductas JSON. Exposes a CLI and a FastAPI JSON API (default port 7001).product-ui— UI companion. Calls theproduct-dlAPI, persists crawls to disk, and serves a Bootstrap web interface for browsing and triggering crawls (default port 7002).list-dl— Scrapes order histories and wishlists on demand. Likeproduct-dlbut for lists (Amazon orders, public wishlists, etc.). Returns structuredOrderobjects. Exposes a CLI and a FastAPI JSON API (default port 7003).
product-dl
Supported sites
| Provider | Domains | ID format |
|---|---|---|
kleinanzeigen |
kleinanzeigen.de | /s-anzeige/…/<id> |
ebay |
ebay.de, ebay.com, ebay.co.uk, … | /itm/…/<id> |
amazon |
amazon.de, amazon.com, amazon.co.uk, … | /dp/<ASIN>, /gp/product/<ASIN> |
aliexpress |
aliexpress.com, aliexpress.us | /item/….html |
thomann |
thomann.de, thomannmusic.com | /<slug>.htm |
Install
pip install product-dl # or: uv add product-dl
CLI
# Fetch a product page and print JSON
product-dl fetch <url>
# Extra options
product-dl fetch <url> \
--headers '{"Accept-Language": "de-DE"}' \
--cookies path/to/cookies.json \
--data-dir ./data \
--save-example my-label \
--indent 4
# Parse a locally saved HTML file
product-dl parse amazon path/to/product.html --url https://www.amazon.de/dp/B08N5WRWNW
# Start the API server
product-dl serve --host 0.0.0.0 --port 7001
# Export the OpenAPI schema (default: YAML)
product-dl openapi
product-dl openapi --format json --output ./openapi.json
# List discovered providers
product-dl providers
product-dl providers --format json
--data-dir / PRODUCT_DL_DATA_DIR
When set, the CLI (and API server via env var) will:
- Auto-load
<data-dir>/cookies/<provider>.jsonand<data-dir>/headers/<provider>.jsonbefore each request. - Detect Akamai CDN bot-detection and switch to
httpx-curl-cffi(Chrome TLS impersonation) automatically:- If any Akamai cookie key (
ak_bmsc,_abck,bm_sz,bm_sv,RT) is present in the merged cookies. - Or if a 503 response body contains Akamai signatures
(
errors.edgesuite.net/Zero size object).
- If any Akamai cookie key (
- Save Akamai cookies received in curl_cffi responses back to
<data-dir>/cookies/<provider>.jsonfor reuse.
# CLI
product-dl fetch https://www.ebay.de/itm/123 --data-dir ./data
# env var (also used by the API server)
export PRODUCT_DL_DATA_DIR=./data
product-dl fetch https://www.ebay.de/itm/123
API
product-dl serve # http://127.0.0.1:7001
# or
uvicorn product_dl.api.app:app
GET /fetch?url=<url>
POST /fetch {"url": "<url>"}
Both return a ParsedProduct JSON object. PRODUCT_DL_DATA_DIR is read
server-side from the environment — it is not an API parameter.
Latest generated API descriptions are published to the repository gh-pages
branch as:
product_dl/openapi.jsonproduct_dl/openapi.ymlproduct_dl/providers.jsonlist_dl/openapi.jsonlist_dl/openapi.ymllist_dl/providers.jsonlist_dl/login_providers.jsoncatalog/commit_map.json
Output schema (ParsedProduct)
ParsedProduct
├── id: str | int | None
├── title: str
├── description: str | None
├── gallery: list[Image | Video | PDF] | None
│ Image / Video ── url, mime, thumb, width, height
│ Video ── length (seconds / frames)
│ PDF ── url, name, page_count
├── pricing: Pricing | None
│ ├── price: Price | None (value, unit, raw, symbol, suffix)
│ ├── shipping: list[Shipping] (name, price)
│ ├── original: Price | None (pre-discount list price)
│ └── reduction: list[Reduction] (percentage or price discount)
├── canonical_url: str | None
├── fetch_url: str
├── parser: str e.g. "kleinanzeigen", "amazon"
├── website_variant: str | None e.g. "de", "co.uk", "gb"
├── version: int schema version (starts at 1)
├── available: bool | None
├── seller: Seller | None (name, id, url, picture)
├── brand: Brand | None (name, url, logo)
├── category: Category | None (name, id, url, parent)
├── variation_types: list[VariationType] | None
├── tables: dict[str, dict[str, str]] | None
├── meta: PageMeta | None (meta tags, LD+JSON, tracking)
└── extra: <ProviderExtra> provider-specific fields
Adding a provider
Built-in — create src/product_dl/providers/<name>/__init__.py exposing:
PROVIDER_NAME: str
def can_provide(url: str, parsed_url: ParseResult) -> T | None: ...
async def provide(
url: str,
parsed_url: ParseResult,
provide_check: T,
*,
save_files: SaveFiles | None = None,
local_html: str | None = None,
extra_headers: dict[str, str] | None = None,
extra_cookies: dict[str, str] | None = None,
data_dir: Path | None = None,
) -> ParsedProduct: ...
Auto-discovered via pkgutil — no pyproject.toml changes needed.
Use product_dl.utils.http.fetch_html for HTTP requests so Akamai handling
and cookie persistence work automatically.
Third-party — publish a package with a product_dl.providers entry point.
No changes to this project needed.
product-ui
Install
pip install product-dl # product-ui is bundled in the same package
Quick start
# 1. Start the product-dl API backend
product-dl serve
# 2. Start the UI (in another terminal)
product-ui serve
# 3. Open http://127.0.0.1:7002
CLI
# Start the web UI
product-ui serve [--host 127.0.0.1] [--port 7002] [--reload] \
[--data-dir ./crawl_data/product_dl] [--product-dl-api http://localhost:7001]
# Fetch a URL and save to disk (product-dl must be running)
product-ui fetch <url> [--data-dir ./crawl_data/product_dl] [--product-dl-api http://localhost:7001]
Environment variables
| Variable | Default | Description |
|---|---|---|
PRODUCT_UI_DATA_DIR |
./crawl_data/product_dl |
Directory where crawls are stored |
PRODUCT_DL_API |
http://localhost:7001 |
Base URL of the product-dl JSON API |
Disk layout
{data_dir}/{provider}/{YYYY}/{MM}/{DD}/{HH_MM_SS}/{id}/
crawl.json ← {"information": {url, provider, variant, time}, "crawl": <ParsedProduct>}
files/full/{nnn}.{ext} ← base64 gallery files decoded to disk
files/thumb/{nnn}.{ext} ← thumbnails (same index number as matching full)
{data_dir}/index.jsonl ← append-only JSON-Lines: {provider, url, id, title, date, path}
Web UI routes
| Route | Description |
|---|---|
GET / |
Filterable list of crawls; includes new-crawl form (filters via GET → shareable URLs) |
GET /crawl?url=<path> |
Detail view: gallery, pricing, variations, tables, provider extras |
POST /fetch |
Trigger a new crawl (form field url); saves to disk, redirects to detail |
GET /data/… |
Static serving of saved crawl files (images, PDFs, etc.) |
list-dl
Scrapes order histories and wishlists from e-commerce sites on demand. Mirror of product-dl architecture but for lists instead of individual products.
Supported providers
| Provider | Status | Notes |
|---|---|---|
amazon_orders |
Active | Amazon order history scraper (pagination, delivery states, items). Requires authentication. |
Install
# Bundled with product-dl
pip install product-dl
Login & Credentials
list-dl uses a pluggable login provider system. Each provider handles site-specific authentication and stores credentials locally for reuse.
Login Command
# Interactive login (prompts for password)
list-dl login amazon --username user@example.com
# Non-interactive login (provide all credentials)
list-dl login amazon --username user@example.com --password mypassword
# Login to a specific flavor (e.g., amazon.de)
list-dl login amazon --flavor de --username user@example.com --password mypassword
Credentials are stored in ./crawl_data/list_dl/login/{site}/login.json.
Environment Variables
Credentials can be loaded from environment variables. Precedence (highest to lowest):
- CLI arguments (
--username,--password,--code) LOGIN_{SITE}_{FLAVOR}_{FIELD}(e.g.,LOGIN_AMAZON_DE_USERNAME)LOGIN_{SITE}_{FIELD}(e.g.,LOGIN_AMAZON_USERNAME)- Generic field name (e.g.,
USERNAME,PASSWORD,CODE) — Warning: genericUSERNAMEdefaults to the Linux user if not explicitly set
# Load credentials from environment
export LOGIN_AMAZON_USERNAME=user@example.com
export LOGIN_AMAZON_PASSWORD=mypassword
list-dl login amazon
# Or with flavor
export LOGIN_AMAZON_DE_USERNAME=user@example.com
export LOGIN_AMAZON_DE_PASSWORD=mypassword
list-dl login amazon --flavor de
# Non-interactive (no TTY)
echo mypassword | list-dl login amazon --username user@example.com
.env File Support
The login command reads from .env.local (highest priority) and .env files:
# .env.local (excluded from git)
LOGIN_AMAZON_USERNAME=user@example.com
LOGIN_AMAZON_PASSWORD=mypassword
Then:
list-dl login amazon
Two-Factor Authentication (2FA / TOTP)
When a site requires 2FA, list-dl supports three workflows:
Workflow 1: Single command (all credentials at once)
list-dl login amazon --username user@example.com --password mypass --code 123456
Automatically chains login → 2FA → stores final credentials.
Workflow 2: Two separate commands
# First run: login, save partial credentials with TOTP form
list-dl login amazon --username user@example.com --password mypass
# Output: ⚠ TOTP code required. Run: list-dl login amazon --code <code> --flavor com
# Second run: submit 2FA code using stored credentials
list-dl login amazon --code 123456
Workflow 3: Interactive mode (detects TTY)
# Prompts for missing credentials, including 2FA code when required
list-dl login amazon
# Email address: user@example.com
# Password: (hidden input)
# 2FA Code (6 digits): 123456
2FA codes can also be provided via the CODE environment variable:
export CODE=123456
list-dl login amazon
CLI
# Fetch orders from Amazon (requires prior login)
list-dl fetch amazon orders --mode my_orders
# Fetch a public wishlist (no login required)
list-dl fetch amazon wishlist --url https://www.amazon.de/hz/wishlist/ls/ABC123
# Fetch from a different flavor (amazon.de)
list-dl fetch amazon orders --flavor de --mode my_orders
# Start the API server
list-dl serve --host 0.0.0.0 --port 7003
# Export the OpenAPI schema (default: YAML)
list-dl openapi
list-dl openapi --format json --output ./openapi.json
# List discovered providers
list-dl providers
list-dl providers --format json
list-dl login-providers --format json
API
list-dl serve # http://127.0.0.1:7003
Credentials can be supplied in the request:
POST /fetch
{
"site": "amazon",
"category": "orders",
"mode": "my_orders",
"credentials": {"username": "...", "password": "...", "cookies": {...}},
"fetch_details": true
}
Or, if already logged in, credentials are auto-loaded from disk:
curl -X POST http://localhost:7003/fetch \
-H "Content-Type: application/json" \
-d '{"site": "amazon", "category": "orders", "mode": "my_orders"}'
Output schema (Order)
Order
├── order_number: str unique order ID
├── provider_name: str e.g. "amazon_orders"
├── url: str | None link to order detail page
├── order_date: date | None when order was placed
├── total: Price | None {value, unit, raw, symbol, suffix}
├── dispatch_to: Address | None shipping address
├── delivery: DeliveryStatus | None {state, substate}
│ state: "delivered" | "on_the_way_late" | "cancelled" | ...
│ substate: Optional freeform substate (e.g. "Parcel left in letterbox")
├── delivered_at: datetime | None
├── return_until: date | None
├── documents: list[Attachment] | None invoices, receipts, etc.
├── summary: OrderSummary | None price breakdown
│ ├── items_subtotal, postage, total_before_vat, estimated_vat, total, grand_total
├── payment: PaymentMethod | None how order was paid
│ ├── type, bank, masked_number
├── items: list[OrderItem] | None products in order
│ ├── link: URL
│ ├── product: ParsedProduct | None (fetched via product-dl if available)
│ └── extra: <ProviderItemExtra>
└── extra: <ProviderOrderExtra> provider-specific fields
Adding a provider
Built-in — create src/list_dl/providers/<name>/__init__.py with:
from list_dl.providers.base import ListProviderBase, register
@register
class MyOrdersProvider(ListProviderBase[CHECK_TYPE, MyOrderExtra, MyItemExtra]):
PROVIDER_NAME = "my_orders"
@classmethod
def can(cls, provider_name, mode, url) -> CHECK_TYPE | None:
"""Return truthy if this provider can handle the request."""
...
async def init(self) -> None:
"""Initialize (e.g., open HTTP client)."""
...
async def fetch_orders(self) -> AsyncIterator[Order]:
"""Yield Order objects."""
...
async def fetch_order_detail(self, order: Order) -> Order:
"""Enrich order with detail page data (items, summary, payment)."""
return order # or enriched order
Auto-discovered via pkgutil — no pyproject.toml changes needed. Use httpx with optional Akamai bypass via curl_cffi (same pattern as product-dl).
Deploy
- Remember to bump the version in pyproject.toml:3
-
export PYPI_TOKEN=''
-
uv build uv publish --token "$PYPI_TOKEN"
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file product_dl-0.0.0.dev2.tar.gz.
File metadata
- Download URL: product_dl-0.0.0.dev2.tar.gz
- Upload date:
- Size: 3.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80128e8722f9bba74247f66f507c8f753ea0d69b9c7e5dd8502cf755cf75ffd4
|
|
| MD5 |
e9f0eadcdd895b49055196d87b17c9eb
|
|
| BLAKE2b-256 |
039f6b844a5c4149a839565df73ae8468ae4e332db4d3cab8aa5cbe813945dfe
|
File details
Details for the file product_dl-0.0.0.dev2-py3-none-any.whl.
File metadata
- Download URL: product_dl-0.0.0.dev2-py3-none-any.whl
- Upload date:
- Size: 168.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0f7de92faa1e056f0d5681efb44ff19e4ab7d68ad716fbe2ca6c345683ecba2b
|
|
| MD5 |
c4de3cd173d14e68aef941790e6195fa
|
|
| BLAKE2b-256 |
c94cf1ff850e813b99c7dace0f755f59b9d2260de40bdf98816e77c9f5b8125a
|