devart-dl 4.3.0__tar.gz → 4.3.2__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.
- {devart_dl-4.3.0/devart_dl.egg-info → devart_dl-4.3.2}/PKG-INFO +16 -5
- devart_dl-4.3.2/README.en.md +87 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/README.md +15 -4
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/cli.py +1 -1
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/official_api.py +56 -16
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/provider.py +9 -1
- {devart_dl-4.3.0 → devart_dl-4.3.2/devart_dl.egg-info}/PKG-INFO +16 -5
- {devart_dl-4.3.0 → devart_dl-4.3.2}/pyproject.toml +1 -1
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_official_provider.py +122 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_provider.py +14 -0
- devart_dl-4.3.0/README.en.md +0 -289
- {devart_dl-4.3.0 → devart_dl-4.3.2}/.env.example +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/LICENSE +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/MANIFEST.in +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/bin/devart-dl +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/bin/install.sh +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/__init__.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/api.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/archive.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/auth.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/da_cookie_login.mjs +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/download.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/errors.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/http.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/manager.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/models.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/oauth.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/path.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/da_downloader/targets.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/devart_dl.egg-info/SOURCES.txt +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/devart_dl.egg-info/dependency_links.txt +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/devart_dl.egg-info/entry_points.txt +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/devart_dl.egg-info/requires.txt +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/devart_dl.egg-info/top_level.txt +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/requirements.txt +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/setup.cfg +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/setup.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_api.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_archive.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_cli.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_conformance.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_download.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_http_downloader.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_manager.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_path.py +0 -0
- {devart_dl-4.3.0 → devart_dl-4.3.2}/tests/test_targets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devart-dl
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.2
|
|
4
4
|
Summary: 专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features
|
|
5
5
|
Author: DeviantArt Downloader Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -34,9 +34,11 @@ Dynamic: license-file
|
|
|
34
34
|
|
|
35
35
|
# DeviantArt Downloader
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**语言 / Language:** 中文 · [English](README.en.md)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
> **一个可靠、聚焦的 DeviantArt 下载 / 归档命令行工具。**
|
|
40
|
+
|
|
41
|
+
[完整文档](https://redtidev1918.github.io/deviantart-downloader/) · [更新日志](CHANGELOG.md)
|
|
40
42
|
|
|
41
43
|
[](https://github.com/redtidev1918/deviantart-downloader/actions/workflows/ci.yml)
|
|
42
44
|
[](https://pypi.org/project/devart-dl/)
|
|
@@ -93,9 +95,18 @@ devart-dl login browser # 打开你的 Chrome 登录,自动保存 Cookie,
|
|
|
93
95
|
- **代理与降级**:支持 `--proxy`;未登录 OAuth 时自动回退 Cookie。
|
|
94
96
|
- **Resolve 模式**:`devart-dl resolve URL` 输出 `ResolvedDeviation`/`MediaAsset` 的 JSON 契约,不写文件。
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
## 文档
|
|
99
|
+
|
|
100
|
+
| 你想做什么 | 文档 |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| 下载安装包 | [下载页](https://redtidev1918.github.io/deviantart-downloader/download.md) |
|
|
103
|
+
| 登录与认证 | [登录与认证](docs/LOGIN.md) |
|
|
104
|
+
| 命令、配置与 Python 调用 | [文档站](https://redtidev1918.github.io/deviantart-downloader/) |
|
|
105
|
+
|
|
106
|
+
## 致谢
|
|
97
107
|
|
|
98
|
-
https://
|
|
108
|
+
- [requests](https://github.com/psf/requests):HTTP 客户端。
|
|
109
|
+
- 登录与归档目录的实现与 [gallery-dl](https://github.com/mikf/gallery-dl) 交叉核对过。
|
|
99
110
|
|
|
100
111
|
## 许可证与免责声明
|
|
101
112
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# DeviantArt Downloader
|
|
2
|
+
|
|
3
|
+
**Language / 语言:** [中文](README.md) · English
|
|
4
|
+
|
|
5
|
+
> **A reliable, focused DeviantArt downloader and archival CLI.**
|
|
6
|
+
|
|
7
|
+
📖 [Full documentation](https://redtidev1918.github.io/deviantart-downloader/) · [Changelog](CHANGELOG.md)
|
|
8
|
+
|
|
9
|
+
[](https://github.com/redtidev1918/deviantart-downloader/actions/workflows/ci.yml)
|
|
10
|
+
[](https://pypi.org/project/devart-dl/)
|
|
11
|
+
[](https://pypi.org/project/devart-dl/)
|
|
12
|
+
[](https://pypi.org/project/devart-dl/)
|
|
13
|
+
[](LICENSE)
|
|
14
|
+
|
|
15
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Core Features
|
|
20
|
+
|
|
21
|
+
- **Official API first (OAuth)** — log in once to download originals; no cookie export, no anti-ban delays.
|
|
22
|
+
- **URL-first** — `devart-dl URL` downloads artworks / galleries / favourites / tags / fav.me links directly.
|
|
23
|
+
- **Reliable downloads** — HTTP Range resume, automatic retry, 429 backoff, HTML/empty-file validation, atomic finalize.
|
|
24
|
+
- **Download archive** — `--archive` remembers what you've downloaded in SQLite, skipping it across sessions.
|
|
25
|
+
- **Path templates** — `--directory` / `--filename` support `{id}` `{title}` `{author}` `{published}` `{ext}` and more, with safe sanitization.
|
|
26
|
+
- **Metadata sidecars** — `--write-info-json` writes a `.json` next to each file.
|
|
27
|
+
- **Proxy & fallback** — `--proxy` or proxy environment variables; falls back to cookies when not logged in via OAuth.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### Installation
|
|
34
|
+
|
|
35
|
+
Requires Python 3.10 or newer.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install devart-dl
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Basic Usage (URL-first)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Single artwork (also fav.me links and bare artwork ids)
|
|
45
|
+
devart-dl https://www.deviantart.com/username/art/title-123456
|
|
46
|
+
|
|
47
|
+
# Gallery / all works by an artist
|
|
48
|
+
devart-dl https://www.deviantart.com/username/gallery
|
|
49
|
+
|
|
50
|
+
# Favourites
|
|
51
|
+
devart-dl https://www.deviantart.com/username/favourites
|
|
52
|
+
|
|
53
|
+
# Tag
|
|
54
|
+
devart-dl https://www.deviantart.com/tag/landscape
|
|
55
|
+
|
|
56
|
+
# Resolve to JSON descriptors (download nothing)
|
|
57
|
+
devart-dl resolve https://www.deviantart.com/username/art/title-123456
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Login (OAuth recommended)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
devart-dl login oauth --client-id YOUR_PUBLIC_CLIENT_ID
|
|
64
|
+
devart-dl whoami # verify login
|
|
65
|
+
devart-dl logout # revoke the token
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Register a **Public** OAuth app at [deviantart.com/developers](https://www.deviantart.com/developers/) and whitelist `http://127.0.0.1:8765/callback`. Login happens in the browser — no password or `client_secret` is ever given to the CLI.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Documentation
|
|
73
|
+
|
|
74
|
+
The full install, sign-in, download modes, configuration, command reference, Python API and FAQ live on the [docs site](https://redtidev1918.github.io/deviantart-downloader/).
|
|
75
|
+
|
|
76
|
+
## Acknowledgements
|
|
77
|
+
|
|
78
|
+
- [requests](https://github.com/psf/requests) — the HTTP client.
|
|
79
|
+
- DevTools-free login and archive layout were cross-checked against [gallery-dl](https://github.com/mikf/gallery-dl).
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT License — see [LICENSE](LICENSE).
|
|
84
|
+
|
|
85
|
+
**Important disclaimer:** this tool is for personal learning and research only.
|
|
86
|
+
Respect DeviantArt's terms of service and copyright. Do not use it commercially
|
|
87
|
+
or to overload their servers. The author is not responsible for any misuse.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# DeviantArt Downloader
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**语言 / Language:** 中文 · [English](README.en.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **一个可靠、聚焦的 DeviantArt 下载 / 归档命令行工具。**
|
|
6
|
+
|
|
7
|
+
[完整文档](https://redtidev1918.github.io/deviantart-downloader/) · [更新日志](CHANGELOG.md)
|
|
6
8
|
|
|
7
9
|
[](https://github.com/redtidev1918/deviantart-downloader/actions/workflows/ci.yml)
|
|
8
10
|
[](https://pypi.org/project/devart-dl/)
|
|
@@ -59,9 +61,18 @@ devart-dl login browser # 打开你的 Chrome 登录,自动保存 Cookie,
|
|
|
59
61
|
- **代理与降级**:支持 `--proxy`;未登录 OAuth 时自动回退 Cookie。
|
|
60
62
|
- **Resolve 模式**:`devart-dl resolve URL` 输出 `ResolvedDeviation`/`MediaAsset` 的 JSON 契约,不写文件。
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
## 文档
|
|
65
|
+
|
|
66
|
+
| 你想做什么 | 文档 |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| 下载安装包 | [下载页](https://redtidev1918.github.io/deviantart-downloader/download.md) |
|
|
69
|
+
| 登录与认证 | [登录与认证](docs/LOGIN.md) |
|
|
70
|
+
| 命令、配置与 Python 调用 | [文档站](https://redtidev1918.github.io/deviantart-downloader/) |
|
|
71
|
+
|
|
72
|
+
## 致谢
|
|
63
73
|
|
|
64
|
-
https://
|
|
74
|
+
- [requests](https://github.com/psf/requests):HTTP 客户端。
|
|
75
|
+
- 登录与归档目录的实现与 [gallery-dl](https://github.com/mikf/gallery-dl) 交叉核对过。
|
|
65
76
|
|
|
66
77
|
## 许可证与免责声明
|
|
67
78
|
|
|
@@ -319,27 +319,67 @@ def deviation_uuid(init_data: dict) -> str:
|
|
|
319
319
|
def additional_media_urls(init_data: dict) -> list:
|
|
320
320
|
"""Original-file URLs of a multimedia deviation's extra pages
|
|
321
321
|
(``deviation.extended.additionalMedia``, each entry nests its Wix
|
|
322
|
-
descriptor under ``media``).
|
|
322
|
+
descriptor under ``media``). Mirrors DeviantDrop's pick order: a playable
|
|
323
|
+
video ``types[].b`` (best quality) wins over ``baseUri`` — a video page's
|
|
324
|
+
baseUri is its poster — then the ``baseUri`` file itself, then a
|
|
325
|
+
fullview template. A page whose only representation is a small preview is
|
|
326
|
+
skipped rather than saved as a fake original.
|
|
327
|
+
|
|
328
|
+
Mature pages the server still returns with a ``blur_`` censored image are
|
|
329
|
+
skipped individually; one blocked page does not discard the others.
|
|
323
330
|
"""
|
|
324
|
-
extended = init_data.get("deviation", {}).get("extended") or {}
|
|
325
|
-
entries = extended.get("additionalMedia") or []
|
|
326
331
|
urls: list = []
|
|
327
|
-
for entry in
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
media = entry.get("media")
|
|
332
|
+
for entry in (init_data.get("deviation", {}).get("extended") or {}).get(
|
|
333
|
+
"additionalMedia"
|
|
334
|
+
) or []:
|
|
335
|
+
media = entry.get("media") if isinstance(entry, dict) else None
|
|
331
336
|
if not isinstance(media, dict):
|
|
332
337
|
continue
|
|
333
|
-
|
|
334
|
-
if
|
|
335
|
-
|
|
336
|
-
raw_token = media.get("token")
|
|
337
|
-
token = raw_token[0] if isinstance(raw_token, list) and raw_token else raw_token
|
|
338
|
-
if token:
|
|
339
|
-
urls.append(f"{base}{'&' if '?' in base else '?'}token={token}")
|
|
340
|
-
else:
|
|
341
|
-
urls.append(base)
|
|
338
|
+
url = _descriptor_media_url(media)
|
|
339
|
+
if url and "blur_" not in url:
|
|
340
|
+
urls.append(url)
|
|
342
341
|
return urls
|
|
343
342
|
|
|
344
343
|
|
|
344
|
+
_FILE_EXT_RE = re.compile(
|
|
345
|
+
r"\.(?:png|jpe?g|gif|webp|avif|mp4|m4v|webm|mov|mkv)$", re.IGNORECASE
|
|
346
|
+
)
|
|
347
|
+
_VIDEO_QUALITY_RANK = {"1080p": 4, "720p": 3, "480p": 2, "360p": 1}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _descriptor_media_url(media: dict) -> Optional[str]:
|
|
351
|
+
types = media.get("types")
|
|
352
|
+
types = types if isinstance(types, list) else []
|
|
353
|
+
videos = [
|
|
354
|
+
item
|
|
355
|
+
for item in types
|
|
356
|
+
if isinstance(item, dict) and item.get("t") == "video" and item.get("b")
|
|
357
|
+
]
|
|
358
|
+
if videos:
|
|
359
|
+
best = max(videos, key=lambda item: _VIDEO_QUALITY_RANK.get(item.get("q"), 0))
|
|
360
|
+
return _append_token(str(best["b"]), media.get("token"))
|
|
361
|
+
base = media.get("baseUri")
|
|
362
|
+
if isinstance(base, str) and base and _FILE_EXT_RE.search(base.split("?", 1)[0]):
|
|
363
|
+
return _append_token(base, media.get("token"))
|
|
364
|
+
fullview = next(
|
|
365
|
+
(item for item in types if isinstance(item, dict) and item.get("t") == "fullview"),
|
|
366
|
+
None,
|
|
367
|
+
)
|
|
368
|
+
if fullview:
|
|
369
|
+
if fullview.get("b"):
|
|
370
|
+
return _append_token(str(fullview["b"]), media.get("token"))
|
|
371
|
+
if fullview.get("c") and isinstance(base, str) and base:
|
|
372
|
+
pretty = media.get("prettyName") or "image"
|
|
373
|
+
url = base + str(fullview["c"]).replace("<prettyName>", pretty)
|
|
374
|
+
return _append_token(url, media.get("token"))
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _append_token(url: str, raw_token: Any) -> str:
|
|
379
|
+
token = raw_token[0] if isinstance(raw_token, list) and raw_token else raw_token
|
|
380
|
+
if not token:
|
|
381
|
+
return url
|
|
382
|
+
return f"{url}{'&' if '?' in url else '?'}token={token}"
|
|
383
|
+
|
|
384
|
+
|
|
345
385
|
__all__ = ["OfficialApiClient", "OriginalDownload", "resolve_uuid", "deviation_init", "deviation_uuid", "additional_media_urls"]
|
|
@@ -13,6 +13,7 @@ from typing import Any, Callable, Iterator, Optional, Protocol
|
|
|
13
13
|
|
|
14
14
|
from .api import ActionType, DeviantArtAPI
|
|
15
15
|
from .errors import MediaUnavailableError, ParseError
|
|
16
|
+
from .errors import AuthenticationError, NetworkError
|
|
16
17
|
from .models import Deviation, DownloadItem
|
|
17
18
|
from .official_api import (
|
|
18
19
|
OfficialApiClient,
|
|
@@ -262,6 +263,13 @@ class OfficialProvider:
|
|
|
262
263
|
ext = _ext_from_url(original.filename)
|
|
263
264
|
return original.url, ext
|
|
264
265
|
content = deviation.get("content") or {}
|
|
266
|
+
# `content.src` is a poster for video works; only videos[].src is playable.
|
|
267
|
+
videos = deviation.get("videos") or []
|
|
268
|
+
if isinstance(videos, list):
|
|
269
|
+
for video in videos:
|
|
270
|
+
src = video.get("src") if isinstance(video, dict) else None
|
|
271
|
+
if src:
|
|
272
|
+
return str(src), _ext_from_url(str(src))
|
|
265
273
|
if content.get("src"):
|
|
266
274
|
return str(content["src"]), _ext_from_url(str(content["src"]))
|
|
267
275
|
preview = deviation.get("preview") or {}
|
|
@@ -337,7 +345,7 @@ class CompositeProvider:
|
|
|
337
345
|
if self.official is not None:
|
|
338
346
|
try:
|
|
339
347
|
yield from self.official.resolve(target)
|
|
340
|
-
except MediaUnavailableError:
|
|
348
|
+
except (MediaUnavailableError, NetworkError, AuthenticationError):
|
|
341
349
|
if self.web is not None:
|
|
342
350
|
yield from self.web.resolve(target)
|
|
343
351
|
return
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devart-dl
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.2
|
|
4
4
|
Summary: 专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features
|
|
5
5
|
Author: DeviantArt Downloader Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -34,9 +34,11 @@ Dynamic: license-file
|
|
|
34
34
|
|
|
35
35
|
# DeviantArt Downloader
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**语言 / Language:** 中文 · [English](README.en.md)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
> **一个可靠、聚焦的 DeviantArt 下载 / 归档命令行工具。**
|
|
40
|
+
|
|
41
|
+
[完整文档](https://redtidev1918.github.io/deviantart-downloader/) · [更新日志](CHANGELOG.md)
|
|
40
42
|
|
|
41
43
|
[](https://github.com/redtidev1918/deviantart-downloader/actions/workflows/ci.yml)
|
|
42
44
|
[](https://pypi.org/project/devart-dl/)
|
|
@@ -93,9 +95,18 @@ devart-dl login browser # 打开你的 Chrome 登录,自动保存 Cookie,
|
|
|
93
95
|
- **代理与降级**:支持 `--proxy`;未登录 OAuth 时自动回退 Cookie。
|
|
94
96
|
- **Resolve 模式**:`devart-dl resolve URL` 输出 `ResolvedDeviation`/`MediaAsset` 的 JSON 契约,不写文件。
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
## 文档
|
|
99
|
+
|
|
100
|
+
| 你想做什么 | 文档 |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| 下载安装包 | [下载页](https://redtidev1918.github.io/deviantart-downloader/download.md) |
|
|
103
|
+
| 登录与认证 | [登录与认证](docs/LOGIN.md) |
|
|
104
|
+
| 命令、配置与 Python 调用 | [文档站](https://redtidev1918.github.io/deviantart-downloader/) |
|
|
105
|
+
|
|
106
|
+
## 致谢
|
|
97
107
|
|
|
98
|
-
https://
|
|
108
|
+
- [requests](https://github.com/psf/requests):HTTP 客户端。
|
|
109
|
+
- 登录与归档目录的实现与 [gallery-dl](https://github.com/mikf/gallery-dl) 交叉核对过。
|
|
99
110
|
|
|
100
111
|
## 许可证与免责声明
|
|
101
112
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devart-dl"
|
|
7
|
-
version = "4.3.
|
|
7
|
+
version = "4.3.2"
|
|
8
8
|
description = "专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -104,6 +104,113 @@ def test_artwork_multimedia_yields_every_file(monkeypatch) -> None:
|
|
|
104
104
|
assert items[2].extension == "png"
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
def test_mature_blurred_extra_pages_are_skipped(monkeypatch) -> None:
|
|
108
|
+
init = {
|
|
109
|
+
"deviation": {
|
|
110
|
+
"extended": {
|
|
111
|
+
"deviationUuid": "uuid-1",
|
|
112
|
+
"additionalMedia": [
|
|
113
|
+
{"media": {"baseUri": "https://media.test/blur_page2.jpg", "token": "t"}},
|
|
114
|
+
{"media": {"baseUri": "https://media.test/page3.png", "token": "t"}},
|
|
115
|
+
],
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
monkeypatch.setattr(provider_mod, "deviation_init", lambda identifier, username=None, **_: init)
|
|
120
|
+
client = FakeClient()
|
|
121
|
+
client.deviations["uuid-1"] = make_deviation()
|
|
122
|
+
provider = OfficialProvider(client, quality="f")
|
|
123
|
+
|
|
124
|
+
items = list(provider.resolve(TargetParser.parse("123456")))
|
|
125
|
+
|
|
126
|
+
assert [item.media_url for item in items] == [
|
|
127
|
+
"https://images.test/full.jpg",
|
|
128
|
+
"https://media.test/page3.png?token=t",
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_extra_video_page_uses_playable_source_not_poster(monkeypatch) -> None:
|
|
133
|
+
init = {
|
|
134
|
+
"deviation": {
|
|
135
|
+
"extended": {
|
|
136
|
+
"deviationUuid": "uuid-1",
|
|
137
|
+
"additionalMedia": [
|
|
138
|
+
{"media": {
|
|
139
|
+
"baseUri": "https://media.test/page2.jpg",
|
|
140
|
+
"types": [
|
|
141
|
+
{"t": "video", "q": "480p", "b": "https://videos.test/p2-480.mp4"},
|
|
142
|
+
{"t": "video", "q": "1080p", "b": "https://videos.test/p2-1080.mp4"},
|
|
143
|
+
],
|
|
144
|
+
"token": "t",
|
|
145
|
+
}},
|
|
146
|
+
],
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
monkeypatch.setattr(provider_mod, "deviation_init", lambda identifier, username=None, **_: init)
|
|
151
|
+
client = FakeClient()
|
|
152
|
+
client.deviations["uuid-1"] = make_deviation()
|
|
153
|
+
provider = OfficialProvider(client, quality="f")
|
|
154
|
+
|
|
155
|
+
items = list(provider.resolve(TargetParser.parse("123456")))
|
|
156
|
+
|
|
157
|
+
assert items[1].media_url == "https://videos.test/p2-1080.mp4?token=t"
|
|
158
|
+
assert items[1].extension == "mp4"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_extra_page_uses_fullview_template_when_baseuri_is_not_a_file(monkeypatch) -> None:
|
|
162
|
+
init = {
|
|
163
|
+
"deviation": {
|
|
164
|
+
"extended": {
|
|
165
|
+
"deviationUuid": "uuid-1",
|
|
166
|
+
"additionalMedia": [
|
|
167
|
+
{"media": {
|
|
168
|
+
"baseUri": "https://wixmp.test/hashed",
|
|
169
|
+
"fullview": True,
|
|
170
|
+
"types": [{"t": "fullview", "c": "/f/<prettyName>.jpg"}],
|
|
171
|
+
"prettyName": "page2",
|
|
172
|
+
"token": "t",
|
|
173
|
+
}},
|
|
174
|
+
],
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
monkeypatch.setattr(provider_mod, "deviation_init", lambda identifier, username=None, **_: init)
|
|
179
|
+
client = FakeClient()
|
|
180
|
+
client.deviations["uuid-1"] = make_deviation()
|
|
181
|
+
provider = OfficialProvider(client, quality="f")
|
|
182
|
+
|
|
183
|
+
items = list(provider.resolve(TargetParser.parse("123456")))
|
|
184
|
+
|
|
185
|
+
assert items[1].media_url == "https://wixmp.test/hashed/f/page2.jpg?token=t"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_preview_only_extra_page_is_skipped_not_downgraded(monkeypatch) -> None:
|
|
189
|
+
init = {
|
|
190
|
+
"deviation": {
|
|
191
|
+
"extended": {
|
|
192
|
+
"deviationUuid": "uuid-1",
|
|
193
|
+
"additionalMedia": [
|
|
194
|
+
{"media": {
|
|
195
|
+
"baseUri": "https://wixmp.test/hashed",
|
|
196
|
+
"types": [{"t": "preview", "c": "/f/<prettyName>.jpg"}],
|
|
197
|
+
"prettyName": "page2",
|
|
198
|
+
"token": "t",
|
|
199
|
+
}},
|
|
200
|
+
],
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
monkeypatch.setattr(provider_mod, "deviation_init", lambda identifier, username=None, **_: init)
|
|
205
|
+
client = FakeClient()
|
|
206
|
+
client.deviations["uuid-1"] = make_deviation()
|
|
207
|
+
provider = OfficialProvider(client, quality="f")
|
|
208
|
+
|
|
209
|
+
items = list(provider.resolve(TargetParser.parse("123456")))
|
|
210
|
+
|
|
211
|
+
assert [item.media_url for item in items] == ["https://images.test/full.jpg"]
|
|
212
|
+
|
|
213
|
+
|
|
107
214
|
def test_artwork_best_uses_content(monkeypatch) -> None:
|
|
108
215
|
monkeypatch.setattr(provider_mod, "deviation_init", lambda identifier, username=None, **_: {"deviation": {"extended": {"deviationUuid": "uuid-1", "additionalMedia": []}}})
|
|
109
216
|
client = FakeClient()
|
|
@@ -116,6 +223,21 @@ def test_artwork_best_uses_content(monkeypatch) -> None:
|
|
|
116
223
|
assert client.originals == {} # never called the download endpoint
|
|
117
224
|
|
|
118
225
|
|
|
226
|
+
def test_artwork_video_uses_playable_source_not_poster(monkeypatch) -> None:
|
|
227
|
+
monkeypatch.setattr(provider_mod, "deviation_init", lambda identifier, username=None, **_: {"deviation": {"extended": {"deviationUuid": "uuid-1", "additionalMedia": []}}})
|
|
228
|
+
client = FakeClient()
|
|
229
|
+
client.deviations["uuid-1"] = {
|
|
230
|
+
**make_deviation(content_src="https://images.test/poster.jpg"),
|
|
231
|
+
"videos": [{"src": "https://videos.test/full.mp4"}],
|
|
232
|
+
}
|
|
233
|
+
provider = OfficialProvider(client, quality="f")
|
|
234
|
+
|
|
235
|
+
items = list(provider.resolve(TargetParser.parse("123456")))
|
|
236
|
+
|
|
237
|
+
assert items[0].media_url == "https://videos.test/full.mp4"
|
|
238
|
+
assert items[0].extension == "mp4"
|
|
239
|
+
|
|
240
|
+
|
|
119
241
|
def test_gallery_paginates(monkeypatch) -> None:
|
|
120
242
|
client = FakeClient()
|
|
121
243
|
client.pages[("gallery_all", "alice", 0)] = {
|
|
@@ -6,6 +6,7 @@ import pytest
|
|
|
6
6
|
|
|
7
7
|
from da_downloader.api import ActionType
|
|
8
8
|
from da_downloader.errors import MediaUnavailableError
|
|
9
|
+
from da_downloader.errors import NetworkError
|
|
9
10
|
from da_downloader.models import Deviation, DownloadItem
|
|
10
11
|
from da_downloader.provider import CompositeProvider, WebProvider
|
|
11
12
|
from da_downloader.targets import TargetParser
|
|
@@ -193,6 +194,19 @@ def test_composite_gallery_falls_back_to_web_when_official_unavailable() -> None
|
|
|
193
194
|
assert len(web.api.build_calls) == 1
|
|
194
195
|
|
|
195
196
|
|
|
197
|
+
def test_composite_gallery_falls_back_to_web_when_official_network_fails() -> None:
|
|
198
|
+
web = make_provider(
|
|
199
|
+
pages=[([make_deviation()], False, 1, "")],
|
|
200
|
+
media_urls={"123": "https://images.test/123.jpg"},
|
|
201
|
+
)
|
|
202
|
+
official = RecordingProvider(error=NetworkError("API down"))
|
|
203
|
+
router = CompositeProvider(official=official, web=web)
|
|
204
|
+
|
|
205
|
+
items = list(router.resolve(TargetParser.parse("https://www.deviantart.com/alice/gallery")))
|
|
206
|
+
|
|
207
|
+
assert items[0].artwork_id == "123"
|
|
208
|
+
|
|
209
|
+
|
|
196
210
|
def test_composite_gallery_with_only_web_uses_web() -> None:
|
|
197
211
|
web = make_provider(
|
|
198
212
|
pages=[([make_deviation()], False, 1, "")],
|
devart_dl-4.3.0/README.en.md
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
# DeviantArt Downloader
|
|
2
|
-
|
|
3
|
-
A reliable, focused DeviantArt downloader and archival CLI.
|
|
4
|
-
|
|
5
|
-
**Language / 语言:** [中文](README.md) · English · [Full documentation](https://redtidev1918.github.io/deviantart-downloader/) · [Changelog](CHANGELOG.md)
|
|
6
|
-
|
|
7
|
-
[](https://github.com/redtidev1918/deviantart-downloader/actions/workflows/ci.yml)
|
|
8
|
-
[](https://pypi.org/project/devart-dl/)
|
|
9
|
-
[](https://pypi.org/project/devart-dl/)
|
|
10
|
-
[](https://pypi.org/project/devart-dl/)
|
|
11
|
-
[](LICENSE)
|
|
12
|
-
|
|
13
|
-
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Core Features
|
|
18
|
-
|
|
19
|
-
- **Official API first (OAuth)** — log in once to download originals; no cookie export, no anti-ban delays.
|
|
20
|
-
- **URL-first** — `devart-dl URL` downloads artworks / galleries / favourites / tags / fav.me links directly.
|
|
21
|
-
- **Reliable downloads** — HTTP Range resume, automatic retry, 429 backoff, HTML/empty-file validation, atomic finalize.
|
|
22
|
-
- **Download archive** — `--archive` remembers what you've downloaded in SQLite, skipping it across sessions.
|
|
23
|
-
- **Path templates** — `--directory` / `--filename` support `{id}` `{title}` `{author}` `{published}` `{ext}` and more, with safe sanitization.
|
|
24
|
-
- **Metadata sidecars** — `--write-info-json` writes a `.json` next to each file.
|
|
25
|
-
- **Proxy & fallback** — `--proxy` or proxy environment variables; falls back to cookies when not logged in via OAuth.
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Quick Start
|
|
30
|
-
|
|
31
|
-
### Installation
|
|
32
|
-
|
|
33
|
-
Requires Python 3.10 or newer.
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
pip install devart-dl
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Basic Usage (URL-first)
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
# Single artwork (also fav.me links and bare artwork ids)
|
|
43
|
-
devart-dl https://www.deviantart.com/username/art/title-123456
|
|
44
|
-
|
|
45
|
-
# Gallery / all works by an artist
|
|
46
|
-
devart-dl https://www.deviantart.com/username/gallery
|
|
47
|
-
|
|
48
|
-
# Favourites
|
|
49
|
-
devart-dl https://www.deviantart.com/username/favourites
|
|
50
|
-
|
|
51
|
-
# Tag
|
|
52
|
-
devart-dl https://www.deviantart.com/tag/landscape
|
|
53
|
-
|
|
54
|
-
# Resolve to JSON descriptors (download nothing)
|
|
55
|
-
devart-dl resolve https://www.deviantart.com/username/art/title-123456
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Login (OAuth recommended)
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
devart-dl login oauth --client-id YOUR_PUBLIC_CLIENT_ID
|
|
62
|
-
devart-dl whoami # verify login
|
|
63
|
-
devart-dl logout # revoke the token
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Register a **Public** OAuth app at [deviantart.com/developers](https://www.deviantart.com/developers/) and whitelist `http://127.0.0.1:8765/callback`. Login happens in the browser — no password or `client_secret` is ever given to the CLI.
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
## Main Features
|
|
71
|
-
|
|
72
|
-
### Resolve Mode
|
|
73
|
-
|
|
74
|
-
`devart-dl resolve URL` emits a machine-consumable `ResolvedDeviation` + `MediaAsset[]` JSON contract without writing any file.
|
|
75
|
-
|
|
76
|
-
### Download Modes
|
|
77
|
-
|
|
78
|
-
URL-first is the simplest entry; explicit subcommands are also kept:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
# URL-first
|
|
82
|
-
devart-dl https://www.deviantart.com/username/art/title-123456
|
|
83
|
-
devart-dl https://www.deviantart.com/username/gallery
|
|
84
|
-
devart-dl https://www.deviantart.com/username/gallery/12345 # specific folder
|
|
85
|
-
devart-dl https://www.deviantart.com/username/favourites
|
|
86
|
-
devart-dl https://www.deviantart.com/tag/landscape
|
|
87
|
-
|
|
88
|
-
# Explicit subcommands
|
|
89
|
-
devart-dl url https://www.deviantart.com/username/art/title-123456
|
|
90
|
-
devart-dl artist username
|
|
91
|
-
devart-dl gallery username [gallery_id]
|
|
92
|
-
devart-dl search all "digital art"
|
|
93
|
-
devart-dl fav username folder_id
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
> Note: explicit subcommands keep the old usage for compatibility. The official API has no search endpoint, so `search` always uses the web/cookie path. OAuth and cookies are capabilities that compose — OAuth gives the official API and cookie gives search — instead of one global "login mode".
|
|
97
|
-
|
|
98
|
-
### Authentication
|
|
99
|
-
|
|
100
|
-
**Official API (OAuth) first**, cookies as the fallback.
|
|
101
|
-
|
|
102
|
-
1. **OAuth login (recommended)**
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
devart-dl login oauth --client-id YOUR_PUBLIC_CLIENT_ID
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
- Register a **Public** OAuth app and whitelist `http://127.0.0.1:8765/callback` exactly.
|
|
109
|
-
- Login completes in the browser; tokens are stored at `~/.deviantart_dl/oauth.json` (0600) and auto-refresh.
|
|
110
|
-
- Download commands then use the official API automatically; originals come from the official download endpoint.
|
|
111
|
-
|
|
112
|
-
2. **Cookie login — one-click browser (recommended for cookies)**
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
devart-dl login browser
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
A Chrome window opens to the DeviantArt login page; just log in there. The
|
|
119
|
-
helper drives your **real** Chrome via the DevTools Protocol and captures the
|
|
120
|
-
web cookies (`auth` / `auth_secure` / `userinfo`) automatically — no F12 copy.
|
|
121
|
-
It needs Node.js (>= 22) and Chrome, and saves the session to
|
|
122
|
-
`~/.deviantart_dl/session.json` (0600). Logging in on the real domain is
|
|
123
|
-
required because the DeviantArt login page is behind an AWS WAF bot check that
|
|
124
|
-
blocks headless/proxied browsers; this is also the only way to capture the
|
|
125
|
-
HttpOnly `auth_secure` cookie.
|
|
126
|
-
|
|
127
|
-
3. **Cookie login — manual paste (fallback)**
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
devart-dl login interactive # paste a cookie interactively
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Or create a `cookies.txt`, or set the `DEVIANTART_COOKIES` environment variable. The session is saved at `~/.deviantart_dl/session.json`.
|
|
134
|
-
|
|
135
|
-
### Download Archive & Resume
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
# SQLite archive: skip already-downloaded items across sessions and directories
|
|
139
|
-
devart-dl https://www.deviantart.com/username/gallery --archive ~/.devart-dl/archive.sqlite
|
|
140
|
-
|
|
141
|
-
# Interrupted downloads resume from the .part file — no full re-download
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Filename / Directory Templates
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
devart-dl URL \
|
|
148
|
-
--directory "{author}/{published:%Y-%m}" \
|
|
149
|
-
--filename "{id}_{title}.{ext}"
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Fields: `{id}` `{title}` `{author}` `{username}` `{published}` `{filename}` `{ext}` `{index}`. Paths are sanitized (illegal characters, Windows reserved names, `..` traversal, overly long names) so results always stay inside the download root.
|
|
153
|
-
|
|
154
|
-
### Metadata Sidecar
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
devart-dl URL --write-info-json
|
|
158
|
-
# produces artwork.jpg plus artwork.json (title, author, URL, published, mature, media URL, …)
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
## Configuration Options
|
|
164
|
-
|
|
165
|
-
### Common Options
|
|
166
|
-
|
|
167
|
-
| Option | Description | Default |
|
|
168
|
-
|--------|-------------|---------|
|
|
169
|
-
| `-d, --dest` | Download root directory | `./Downloads` |
|
|
170
|
-
| `--directory` | Directory template (e.g. `{author}/{published:%Y}`) | `{author}` |
|
|
171
|
-
| `--filename` | Filename template (e.g. `{id}_{title}.{ext}`) | `{id}_{title}.{ext}` |
|
|
172
|
-
| `--quality` | `original` (login) / `best` (default) / `preview`; legacy `o/f/p` still accepted | `best` |
|
|
173
|
-
| `--archive` | SQLite download archive path | none |
|
|
174
|
-
| `--write-info-json` | Write a metadata sidecar | off |
|
|
175
|
-
| `--dry-run` | Plan only: resolve and report what would be downloaded, write nothing | off |
|
|
176
|
-
| `--overwrite` | Replace existing files | off (skip) |
|
|
177
|
-
| `--cookies` | Cookie file path | auto-load session |
|
|
178
|
-
| `--proxy` | Proxy URL | auto from environment |
|
|
179
|
-
| `--timeout` | Request timeout (seconds) | `60` |
|
|
180
|
-
| `--retries` | Max retries | `3` |
|
|
181
|
-
| `--limit` | Items per page | `24` |
|
|
182
|
-
|
|
183
|
-
### Output Options
|
|
184
|
-
|
|
185
|
-
- `-v, --verbose`: show details (including skip reasons).
|
|
186
|
-
- `-q, --quiet`: only the summary and errors.
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## Command Reference
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
# URL-first (recommended)
|
|
194
|
-
devart-dl <URL>
|
|
195
|
-
|
|
196
|
-
# Explicit subcommands
|
|
197
|
-
devart-dl url <artwork_url>
|
|
198
|
-
devart-dl artist <username>
|
|
199
|
-
devart-dl gallery <username> [gallery_id]
|
|
200
|
-
devart-dl search <username|all> <query>
|
|
201
|
-
devart-dl fav <username> <folder_id>
|
|
202
|
-
|
|
203
|
-
# Login / session
|
|
204
|
-
devart-dl login oauth --client-id <ID> # OAuth login (recommended)
|
|
205
|
-
devart-dl login browser # one-click cookie via your Chrome
|
|
206
|
-
devart-dl login interactive # cookie login (manual paste)
|
|
207
|
-
devart-dl whoami # show login status
|
|
208
|
-
devart-dl logout # revoke token and clear local session
|
|
209
|
-
|
|
210
|
-
# Other
|
|
211
|
-
devart-dl version
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
---
|
|
215
|
-
|
|
216
|
-
## Advanced Usage
|
|
217
|
-
|
|
218
|
-
### Proxy
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
# Command-line
|
|
222
|
-
devart-dl URL --proxy http://127.0.0.1:7890
|
|
223
|
-
|
|
224
|
-
# Environment variables (HTTP/HTTPS)
|
|
225
|
-
export HTTP_PROXY=http://127.0.0.1:7890
|
|
226
|
-
export HTTPS_PROXY=http://127.0.0.1:7890
|
|
227
|
-
export ALL_PROXY=http://127.0.0.1:7890
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Python API
|
|
231
|
-
|
|
232
|
-
```python
|
|
233
|
-
from pathlib import Path
|
|
234
|
-
from da_downloader import Downloader
|
|
235
|
-
from da_downloader.download import build_downloader
|
|
236
|
-
|
|
237
|
-
downloader = build_downloader(
|
|
238
|
-
destination=Path('downloads'),
|
|
239
|
-
archive=Path('archive.sqlite'), # optional: skip already-downloaded
|
|
240
|
-
quality='best', # original / best / preview
|
|
241
|
-
write_info_json=True, # write a metadata .json next to each file
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
results = downloader.download('https://www.deviantart.com/username/gallery')
|
|
245
|
-
for r in results:
|
|
246
|
-
print(r.status, r.path)
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
When logged in via `devart-dl login oauth`, the official API is used automatically; otherwise it falls back to cookies.
|
|
250
|
-
|
|
251
|
-
---
|
|
252
|
-
|
|
253
|
-
## FAQ
|
|
254
|
-
|
|
255
|
-
**Q: Downloads are incomplete / skip some artworks?**
|
|
256
|
-
**A:** Upgrade to **v3.4.0+**. Older versions fetched only the Featured folder;
|
|
257
|
-
v3.3.1 walks all gallery folders, and v3.4.0 reports parsing/network failures
|
|
258
|
-
loudly and resumes interrupted downloads via Range.
|
|
259
|
-
|
|
260
|
-
**Q: ModuleNotFoundError after pip install?**
|
|
261
|
-
**A:** Upgrade to **v3.4.0+** (`pip install -U devart-dl`); the console entry point is fixed.
|
|
262
|
-
|
|
263
|
-
**Q: Got a 403 Forbidden error?**
|
|
264
|
-
**A:** Usually not logged in or an expired cookie. Prefer `devart-dl login oauth`
|
|
265
|
-
(official API); otherwise `devart-dl login browser` for a one-click cookie
|
|
266
|
-
(`devart-dl login interactive` to paste one manually).
|
|
267
|
-
|
|
268
|
-
**Q: Got a 429 Too Many Requests?**
|
|
269
|
-
**A:** The official API (OAuth) usually avoids this; the cookie path retries with
|
|
270
|
-
backoff. If it persists, pause for a while.
|
|
271
|
-
|
|
272
|
-
**Q: Downloads are slow?**
|
|
273
|
-
**A:** Use a proxy (`--proxy`), or lower the quality (`--quality preview`).
|
|
274
|
-
|
|
275
|
-
**Q: How do I export a cookie?**
|
|
276
|
-
**A:** Easiest is `devart-dl login browser` — it opens your own Chrome and
|
|
277
|
-
captures the cookie automatically. Otherwise open Chrome/Edge DevTools (F12) →
|
|
278
|
-
Application → Cookies, copy the `auth` and `auth_secure` fields, and paste them
|
|
279
|
-
via `devart-dl login interactive`.
|
|
280
|
-
|
|
281
|
-
---
|
|
282
|
-
|
|
283
|
-
## License
|
|
284
|
-
|
|
285
|
-
MIT License — see [LICENSE](LICENSE).
|
|
286
|
-
|
|
287
|
-
**Important disclaimer:** this tool is for personal learning and research only.
|
|
288
|
-
Respect DeviantArt's terms of service and copyright. Do not use it commercially
|
|
289
|
-
or to overload their servers. The author is not responsible for any misuse.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|