behance-dl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- behance_dl-0.1.0/.github/workflows/ci.yml +47 -0
- behance_dl-0.1.0/.github/workflows/publish.yml +46 -0
- behance_dl-0.1.0/.gitignore +32 -0
- behance_dl-0.1.0/AGENTS.md +245 -0
- behance_dl-0.1.0/LICENSE +21 -0
- behance_dl-0.1.0/PKG-INFO +152 -0
- behance_dl-0.1.0/README.md +119 -0
- behance_dl-0.1.0/pyproject.toml +71 -0
- behance_dl-0.1.0/src/behance_dl/__init__.py +18 -0
- behance_dl-0.1.0/src/behance_dl/__main__.py +10 -0
- behance_dl-0.1.0/src/behance_dl/cli.py +170 -0
- behance_dl-0.1.0/src/behance_dl/client.py +204 -0
- behance_dl-0.1.0/src/behance_dl/errors.py +15 -0
- behance_dl-0.1.0/src/behance_dl/gallery.py +115 -0
- behance_dl-0.1.0/src/behance_dl/models.py +79 -0
- behance_dl-0.1.0/src/behance_dl/naming.py +17 -0
- behance_dl-0.1.0/src/behance_dl/selection.py +114 -0
- behance_dl-0.1.0/src/behance_dl/storage.py +184 -0
- behance_dl-0.1.0/tests/conftest.py +131 -0
- behance_dl-0.1.0/tests/test_cli.py +152 -0
- behance_dl-0.1.0/tests/test_client.py +181 -0
- behance_dl-0.1.0/tests/test_gallery.py +180 -0
- behance_dl-0.1.0/tests/test_naming.py +43 -0
- behance_dl-0.1.0/tests/test_selection.py +218 -0
- behance_dl-0.1.0/tests/test_storage.py +229 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
# ubuntu-latest (26.04) no longer ships a Python 3.9 build, so the Linux jobs
|
|
19
|
+
# use 22.04 where every supported interpreter is available.
|
|
20
|
+
os: [ubuntu-22.04, windows-latest, macos-latest]
|
|
21
|
+
python-version: ["3.9", "3.13"]
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
- name: Install the package and its test tools
|
|
28
|
+
run: python -m pip install -e ".[dev]"
|
|
29
|
+
- name: Run the tests
|
|
30
|
+
run: python -m pytest --cov=behance_dl --cov-report=term-missing
|
|
31
|
+
|
|
32
|
+
lint:
|
|
33
|
+
name: Lint and format
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
- uses: actions/setup-python@v5
|
|
38
|
+
with:
|
|
39
|
+
python-version: "3.13"
|
|
40
|
+
- name: Install the package and its test tools
|
|
41
|
+
run: python -m pip install -e ".[dev]"
|
|
42
|
+
- name: Check lint rules
|
|
43
|
+
run: ruff check .
|
|
44
|
+
- name: Check formatting
|
|
45
|
+
run: ruff format --check .
|
|
46
|
+
- name: Check that the package builds
|
|
47
|
+
run: python -m build
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build the distribution
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.13"
|
|
20
|
+
- name: Install the build tool
|
|
21
|
+
run: python -m pip install build
|
|
22
|
+
- name: Build the wheel and sdist
|
|
23
|
+
run: python -m build
|
|
24
|
+
- name: Store the distribution
|
|
25
|
+
uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
name: Upload to PyPI
|
|
32
|
+
needs: build
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment:
|
|
35
|
+
name: pypi
|
|
36
|
+
url: https://pypi.org/p/behance-dl
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- name: Fetch the distribution
|
|
41
|
+
uses: actions/download-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist/
|
|
45
|
+
- name: Publish
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Byte-compiled files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Build and distribution output
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Test and coverage output
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
.coverage.*
|
|
16
|
+
htmlcov/
|
|
17
|
+
coverage.xml
|
|
18
|
+
|
|
19
|
+
# Virtual environments
|
|
20
|
+
.venv/
|
|
21
|
+
venv/
|
|
22
|
+
env/
|
|
23
|
+
|
|
24
|
+
# Tool caches
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
.mypy_cache/
|
|
27
|
+
|
|
28
|
+
# Editors and operating systems
|
|
29
|
+
.vscode/
|
|
30
|
+
.idea/
|
|
31
|
+
.DS_Store
|
|
32
|
+
Thumbs.db
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to AI coding agents when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
`behance-dl` is a command line downloader for public Behance project galleries, published to PyPI as `behance-dl` with the import package `behance_dl`.
|
|
8
|
+
|
|
9
|
+
It reads the project payload Behance inlines in a gallery page (`beconfig-store_state`), chooses one CDN variant per image, and downloads the files concurrently with resume support. It keeps gallery order, records which variant produced each file in `manifest.json`, and records prototypes and video modules as links in `embeds.json`.
|
|
10
|
+
|
|
11
|
+
Technology stack: Python 3.9 or newer, standard library only at runtime, Hatchling for the build, pytest for tests, Ruff for lint and format, GitHub Actions for CI and publishing.
|
|
12
|
+
|
|
13
|
+
Key behaviours that must not regress:
|
|
14
|
+
|
|
15
|
+
- Gallery pages answer 404 for `/gallery/{id}` with no slug, so requests use `/gallery/{id}/gallery`.
|
|
16
|
+
- Behance answers 403 with a cookie challenge. The client sets `js_challenge_value` and retries once with a fresh `Request`, because a reused request keeps the stale `Cookie` header.
|
|
17
|
+
- Variant ranking prefers `source`, then a plain bucket, then an `_opt_` copy, and drops `_still` buckets whenever an animated variant exists. A still bucket holds only the first frame of an animated image.
|
|
18
|
+
- Only `https` URLs on `behance.net` or a `.behance.net` subdomain may be requested. The page payload is untrusted input.
|
|
19
|
+
- The CDN sometimes serves JPEG bytes under a `.png` name, so the saved extension comes from the file's magic bytes.
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- Python 3.9 or newer. The floor exists because `ThreadPoolExecutor.shutdown(cancel_futures=True)` requires 3.9.
|
|
24
|
+
- `pip` and `venv` from the same interpreter.
|
|
25
|
+
- No external services, credentials, or API keys. Behance is accessed anonymously.
|
|
26
|
+
- Windows is the primary development platform, and CI also runs Linux and macOS.
|
|
27
|
+
|
|
28
|
+
## Core Philosophy
|
|
29
|
+
|
|
30
|
+
### Trade-off Priority (when conflicts arise)
|
|
31
|
+
|
|
32
|
+
1. **Correctness**: the download matches the gallery and the record stays truthful.
|
|
33
|
+
2. **Simplicity and readability**: one small function per job, standard library first.
|
|
34
|
+
3. **Testability**: network access sits behind `BehanceClient`, so tests use fakes.
|
|
35
|
+
4. **Performance**: parallel downloads and streaming transfers, without speculative optimisation.
|
|
36
|
+
5. **Abstraction and reuse**: extract only when duplication is already costing more than a shared helper.
|
|
37
|
+
|
|
38
|
+
### Ground Rules
|
|
39
|
+
|
|
40
|
+
- Read and understand the existing module before changing it, and preserve the behaviours listed in Project Overview.
|
|
41
|
+
- If a requirement is ambiguous, ask before writing code.
|
|
42
|
+
- Do not overengineer. Build for today's requirements.
|
|
43
|
+
- A change to `BehanceClient`, the payload parsing, or the manifest format affects every command, so verify the touched paths end to end.
|
|
44
|
+
|
|
45
|
+
## Design Principles
|
|
46
|
+
|
|
47
|
+
### SOLID
|
|
48
|
+
|
|
49
|
+
| Principle | Rule | Common Mistake |
|
|
50
|
+
|-----------|------|----------------|
|
|
51
|
+
| **SRP** (Single Responsibility) | Each module has one axis of change. `client` speaks HTTP, `gallery` parses payloads, `selection` ranks variants, `storage` writes files, `cli` parses arguments and reports. | Putting reporting inside the save step, or network calls inside selection. |
|
|
52
|
+
| **OCP** (Open/Closed) | Add a new module type by extending the dispatch in `gallery.image_sources` and `gallery.embed_assets`. | Scattering module-type checks across several functions. |
|
|
53
|
+
| **LSP** (Liskov Substitution) | A fake client used in tests must honour the same contract as `BehanceClient`. | A fake that returns bodies the real client would reject. |
|
|
54
|
+
| **ISP** (Interface Segregation) | Fakes implement only `get_text` and `download`, which is all the planning and saving code uses. | A test double exposing unrelated helpers. |
|
|
55
|
+
| **DIP** (Dependency Inversion) | `plan_gallery` and `save_gallery` accept a client object rather than constructing one. | Constructing `BehanceClient` deep inside planning code. |
|
|
56
|
+
|
|
57
|
+
### DRY, KISS, YAGNI
|
|
58
|
+
|
|
59
|
+
- One rule lives in one place. The Behance host rule lives in `gallery.is_behance_host`, and the file-name rule lives in `naming.safe_name`.
|
|
60
|
+
- Prefer duplication over a wrong abstraction. Two similar-looking expressions with different purposes stay separate.
|
|
61
|
+
- Standard library before any dependency. Runtime dependencies stay at zero unless a feature genuinely needs one.
|
|
62
|
+
- No speculative options, flags, or abstractions.
|
|
63
|
+
|
|
64
|
+
## Language and Runtime Constraints
|
|
65
|
+
|
|
66
|
+
The package supports Python 3.9, so these are unavailable at runtime:
|
|
67
|
+
|
|
68
|
+
- `match`/`case` and PEP 604 unions in evaluated positions (3.10+).
|
|
69
|
+
- Builtin generics in evaluated positions, such as `list[int]` (3.9+). Every module uses `from __future__ import annotations`, so annotations may use them, but runtime expressions may not.
|
|
70
|
+
- `datetime.UTC`, `itertools.batched`, and `tomllib` (3.11+).
|
|
71
|
+
- Exception groups (3.11+).
|
|
72
|
+
|
|
73
|
+
Keep `from __future__ import annotations` at the top of every module.
|
|
74
|
+
|
|
75
|
+
## Code Quality
|
|
76
|
+
|
|
77
|
+
### Linting and Formatting
|
|
78
|
+
|
|
79
|
+
- Ruff handles both linting and formatting, configured in `pyproject.toml` at line length 100.
|
|
80
|
+
- After changing files, run `ruff check .` and `ruff format .`. Both must pass before a change is complete.
|
|
81
|
+
- Fix a violation before configuring or suppressing it. Use a per-line `noqa` only for a genuine false positive, and never a blanket suppression.
|
|
82
|
+
- `tests/conftest.py` is the shared test double module, registered as first-party for import sorting.
|
|
83
|
+
|
|
84
|
+
### Naming
|
|
85
|
+
|
|
86
|
+
- Functions start with a verb, classes are nouns, and booleans read as a yes/no question.
|
|
87
|
+
- No generic names such as `data`, `result`, `obj`, `thing`, `temp`, or `utils`.
|
|
88
|
+
- No abbreviations beyond universally understood ones. `url` inside a tuple unpacking may stay lowercase when it names the element, and prose uses `URL`.
|
|
89
|
+
- One concept keeps one name across modules. A saved image is a `SavedImage` in the record, an `ImageAsset` when planned, and a `StoredImage` when written.
|
|
90
|
+
|
|
91
|
+
### Strong Typing
|
|
92
|
+
|
|
93
|
+
- Annotate every public function's parameters and return value.
|
|
94
|
+
- Avoid `Any` except where a JSON payload is genuinely untyped, and narrow it immediately with `isinstance` checks.
|
|
95
|
+
- Do not cast to `Any` to make an error disappear.
|
|
96
|
+
|
|
97
|
+
### Immutability
|
|
98
|
+
|
|
99
|
+
- Dataclasses are frozen. Return new objects instead of mutating inputs.
|
|
100
|
+
- Never expose a mutable internal collection. `GalleryProject.modules` is a tuple.
|
|
101
|
+
|
|
102
|
+
### Early Returns and Guard Clauses
|
|
103
|
+
|
|
104
|
+
- Put the happy path at the lowest nesting level and return early for the failure case.
|
|
105
|
+
- Repeated numbers and strings become named constants, such as `RETRYABLE_STATUS` and `TRANSFER_BLOCK_SIZE`.
|
|
106
|
+
|
|
107
|
+
### Comments and Docstrings
|
|
108
|
+
|
|
109
|
+
- Write a comment only when it explains why, never what.
|
|
110
|
+
- A docstring states the unit's purpose without restating its signature or parameters.
|
|
111
|
+
- Comments and docstrings must make sense to a cold reader. No references to reviews, tickets, or conversations.
|
|
112
|
+
- Never use em-dashes or en-dashes, and never use a hyphen as a clause separator. End the sentence or use a comma.
|
|
113
|
+
|
|
114
|
+
### Functions
|
|
115
|
+
|
|
116
|
+
- One job per function. No boolean parameter that switches behaviour.
|
|
117
|
+
- Pass a callback rather than a flag when the caller needs to react to per-item results, as `download_images` does with `on_saved`.
|
|
118
|
+
|
|
119
|
+
### Output Conventions
|
|
120
|
+
|
|
121
|
+
- Progress and plans go to standard output. Warnings and errors go to standard error.
|
|
122
|
+
- Exit codes: `0` success, `1` a gallery failed, `2` invalid arguments, `130` interrupted.
|
|
123
|
+
- Never log or print credentials, and there are none to print.
|
|
124
|
+
|
|
125
|
+
## Architecture
|
|
126
|
+
|
|
127
|
+
### Module Layout
|
|
128
|
+
|
|
129
|
+
| Module | Responsibility |
|
|
130
|
+
|--------|----------------|
|
|
131
|
+
| `client.py` | HTTP session, cookie challenge, retries, atomic image transfers. |
|
|
132
|
+
| `gallery.py` | Behance host and URL rules, gallery id parsing, payload parsing, module walking. |
|
|
133
|
+
| `selection.py` | Bucket ranking, variant selection, building a `GalleryPlan`. |
|
|
134
|
+
| `storage.py` | Manifest reading and writing, parallel downloads, metadata files. |
|
|
135
|
+
| `models.py` | Frozen dataclasses shared across modules. |
|
|
136
|
+
| `naming.py` | File-name safety rules. |
|
|
137
|
+
| `errors.py` | `BehanceError`, `GalleryError`, `DownloadError`. |
|
|
138
|
+
| `cli.py` | Argument parsing, reporting, exit codes. |
|
|
139
|
+
|
|
140
|
+
### Dependency Direction
|
|
141
|
+
|
|
142
|
+
`cli` depends on `selection` and `storage`. `selection` depends on `gallery` and `models`. `storage` depends on `client` and `models`. `gallery` depends on `client` and `models`. `models` and `naming` depend on nothing local. No module imports `cli`, and no cycles are allowed.
|
|
143
|
+
|
|
144
|
+
### Layer Rules
|
|
145
|
+
|
|
146
|
+
- Network access happens only in `client.py`.
|
|
147
|
+
- Filesystem writes happen only in `storage.py`.
|
|
148
|
+
- Selection works on plain payload dictionaries and never performs IO.
|
|
149
|
+
- Errors cross module boundaries as `BehanceError` subclasses, and `cli.main` is the single place that converts them to exit codes.
|
|
150
|
+
|
|
151
|
+
## Error Handling
|
|
152
|
+
|
|
153
|
+
- Define domain errors in `errors.py`. Do not raise bare `Exception` or `RuntimeError` directly.
|
|
154
|
+
- Chain re-raises explicitly with `from error` to preserve the cause.
|
|
155
|
+
- Convert untrusted payload shapes into `GalleryError` with enough context to identify the gallery.
|
|
156
|
+
- A single failed image must not abort a gallery, and a single failed gallery must not abort a batch.
|
|
157
|
+
- An interruption must leave the record truthful, so record each image as soon as it lands.
|
|
158
|
+
- Never fail a run because cleanup or bookkeeping failed. Warn and continue.
|
|
159
|
+
|
|
160
|
+
## Security
|
|
161
|
+
|
|
162
|
+
### Input Validation and Injection
|
|
163
|
+
|
|
164
|
+
- Treat every value in the page payload as untrusted.
|
|
165
|
+
- Only `https` URLs on `behance.net` or a `.behance.net` subdomain may be requested. Enforce it in `gallery.is_behance_url` and never bypass it.
|
|
166
|
+
- Never build a file path from raw remote text. Pass names through `naming.safe_name`, and validate recorded names with `naming.is_plain_file_name`.
|
|
167
|
+
- A recorded path must stay inside the gallery folder and share the planned stem.
|
|
168
|
+
- Reject a response whose content type is not `image/*`, or whose bytes match no known image magic.
|
|
169
|
+
|
|
170
|
+
### Secrets Management
|
|
171
|
+
|
|
172
|
+
- Never commit credentials, tokens, or API keys.
|
|
173
|
+
- Never place a secret in a workflow file, a test fixture, or a log line.
|
|
174
|
+
|
|
175
|
+
### Dependencies and Supply Chain
|
|
176
|
+
|
|
177
|
+
- Runtime dependencies stay at zero.
|
|
178
|
+
- Development dependencies are pinned by floor in `pyproject.toml` and resolved in CI.
|
|
179
|
+
- Do not add a dependency with a known vulnerability that affects this code.
|
|
180
|
+
|
|
181
|
+
## Testing
|
|
182
|
+
|
|
183
|
+
> **Test code is production code.** It receives the same care, review, and quality standards.
|
|
184
|
+
|
|
185
|
+
### Core Principles
|
|
186
|
+
|
|
187
|
+
- Tests must never touch the network. Use the fakes in `tests/conftest.py`, or stub `_opener` on `BehanceClient`.
|
|
188
|
+
- Follow Arrange-Act-Assert, with one act and one logical assertion per test.
|
|
189
|
+
- Tests must be independent, deterministic, and order-agnostic.
|
|
190
|
+
- Pin specific exception types in `pytest.raises`.
|
|
191
|
+
- Test behaviour through public functions. Stubbing `client._opener` is the accepted exception, because the cookie jar has no other seam.
|
|
192
|
+
- Cover the happy path and adversarial cases: hostile URLs, malformed payloads, unreadable manifests, truncated transfers, and locked files.
|
|
193
|
+
- Add a regression test for every bug fixed.
|
|
194
|
+
|
|
195
|
+
### How to Run Tests
|
|
196
|
+
|
|
197
|
+
```powershell
|
|
198
|
+
python -m venv .venv
|
|
199
|
+
.venv\Scripts\activate
|
|
200
|
+
pip install -e ".[dev]"
|
|
201
|
+
pytest
|
|
202
|
+
ruff check .
|
|
203
|
+
ruff format --check .
|
|
204
|
+
python -m build
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
On Linux or macOS, activate with `source .venv/bin/activate` and call the tools as `python -m pytest`, `python -m ruff`, and `python -m build`.
|
|
208
|
+
|
|
209
|
+
### Test Naming
|
|
210
|
+
|
|
211
|
+
- pytest collects `test_` functions. Use `test_[subject]_[aspect]` as the default, for example `test_still_bucket_dropped_when_animated_variant_exists`.
|
|
212
|
+
- Use `test_[behaviour]` only when splitting into subject and aspect would be artificial.
|
|
213
|
+
- Do not encode assertion types or fixture names in a test name.
|
|
214
|
+
|
|
215
|
+
### Test File Rules
|
|
216
|
+
|
|
217
|
+
- One file per module under test: `test_client.py`, `test_gallery.py`, `test_selection.py`, `test_storage.py`, `test_naming.py`, `test_cli.py`.
|
|
218
|
+
- Shared doubles and payload builders live in `tests/conftest.py`, and test-local helpers stay in the test module that uses them.
|
|
219
|
+
|
|
220
|
+
### Coverage
|
|
221
|
+
|
|
222
|
+
- **Minimum acceptable: 85%**, measured with branch coverage.
|
|
223
|
+
- Run `pytest --cov=behance_dl --cov-report=term-missing` and report the result for the touched modules.
|
|
224
|
+
|
|
225
|
+
### All Created Tests MUST Pass
|
|
226
|
+
|
|
227
|
+
- Every test you create or modify must pass, with zero failures and zero skips.
|
|
228
|
+
- Never disable, skip, or delete a test to hide a failure. Fix the code first when the code is wrong.
|
|
229
|
+
- When a test fails, decide whether the code or the test is wrong before editing either.
|
|
230
|
+
|
|
231
|
+
### Anti-Patterns (Forbidden)
|
|
232
|
+
|
|
233
|
+
- A test that asserts nothing.
|
|
234
|
+
- A test that mocks the unit under test.
|
|
235
|
+
- A test that depends on the order of other tests.
|
|
236
|
+
- A fake that returns data the real client would reject.
|
|
237
|
+
|
|
238
|
+
## Versioning
|
|
239
|
+
|
|
240
|
+
- The version lives in `src/behance_dl/__init__.py` and Hatchling reads it from there, so bump it in one place.
|
|
241
|
+
- Every release must come from a working tree with passing tests, passing lint, and no uncommitted changes.
|
|
242
|
+
|
|
243
|
+
## Rule of Thumb
|
|
244
|
+
|
|
245
|
+
**When in doubt: correctness over cleverness, one honest record over a silent skip, and a clear error over a wrong file on disk.**
|
behance_dl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Elian Kevin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: behance-dl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Download the images of public Behance project galleries.
|
|
5
|
+
Project-URL: Homepage, https://github.com/kevinkwee/behance-dl
|
|
6
|
+
Project-URL: Repository, https://github.com/kevinkwee/behance-dl
|
|
7
|
+
Project-URL: Issues, https://github.com/kevinkwee/behance-dl/issues
|
|
8
|
+
Project-URL: Releases, https://github.com/kevinkwee/behance-dl/releases
|
|
9
|
+
Author-email: Michael Elian Kevin <19345886+kevinkwee@users.noreply.github.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: behance,cli,downloader,gallery,portfolio
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# behance-dl
|
|
35
|
+
|
|
36
|
+
Download the images of a public [Behance](https://www.behance.net) project gallery.
|
|
37
|
+
|
|
38
|
+
`behance-dl` reads the project data Behance inlines in the gallery page, picks the best CDN
|
|
39
|
+
variant of every image, and downloads them in parallel with resume support. It keeps the
|
|
40
|
+
gallery order, records which variant produced each file, and logs the content it cannot
|
|
41
|
+
download as files (prototypes, video players) so nothing is silently lost.
|
|
42
|
+
|
|
43
|
+
No API key, account, or browser automation is required.
|
|
44
|
+
|
|
45
|
+
> Not affiliated with Behance or Adobe. Use it for offline viewing of published work, and
|
|
46
|
+
> respect each creator's licence before redistributing anything.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```console
|
|
51
|
+
pip install behance-dl
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requires Python 3.9 or newer. No third-party runtime dependencies.
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
behance-dl "https://www.behance.net/gallery/123456789/Sample-Gallery"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Every gallery argument can be a full Behance URL, with or without query parameters, or a
|
|
63
|
+
bare project id:
|
|
64
|
+
|
|
65
|
+
```console
|
|
66
|
+
behance-dl 123456789
|
|
67
|
+
behance-dl "https://www.behance.net/gallery/123456789/Sample-Gallery?l=22"
|
|
68
|
+
behance-dl 123456789 234567890 345678901 -o downloads
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Options
|
|
72
|
+
|
|
73
|
+
| Option | Meaning |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `-o`, `--output DIR` | Folder that receives one subfolder per gallery. Default: current folder. |
|
|
76
|
+
| `-W`, `--max-width PX` | Largest image at or below this width, else the smallest available. Default: the largest available. |
|
|
77
|
+
| `-j`, `--workers N` | Parallel downloads. Default: 4. |
|
|
78
|
+
| `--attempts N` | Attempts per request before giving up. Default: 3. |
|
|
79
|
+
| `--timeout SECONDS` | Per-request timeout. Default: 30. |
|
|
80
|
+
| `--force` | Ignore the saved record and fetch every image again. |
|
|
81
|
+
| `--dry-run` | List what would be downloaded without writing anything. |
|
|
82
|
+
| `--version` | Print the version and exit. |
|
|
83
|
+
|
|
84
|
+
### What a gallery folder contains
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
123456789_Sample-Gallery/
|
|
88
|
+
001_0a1b2c3123456789.5f6d7e8f9a0b1.png
|
|
89
|
+
002_4d5e6f7123456789.5f6d7e9a1c2d3.png
|
|
90
|
+
003_8a9b0c1123456789.5f6d7eab2e3f4.jpg
|
|
91
|
+
embeds.json # prototypes and video players that are links, not files
|
|
92
|
+
manifest.json # which CDN variant produced each saved file
|
|
93
|
+
project.json # the full project payload Behance served
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Files are prefixed with their position in the gallery, so a plain alphabetical listing
|
|
97
|
+
follows the project order. The extension is taken from the file's magic bytes, because
|
|
98
|
+
Behance's CDN sometimes serves JPEG data under a `.png` name.
|
|
99
|
+
|
|
100
|
+
`embeds.json` records modules that cannot be downloaded as images, such as Figma prototypes
|
|
101
|
+
and Adobe video players, with their source URL, caption and dimensions.
|
|
102
|
+
|
|
103
|
+
## Re-running, resume and width changes
|
|
104
|
+
|
|
105
|
+
`manifest.json` records the URL each file came from. A re-run only fetches what is missing,
|
|
106
|
+
and a run with a different `-W` re-fetches the affected images instead of leaving files at
|
|
107
|
+
the old size. An interrupted run keeps whatever already landed, so `Ctrl+C` followed by the
|
|
108
|
+
same command continues where it stopped.
|
|
109
|
+
|
|
110
|
+
Delete `manifest.json` (or pass `--force`) to fetch everything again.
|
|
111
|
+
|
|
112
|
+
## How it works
|
|
113
|
+
|
|
114
|
+
1. `GET https://www.behance.net/gallery/{id}/gallery`. Behance answers 404 for an id with no
|
|
115
|
+
trailing slug and ignores the slug itself, so any slug works.
|
|
116
|
+
2. Behance answers 403 with a cookie challenge
|
|
117
|
+
(`document.cookie = "js_challenge_value=..."; location.reload()`). The client sets the
|
|
118
|
+
cookie and retries once. This is not a CAPTCHA and needs no credentials.
|
|
119
|
+
3. The project payload is read from `<script id="beconfig-store_state">`. It lists every
|
|
120
|
+
module and every CDN variant of each image with its pixel width.
|
|
121
|
+
4. Variants are filtered and ranked: WebP copies are skipped so the saved file matches the
|
|
122
|
+
original format, `_still` buckets are dropped when an animated variant exists (a still
|
|
123
|
+
bucket holds only the first frame of a GIF), and equal widths prefer `source` over a
|
|
124
|
+
plain bucket over an `_opt_` copy.
|
|
125
|
+
5. Images are downloaded in a thread pool, each to a `.part` file that is verified against
|
|
126
|
+
`Content-Length`, checked for an image content type and real magic bytes, then renamed
|
|
127
|
+
into place.
|
|
128
|
+
|
|
129
|
+
`MediaCollectionModule` carousels are expanded into one file per component. Only URLs on
|
|
130
|
+
`https://*.behance.net` are ever requested, so a tampered payload cannot point the tool at
|
|
131
|
+
a local file or a third-party host.
|
|
132
|
+
|
|
133
|
+
## Limitations
|
|
134
|
+
|
|
135
|
+
- Only public projects. Private, removed, or login-gated projects report a clear error.
|
|
136
|
+
- Images only. Prototypes and video modules are recorded as links in `embeds.json`.
|
|
137
|
+
- Behance can change its page format at any time; the payload shape is checked and a
|
|
138
|
+
mismatch is reported as an error instead of producing an empty folder.
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
```console
|
|
143
|
+
python -m venv .venv
|
|
144
|
+
.venv\Scripts\activate # Windows
|
|
145
|
+
pip install -e ".[dev]"
|
|
146
|
+
pytest
|
|
147
|
+
ruff check .
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# behance-dl
|
|
2
|
+
|
|
3
|
+
Download the images of a public [Behance](https://www.behance.net) project gallery.
|
|
4
|
+
|
|
5
|
+
`behance-dl` reads the project data Behance inlines in the gallery page, picks the best CDN
|
|
6
|
+
variant of every image, and downloads them in parallel with resume support. It keeps the
|
|
7
|
+
gallery order, records which variant produced each file, and logs the content it cannot
|
|
8
|
+
download as files (prototypes, video players) so nothing is silently lost.
|
|
9
|
+
|
|
10
|
+
No API key, account, or browser automation is required.
|
|
11
|
+
|
|
12
|
+
> Not affiliated with Behance or Adobe. Use it for offline viewing of published work, and
|
|
13
|
+
> respect each creator's licence before redistributing anything.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```console
|
|
18
|
+
pip install behance-dl
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.9 or newer. No third-party runtime dependencies.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```console
|
|
26
|
+
behance-dl "https://www.behance.net/gallery/123456789/Sample-Gallery"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Every gallery argument can be a full Behance URL, with or without query parameters, or a
|
|
30
|
+
bare project id:
|
|
31
|
+
|
|
32
|
+
```console
|
|
33
|
+
behance-dl 123456789
|
|
34
|
+
behance-dl "https://www.behance.net/gallery/123456789/Sample-Gallery?l=22"
|
|
35
|
+
behance-dl 123456789 234567890 345678901 -o downloads
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Options
|
|
39
|
+
|
|
40
|
+
| Option | Meaning |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `-o`, `--output DIR` | Folder that receives one subfolder per gallery. Default: current folder. |
|
|
43
|
+
| `-W`, `--max-width PX` | Largest image at or below this width, else the smallest available. Default: the largest available. |
|
|
44
|
+
| `-j`, `--workers N` | Parallel downloads. Default: 4. |
|
|
45
|
+
| `--attempts N` | Attempts per request before giving up. Default: 3. |
|
|
46
|
+
| `--timeout SECONDS` | Per-request timeout. Default: 30. |
|
|
47
|
+
| `--force` | Ignore the saved record and fetch every image again. |
|
|
48
|
+
| `--dry-run` | List what would be downloaded without writing anything. |
|
|
49
|
+
| `--version` | Print the version and exit. |
|
|
50
|
+
|
|
51
|
+
### What a gallery folder contains
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
123456789_Sample-Gallery/
|
|
55
|
+
001_0a1b2c3123456789.5f6d7e8f9a0b1.png
|
|
56
|
+
002_4d5e6f7123456789.5f6d7e9a1c2d3.png
|
|
57
|
+
003_8a9b0c1123456789.5f6d7eab2e3f4.jpg
|
|
58
|
+
embeds.json # prototypes and video players that are links, not files
|
|
59
|
+
manifest.json # which CDN variant produced each saved file
|
|
60
|
+
project.json # the full project payload Behance served
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Files are prefixed with their position in the gallery, so a plain alphabetical listing
|
|
64
|
+
follows the project order. The extension is taken from the file's magic bytes, because
|
|
65
|
+
Behance's CDN sometimes serves JPEG data under a `.png` name.
|
|
66
|
+
|
|
67
|
+
`embeds.json` records modules that cannot be downloaded as images, such as Figma prototypes
|
|
68
|
+
and Adobe video players, with their source URL, caption and dimensions.
|
|
69
|
+
|
|
70
|
+
## Re-running, resume and width changes
|
|
71
|
+
|
|
72
|
+
`manifest.json` records the URL each file came from. A re-run only fetches what is missing,
|
|
73
|
+
and a run with a different `-W` re-fetches the affected images instead of leaving files at
|
|
74
|
+
the old size. An interrupted run keeps whatever already landed, so `Ctrl+C` followed by the
|
|
75
|
+
same command continues where it stopped.
|
|
76
|
+
|
|
77
|
+
Delete `manifest.json` (or pass `--force`) to fetch everything again.
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
1. `GET https://www.behance.net/gallery/{id}/gallery`. Behance answers 404 for an id with no
|
|
82
|
+
trailing slug and ignores the slug itself, so any slug works.
|
|
83
|
+
2. Behance answers 403 with a cookie challenge
|
|
84
|
+
(`document.cookie = "js_challenge_value=..."; location.reload()`). The client sets the
|
|
85
|
+
cookie and retries once. This is not a CAPTCHA and needs no credentials.
|
|
86
|
+
3. The project payload is read from `<script id="beconfig-store_state">`. It lists every
|
|
87
|
+
module and every CDN variant of each image with its pixel width.
|
|
88
|
+
4. Variants are filtered and ranked: WebP copies are skipped so the saved file matches the
|
|
89
|
+
original format, `_still` buckets are dropped when an animated variant exists (a still
|
|
90
|
+
bucket holds only the first frame of a GIF), and equal widths prefer `source` over a
|
|
91
|
+
plain bucket over an `_opt_` copy.
|
|
92
|
+
5. Images are downloaded in a thread pool, each to a `.part` file that is verified against
|
|
93
|
+
`Content-Length`, checked for an image content type and real magic bytes, then renamed
|
|
94
|
+
into place.
|
|
95
|
+
|
|
96
|
+
`MediaCollectionModule` carousels are expanded into one file per component. Only URLs on
|
|
97
|
+
`https://*.behance.net` are ever requested, so a tampered payload cannot point the tool at
|
|
98
|
+
a local file or a third-party host.
|
|
99
|
+
|
|
100
|
+
## Limitations
|
|
101
|
+
|
|
102
|
+
- Only public projects. Private, removed, or login-gated projects report a clear error.
|
|
103
|
+
- Images only. Prototypes and video modules are recorded as links in `embeds.json`.
|
|
104
|
+
- Behance can change its page format at any time; the payload shape is checked and a
|
|
105
|
+
mismatch is reported as an error instead of producing an empty folder.
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```console
|
|
110
|
+
python -m venv .venv
|
|
111
|
+
.venv\Scripts\activate # Windows
|
|
112
|
+
pip install -e ".[dev]"
|
|
113
|
+
pytest
|
|
114
|
+
ruff check .
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT. See [LICENSE](LICENSE).
|