devart-dl 4.3.3__tar.gz → 4.3.4__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.3/devart_dl.egg-info → devart_dl-4.3.4}/PKG-INFO +2 -1
  2. {devart_dl-4.3.3 → devart_dl-4.3.4}/README.en.md +1 -0
  3. {devart_dl-4.3.3 → devart_dl-4.3.4}/README.md +1 -0
  4. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/models.py +15 -0
  5. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/official_api.py +21 -1
  6. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/provider.py +30 -10
  7. {devart_dl-4.3.3 → devart_dl-4.3.4/devart_dl.egg-info}/PKG-INFO +2 -1
  8. {devart_dl-4.3.3 → devart_dl-4.3.4}/pyproject.toml +1 -1
  9. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_official_provider.py +27 -0
  10. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_provider.py +25 -0
  11. {devart_dl-4.3.3 → devart_dl-4.3.4}/.env.example +0 -0
  12. {devart_dl-4.3.3 → devart_dl-4.3.4}/LICENSE +0 -0
  13. {devart_dl-4.3.3 → devart_dl-4.3.4}/MANIFEST.in +0 -0
  14. {devart_dl-4.3.3 → devart_dl-4.3.4}/bin/devart-dl +0 -0
  15. {devart_dl-4.3.3 → devart_dl-4.3.4}/bin/install.sh +0 -0
  16. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/__init__.py +0 -0
  17. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/api.py +0 -0
  18. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/archive.py +0 -0
  19. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/auth.py +0 -0
  20. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/cli.py +0 -0
  21. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/da_cookie_login.mjs +0 -0
  22. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/download.py +0 -0
  23. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/errors.py +0 -0
  24. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/http.py +0 -0
  25. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/manager.py +0 -0
  26. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/oauth.py +0 -0
  27. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/path.py +0 -0
  28. {devart_dl-4.3.3 → devart_dl-4.3.4}/da_downloader/targets.py +0 -0
  29. {devart_dl-4.3.3 → devart_dl-4.3.4}/devart_dl.egg-info/SOURCES.txt +0 -0
  30. {devart_dl-4.3.3 → devart_dl-4.3.4}/devart_dl.egg-info/dependency_links.txt +0 -0
  31. {devart_dl-4.3.3 → devart_dl-4.3.4}/devart_dl.egg-info/entry_points.txt +0 -0
  32. {devart_dl-4.3.3 → devart_dl-4.3.4}/devart_dl.egg-info/requires.txt +0 -0
  33. {devart_dl-4.3.3 → devart_dl-4.3.4}/devart_dl.egg-info/top_level.txt +0 -0
  34. {devart_dl-4.3.3 → devart_dl-4.3.4}/requirements.txt +0 -0
  35. {devart_dl-4.3.3 → devart_dl-4.3.4}/setup.cfg +0 -0
  36. {devart_dl-4.3.3 → devart_dl-4.3.4}/setup.py +0 -0
  37. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_api.py +0 -0
  38. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_archive.py +0 -0
  39. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_cli.py +0 -0
  40. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_conformance.py +0 -0
  41. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_download.py +0 -0
  42. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_http_downloader.py +0 -0
  43. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_manager.py +0 -0
  44. {devart_dl-4.3.3 → devart_dl-4.3.4}/tests/test_path.py +0 -0
  45. {devart_dl-4.3.3 → devart_dl-4.3.4}/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
3
+ Version: 4.3.4
4
4
  Summary: 专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features
5
5
  Author: DeviantArt Downloader Team
6
6
  License-Expression: MIT
@@ -94,6 +94,7 @@ devart-dl login browser # 打开你的 Chrome 登录,自动保存 Cookie,
94
94
  - **路径模板 + 元数据 sidecar**:`--directory`/`--filename` 模板与 `--write-info-json`。
95
95
  - **代理与降级**:支持 `--proxy`;未登录 OAuth 时自动回退 Cookie。
96
96
  - **Resolve 模式**:`devart-dl resolve URL` 输出 `ResolvedDeviation`/`MediaAsset` 的 JSON 契约,不写文件。
97
+ - **付费/订阅锁定识别**:官方 API 返回 `premium_folder_data` / `tier_access`,或网页端非成熟作品的模糊主图(`blur_`),都会识别为「需要订阅/购买」:网页通道跳过,官方通道明确报错,不再把打码图当原图保存。
97
98
 
98
99
  ## 文档
99
100
 
