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.
Files changed (45) hide show
  1. {devart_dl-4.3.1/devart_dl.egg-info → devart_dl-4.3.3}/PKG-INFO +2 -2
  2. {devart_dl-4.3.1 → devart_dl-4.3.3}/README.md +1 -1
  3. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/official_api.py +53 -18
  4. {devart_dl-4.3.1 → devart_dl-4.3.3/devart_dl.egg-info}/PKG-INFO +2 -2
  5. {devart_dl-4.3.1 → devart_dl-4.3.3}/pyproject.toml +1 -1
  6. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_official_provider.py +82 -0
  7. {devart_dl-4.3.1 → devart_dl-4.3.3}/.env.example +0 -0
  8. {devart_dl-4.3.1 → devart_dl-4.3.3}/LICENSE +0 -0
  9. {devart_dl-4.3.1 → devart_dl-4.3.3}/MANIFEST.in +0 -0
  10. {devart_dl-4.3.1 → devart_dl-4.3.3}/README.en.md +0 -0
  11. {devart_dl-4.3.1 → devart_dl-4.3.3}/bin/devart-dl +0 -0
  12. {devart_dl-4.3.1 → devart_dl-4.3.3}/bin/install.sh +0 -0
  13. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/__init__.py +0 -0
  14. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/api.py +0 -0
  15. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/archive.py +0 -0
  16. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/auth.py +0 -0
  17. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/cli.py +0 -0
  18. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/da_cookie_login.mjs +0 -0
  19. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/download.py +0 -0
  20. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/errors.py +0 -0
  21. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/http.py +0 -0
  22. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/manager.py +0 -0
  23. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/models.py +0 -0
  24. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/oauth.py +0 -0
  25. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/path.py +0 -0
  26. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/provider.py +0 -0
  27. {devart_dl-4.3.1 → devart_dl-4.3.3}/da_downloader/targets.py +0 -0
  28. {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/SOURCES.txt +0 -0
  29. {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/dependency_links.txt +0 -0
  30. {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/entry_points.txt +0 -0
  31. {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/requires.txt +0 -0
  32. {devart_dl-4.3.1 → devart_dl-4.3.3}/devart_dl.egg-info/top_level.txt +0 -0
  33. {devart_dl-4.3.1 → devart_dl-4.3.3}/requirements.txt +0 -0
  34. {devart_dl-4.3.1 → devart_dl-4.3.3}/setup.cfg +0 -0
  35. {devart_dl-4.3.1 → devart_dl-4.3.3}/setup.py +0 -0
  36. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_api.py +0 -0
  37. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_archive.py +0 -0
  38. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_cli.py +0 -0
  39. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_conformance.py +0 -0
  40. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_download.py +0 -0
  41. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_http_downloader.py +0 -0
  42. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_manager.py +0 -0
  43. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_path.py +0 -0
  44. {devart_dl-4.3.1 → devart_dl-4.3.3}/tests/test_provider.py +0 -0
  45. {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.1
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``). Prefers the raw ``baseUri`` file + token.
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 entries:
331
- if not isinstance(entry, dict):
332
- continue
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
- base = media.get("baseUri")
337
- if not isinstance(base, str) or not base:
338
- continue
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.1
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.1"
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