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.
@@ -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.**
@@ -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).