@@ -25,6 +25,7 @@ See [CHANGELOG.md](CHANGELOG.md) for release notes.
25
25
  - **Path templates** — `--directory` / `--filename` support `{id}` `{title}` `{author}` `{published}` `{ext}` and more, with safe sanitization.
26
26
  - **Metadata sidecars** — `--write-info-json` writes a `.json` next to each file.
27
27
  - **Proxy & fallback** — `--proxy` or proxy environment variables; falls back to cookies when not logged in via OAuth.
28
+ - **Paid / subscription-lock detection** — official API `premium_folder_data` / `tier_access`, or a blurred (`blur_`) non-mature main on the web path, is recognised as subscription/purchase-gated: the web path skips it and the official path fails with a clear message instead of saving a censored placeholder.
28
29
 
29
30
  ---
30
31
 
@@ -60,6 +60,7 @@ devart-dl login browser # 打开你的 Chrome 登录,自动保存 Cookie,
60
60
  - **路径模板 + 元数据 sidecar**:`--directory`/`--filename` 模板与 `--write-info-json`。
61
61
  - **代理与降级**:支持 `--proxy`;未登录 OAuth 时自动回退 Cookie。
62
62
  - **Resolve 模式**:`devart-dl resolve URL` 输出 `ResolvedDeviation`/`MediaAsset` 的 JSON 契约,不写文件。
63
+ - **付费/订阅锁定识别**:官方 API 返回 `premium_folder_data` / `tier_access`,或网页端非成熟作品的模糊主图(`blur_`),都会识别为「需要订阅/购买」:网页通道跳过,官方通道明确报错,不再把打码图当原图保存。
63
64
 
64
65
  ## 文档
65
66
 
@@ -29,6 +29,7 @@ class Deviation:
29
29
  is_downloadable: bool
30
30
  is_mature: bool
31
31
  deviation_type: str
32
+ premium: bool = False
32
33
 
33
34
  @classmethod
34
35
  def from_api_response(cls, data: Dict[str, Any]) -> "Deviation":
@@ -63,6 +64,7 @@ class Deviation:
63
64
  ),
64
65
  is_mature=bool(data.get("isMature", data.get("is_mature", False))),
65
66
  deviation_type=str(deviation_type),
67
+ premium=_is_locked(data),
66
68
  )
67
69
 
68
70
  def _extract_extension_from_media(self, media: Dict[str, Any]) -> str:
@@ -91,12 +93,25 @@ class Deviation:
91
93
  flags = []
92
94
  if self.is_mature:
93
95
  flags.append("MATURE")
96
+ if self.premium:
97
+ flags.append("PREMIUM")
94
98
  if self.is_downloadable:
95
99
  flags.append("DOWNLOADABLE")
96
100
  flag_str = f" [{', '.join(flags)}]" if flags else ""
97
101
  return f"{self.title} by {self.author}{flag_str}"
98
102
 
99
103
 
104
+ def _is_locked(data: Dict[str, Any]) -> bool:
105
+ """True for premium/subscription-locked works (official/web DTO shapes)."""
106
+ premium = data.get("premium_folder_data") or data.get("premiumFolderData")
107
+ if isinstance(premium, dict):
108
+ access = premium.get("has_access", premium.get("hasAccess"))
109
+ if access is False:
110
+ return True
111
+ tier = data.get("tier_access") or data.get("tierAccess")
112
+ return tier in ("locked", "locked-subscribed")
113
+
114
+
100
115
  @dataclass(frozen=True)
101
116
  class DownloadItem:
102
117
  """A single resolved media file, produced by a provider and consumed by the
@@ -316,6 +316,26 @@ def deviation_uuid(init_data: dict) -> str:
316
316
  return uuid
317
317
 
318
318
 
319
+ def is_locked(deviation: dict) -> bool:
320
+ """Premium/paid works the current account cannot access.
321
+
322
+ The official DTO marks these with ``premium_folder_data.has_access ==
323
+ False`` or ``tier_access`` locked states (matching DAKit); the web DTO may
324
+ use the camelCase pair ``premiumFolderData.hasAccess`` / ``tierAccess``.
325
+ """
326
+ premium = deviation.get("premium_folder_data") or deviation.get(
327
+ "premiumFolderData"
328
+ )
329
+ if isinstance(premium, dict) and premium.get("has_access", premium.get("hasAccess")) is False:
330
+ return True
331
+ tier = deviation.get("tier_access") or deviation.get("tierAccess")
332
+ return tier in ("locked", "locked-subscribed")
333
+
334
+
335
+ def is_blurred(url: str) -> bool:
336
+ """True when the URL is a censored ``blur_`` Wix transform (mature/paid)."""
337
+ return "blur_" in url
338
+
319
339
  def additional_media_urls(init_data: dict) -> list:
320
340
  """Original-file URLs of a multimedia deviation's extra pages
