devart-dl 4.3.1__tar.gz → 4.3.3__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.1/devart_dl.egg-info → devart_dl-4.3.3}/PKG-INFO +2 -2
- {devart_dl-4.3.1 → devart_dl-4.3.3}/README.md +1 -1
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/official_api.py +53 -18
- {devart_dl-4.3.1 → devart_dl-4.3.3/devart_dl.egg-info}/PKG-INFO +2 -2
- {devart_dl-4.3.1 → devart_dl-4.3.3}/pyproject.toml +1 -1
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_official_provider.py +82 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/.env.example +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/LICENSE +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/MANIFEST.in +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/README.en.md +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/bin/devart-dl +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/bin/install.sh +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/__init__.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/api.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/archive.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/auth.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/cli.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/da_cookie_login.mjs +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/download.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/errors.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/http.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/manager.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/models.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/oauth.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/path.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/provider.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/targets.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/SOURCES.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/dependency_links.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/entry_points.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/requires.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/top_level.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/requirements.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/setup.cfg +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/setup.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_api.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_archive.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_cli.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_conformance.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_download.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_http_downloader.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_manager.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_path.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_provider.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.3}/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.3
|
|
4
4
|
Summary: 专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features
|
|
5
5
|
Author: DeviantArt Downloader Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -85,7 +85,7 @@ devart-dl login browser # 打开你的 Chrome 登录,自动保存 Cookie,
|
|
|
85
85
|
|
|
86
86
|
> 普通用户无需手动下载 GitHub Release 里的 `.whl`、`.tar.gz` 或 Source code,运行上面的 `pip install` 即可。文件区别和 SHA256 校验方法见[完整文档](https://redtidev1918.github.io/deviantart-downloader/#/?id=github-release-%e9%87%8c%e7%9a%84%e6%96%87%e4%bb%b6%e6%98%af%e4%bb%80%e4%b9%88)。
|
|
87
87
|
|
|
88
|
-
##
|
|
88
|
+
## 功能
|
|
89
89
|
|
|
90
90
|
- **官方 API(OAuth)优先**:登录一次即可下载原图,无需导出 Cookie,也无需防封延时。
|
|
91
91
|
- **URL 优先**:直接粘贴作品 / 画廊 / 收藏夹 / 标签链接即可下载。
|
|
@@ -51,7 +51,7 @@ devart-dl login browser # 打开你的 Chrome 登录,自动保存 Cookie,
|
|
|
51
51
|
|
|
52
52
|
> 普通用户无需手动下载 GitHub Release 里的 `.whl`、`.tar.gz` 或 Source code,运行上面的 `pip install` 即可。文件区别和 SHA256 校验方法见[完整文档](https://redtidev1918.github.io/deviantart-downloader/#/?id=github-release-%e9%87%8c%e7%9a%84%e6%96%87%e4%bb%b6%e6%98%af%e4%bb%80%e4%b9%88)。
|
|
53
53
|
|
|
54
|
-
##
|
|
54
|
+
## 功能
|
|
55
55
|
|
|
56
56
|
- **官方 API(OAuth)优先**:登录一次即可下载原图,无需导出 Cookie,也无需防封延时。
|
|
57
57
|
- **URL 优先**:直接粘贴作品 / 画廊 / 收藏夹 / 标签链接即可下载。
|
|
@@ -319,32 +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.
|
|
323
327
|
|
|
324
328
|
Mature pages the server still returns with a ``blur_`` censored image are
|
|
325
329
|
skipped individually; one blocked page does not discard the others.
|
|
326
330
|
"""
|
|
327
|
-
extended = init_data.get("deviation", {}).get("extended") or {}
|
|
328
|
-
entries = extended.get("additionalMedia") or []
|
|
329
331
|
urls: list = []
|
|
330
|
-
for entry in
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
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
|
|
334
336
|
if not isinstance(media, dict):
|
|
335
337
|
continue
|
|
336
|
-
|
|
337
|
-
if
|
|
338
|
-
|
|
339
|
-
if "blur_" in base:
|
|
340
|
-
continue
|
|
341
|
-
raw_token = media.get("token")
|
|
342
|
-
token = raw_token[0] if isinstance(raw_token, list) and raw_token else raw_token
|
|
343
|
-
if token:
|
|
344
|
-
urls.append(f"{base}{'&' if '?' in base else '?'}token={token}")
|
|
345
|
-
else:
|
|
346
|
-
urls.append(base)
|
|
338
|
+
url = _descriptor_media_url(media)
|
|
339
|
+
if url and "blur_" not in url:
|
|
340
|
+
urls.append(url)
|
|
347
341
|
return urls
|
|
348
342
|
|
|
349
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
|
+
|
|
350
385
|
__all__ = ["OfficialApiClient", "OriginalDownload", "resolve_uuid", "deviation_init", "deviation_uuid", "additional_media_urls"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devart-dl
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.3
|
|
4
4
|
Summary: 专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features
|
|
5
5
|
Author: DeviantArt Downloader Team
|
|
6
6
|
License-Expression: MIT
|
|
@@ -85,7 +85,7 @@ devart-dl login browser # 打开你的 Chrome 登录,自动保存 Cookie,
|
|
|
85
85
|
|
|
86
86
|
> 普通用户无需手动下载 GitHub Release 里的 `.whl`、`.tar.gz` 或 Source code,运行上面的 `pip install` 即可。文件区别和 SHA256 校验方法见[完整文档](https://redtidev1918.github.io/deviantart-downloader/#/?id=github-release-%e9%87%8c%e7%9a%84%e6%96%87%e4%bb%b6%e6%98%af%e4%bb%80%e4%b9%88)。
|
|
87
87
|
|
|
88
|
-
##
|
|
88
|
+
## 功能
|
|
89
89
|
|
|
90
90
|
- **官方 API(OAuth)优先**:登录一次即可下载原图,无需导出 Cookie,也无需防封延时。
|
|
91
91
|
- **URL 优先**:直接粘贴作品 / 画廊 / 收藏夹 / 标签链接即可下载。
|
|
@@ -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.3"
|
|
8
8
|
description = "专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -129,6 +129,88 @@ def test_mature_blurred_extra_pages_are_skipped(monkeypatch) -> None:
|
|
|
129
129
|
]
|
|
130
130
|
|
|
131
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
|
+
|
|
132
214
|
def test_artwork_best_uses_content(monkeypatch) -> None:
|
|
133
215
|
monkeypatch.setattr(provider_mod, "deviation_init", lambda identifier, username=None, **_: {"deviation": {"extended": {"deviationUuid": "uuid-1", "additionalMedia": []}}})
|
|
134
216
|
client = FakeClient()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|