321
341
  (``deviation.extended.additionalMedia``, each entry nests its Wix
@@ -336,7 +356,7 @@ def additional_media_urls(init_data: dict) -> list:
336
356
  if not isinstance(media, dict):
337
357
  continue
338
358
  url = _descriptor_media_url(media)
339
- if url and "blur_" not in url:
359
+ if url and not is_blurred(url):
340
360
  urls.append(url)
341
361
  return urls
342
362
 
@@ -20,6 +20,8 @@ from .official_api import (
20
20
  additional_media_urls,
21
21
  deviation_init,
22
22
  deviation_uuid,
23
+ is_blurred,
24
+ is_locked,
23
25
  )
24
26
  from .targets import DownloadTarget, TargetKind
25
27
 
@@ -120,6 +122,11 @@ class WebProvider:
120
122
  if media_url is None:
121
123
  logger.warning("no media URL for %r; skipping", deviation.title)
122
124
  return None
125
+ # 付费/订阅锁定:非成熟作品的模糊主图 = 当前账号拿不到原图,跳过而不是把
126
+ # 打码图当成作品保存(成熟打码由 official provider / additional pages 处理)。
127
+ if deviation.premium or (not deviation.is_mature and is_blurred(media_url)):
128
+ logger.warning("premium/subscription-locked %r; skipping", deviation.title)
129
+ return None
123
130
  return DownloadItem(
124
131
  artwork_id=deviation.deviation_id,
125
132
  url=deviation.url,
@@ -262,6 +269,8 @@ class OfficialProvider:
262
269
  original = self.client.original_download(uuid)
263
270
  ext = _ext_from_url(original.filename)
264
271
  return original.url, ext
272
+ url: Optional[str] = None
273
+ ext: Optional[str] = None
265
274
  content = deviation.get("content") or {}
266
275
  # `content.src` is a poster for video works; only videos[].src is playable.
267
276
  videos = deviation.get("videos") or []
@@ -269,16 +278,27 @@ class OfficialProvider:
269
278
  for video in videos:
270
279
  src = video.get("src") if isinstance(video, dict) else None
271
280
  if src:
272
- return str(src), _ext_from_url(str(src))
273
- if content.get("src"):
274
- return str(content["src"]), _ext_from_url(str(content["src"]))
275
- preview = deviation.get("preview") or {}
276
- if preview.get("src"):
277
- return str(preview["src"]), _ext_from_url(str(preview["src"]))
278
- thumbs = deviation.get("thumbs") or []
279
- if thumbs and isinstance(thumbs[0], dict) and thumbs[0].get("src"):
280
- return str(thumbs[0]["src"]), _ext_from_url(str(thumbs[0]["src"]))
281
- raise MediaUnavailableError(f"no media available for deviation {uuid}")
281
+ url, ext = str(src), _ext_from_url(str(src))
282
+ break
283
+ if url is None and content.get("src"):
284
+ url, ext = str(content["src"]), _ext_from_url(str(content["src"]))
285
+ if url is None:
286
+ preview = deviation.get("preview") or {}
287
+ if preview.get("src"):
288
+ url, ext = str(preview["src"]), _ext_from_url(str(preview["src"]))
289
+ if url is None:
290
+ thumbs = deviation.get("thumbs") or []
291
+ if thumbs and isinstance(thumbs[0], dict) and thumbs[0].get("src"):
292
+ url, ext = str(thumbs[0]["src"]), _ext_from_url(str(thumbs[0]["src"]))
293
+ if url is None:
294
+ raise MediaUnavailableError(f"no media available for deviation {uuid}")
295
+ # 付费/订阅锁定:非成熟作品的模糊主图(或显式 premium 字段)=拿不到原图,
296
+ # 明确报错而不是保存一张打码占位图。成熟作品由 mature_content=true 提供未打码源。
297
+ if is_locked(deviation) or (not deviation.get("is_mature") and is_blurred(url)):
298
+ raise MediaUnavailableError(
299
+ f"deviation {uuid} requires a subscription/purchase (premium)"
300
+ )
301
+ return url, ext
282
302
 
283
303
 
284
304
  def _ext_from_url(url: str) -> Optional[str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devart-dl
3
- Version: 4.3.3
3
+ Version: 4.3.4
4
4
  Summary: 专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features
5
5
  Author: DeviantArt Downloader Team
6
6
  License-Expression: MIT
@@ -94,6 +94,7 @@ devart-dl login browser # 打开你的 Chrome 登录,自动保存 Cookie,
94
94
  - **路径模板 + 元数据 sidecar**:`--directory`/`--filename` 模板与 `--write-info-json`。
95
95
  - **代理与降级**:支持 `--proxy`;未登录 OAuth 时自动回退 Cookie。
96
96
  - **Resolve 模式**:`devart-dl resolve URL` 输出 `ResolvedDeviation`/`MediaAsset` 的 JSON 契约,不写文件。
97
+ - **付费/订阅锁定识别**:官方 API 返回 `premium_folder_data` / `tier_access`,或网页端非成熟作品的模糊主图(`blur_`),都会识别为「需要订阅/购买」:网页通道跳过,官方通道明确报错,不再把打码图当原图保存。
97
98
 
98
99
  ## 文档
99
100
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devart-dl"
7
- version = "4.3.3"
7
+ version = "4.3.4"
8
8
  description = "专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -274,6 +274,33 @@ def test_no_media_raises(monkeypatch) -> None:
274
274
  list(provider.resolve(TargetParser.parse("123456")))
275
275
 
276
276
 
277
+ def test_blurred_non_mature_main_raises_premium(monkeypatch) -> None:
278
+ monkeypatch.setattr(provider_mod, "deviation_init", lambda identifier, username=None, **_: {"deviation": {"extended": {"deviationUuid": "uuid-1", "additionalMedia": []}}})
279
+ client = FakeClient()
280
+ client.deviations["uuid-1"] = make_deviation(
281
+ uuid="uuid-1",
282
+ content_src="https://images.test/blur_40/full.jpg",
283
+ is_downloadable=False,
284
+ mature=False,
285
+ )
286
+ provider = OfficialProvider(client, quality="f")
287
+
288
+ with pytest.raises(MediaUnavailableError, match="subscription/purchase"):
289
+ list(provider.resolve(TargetParser.parse("123456")))
290
+
291
+
292
+ def test_premium_folder_without_access_raises_premium(monkeypatch) -> None:
293
+ monkeypatch.setattr(provider_mod, "deviation_init", lambda identifier, username=None, **_: {"deviation": {"extended": {"deviationUuid": "uuid-1", "additionalMedia": []}}})
294
+ client = FakeClient()
295
+ deviation = make_deviation(uuid="uuid-1", is_downloadable=False)
296
+ deviation["premium_folder_data"] = {"has_access": False}
297
+ client.deviations["uuid-1"] = deviation
298
+ provider = OfficialProvider(client, quality="f")
299
+
300
+ with pytest.raises(MediaUnavailableError, match="subscription/purchase"):
301
+ list(provider.resolve(TargetParser.parse("123456")))
302
+
303
+
277
304
  def test_tag_target() -> None:
278
305
  client = FakeClient()
279
306
  client.pages[("browse_tags", "landscape", 0)] = {
@@ -74,6 +74,31 @@ def test_gallery_yields_items_with_media_url_and_extension() -> None:
74
74
  assert items[0].extension == "png"
75
75
 
76
76
 
77
+ def test_blurred_non_mature_item_is_skipped() -> None:
78
+ dev = make_deviation()
79
+ provider = make_provider(
80
+ pages=[([dev], False, 1, "")],
81
+ media_urls={"123": "https://images.test/blur_40/123.jpg"},
82
+ )
83
+
84
+ items = list(provider.resolve(TargetParser.parse("https://www.deviantart.com/alice/gallery")))
85
+
86
+ assert items == []
87
+
88
+
89
+ def test_premium_locked_item_is_skipped() -> None:
90
+ dev = make_deviation()
91
+ dev.premium = True
92
+ provider = make_provider(
93
+ pages=[([dev], False, 1, "")],
94
+ media_urls={"123": "https://images.test/123.jpg"},
95
+ )
96
+
97
+ items = list(provider.resolve(TargetParser.parse("https://www.deviantart.com/alice/gallery")))
98
+
99
+ assert items == []
100
+
101
+
77
102
  def test_pagination_across_two_pages() -> None:
78
103
  first = make_deviation("1")
79
104
  second = make_deviation("2")
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