devart-dl 4.4.2__tar.gz → 4.4.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.4.2/devart_dl.egg-info → devart_dl-4.4.3}/PKG-INFO +1 -1
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/api.py +10 -11
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/download.py +1 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/models.py +4 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/official_api.py +12 -4
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/provider.py +14 -2
- devart_dl-4.4.3/da_downloader/wix_media.py +55 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3/devart_dl.egg-info}/PKG-INFO +1 -1
- {devart_dl-4.4.2 → devart_dl-4.4.3}/devart_dl.egg-info/SOURCES.txt +2 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/pyproject.toml +1 -1
- devart_dl-4.4.3/tests/test_bounded_media.py +104 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/.env.example +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/LICENSE +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/MANIFEST.in +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/README.en.md +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/README.md +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/bin/devart-dl +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/bin/install.sh +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/__init__.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/archive.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/auth.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/cli.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/da_cookie_login.mjs +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/errors.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/http.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/manager.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/oauth.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/path.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/da_downloader/targets.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/devart_dl.egg-info/dependency_links.txt +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/devart_dl.egg-info/entry_points.txt +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/devart_dl.egg-info/requires.txt +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/devart_dl.egg-info/top_level.txt +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/requirements.txt +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/setup.cfg +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/setup.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_api.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_archive.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_cli.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_conformance.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_download.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_http_downloader.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_manager.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_official_provider.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_path.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_provider.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_targets.py +0 -0
- {devart_dl-4.4.2 → devart_dl-4.4.3}/tests/test_web_session_verify.py +0 -0
|
@@ -12,6 +12,7 @@ import requests
|
|
|
12
12
|
from requests.exceptions import RequestException
|
|
13
13
|
|
|
14
14
|
from .models import Deviation, ActionType
|
|
15
|
+
from .wix_media import append_media_token, bounded_fullview_url
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
@@ -324,9 +325,13 @@ class DeviantArtAPI:
|
|
|
324
325
|
None,
|
|
325
326
|
)
|
|
326
327
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
328
|
+
bounded = bounded_fullview_url(media)
|
|
329
|
+
if bounded:
|
|
330
|
+
logger.warning("Using DA fullview display image, not the uploaded original")
|
|
331
|
+
return bounded
|
|
332
|
+
|
|
333
|
+
# Preserve bare files for unrestricted/unknown signatures and GIFs.
|
|
334
|
+
# An extension alone does not establish that a bounded token permits it.
|
|
330
335
|
def _is_file_url(u: str) -> bool:
|
|
331
336
|
path = str(u).split('?', 1)[0].rstrip('/')
|
|
332
337
|
return bool(re.search(r'\.(?:png|jpe?g|gif|webp|mp4|m4v)$', path, re.I))
|
|
@@ -344,10 +349,7 @@ class DeviantArtAPI:
|
|
|
344
349
|
logger.warning("Full view not found, using base URI")
|
|
345
350
|
url = base_uri
|
|
346
351
|
|
|
347
|
-
|
|
348
|
-
url += f"{'&' if '?' in url else '?'}token={token}"
|
|
349
|
-
|
|
350
|
-
return url
|
|
352
|
+
return append_media_token(url, token)
|
|
351
353
|
|
|
352
354
|
def _get_preview_url(self, media: Dict, base_uri: str,
|
|
353
355
|
token: str, pretty_name: str) -> Optional[str]:
|
|
@@ -368,7 +370,4 @@ class DeviantArtAPI:
|
|
|
368
370
|
|
|
369
371
|
url = base_uri + str(preview['c']).replace('<prettyName>', pretty_name)
|
|
370
372
|
|
|
371
|
-
|
|
372
|
-
url += f"{'&' if '?' in url else '?'}token={token}"
|
|
373
|
-
|
|
374
|
-
return url
|
|
373
|
+
return append_media_token(url, token)
|
|
@@ -220,6 +220,7 @@ class MediaAsset:
|
|
|
220
220
|
width: int | None = None
|
|
221
221
|
height: int | None = None
|
|
222
222
|
mature: bool = False
|
|
223
|
+
representation: str | None = None
|
|
223
224
|
|
|
224
225
|
def to_dict(self) -> dict:
|
|
225
226
|
data: dict = {
|
|
@@ -233,6 +234,7 @@ class MediaAsset:
|
|
|
233
234
|
("width", self.width),
|
|
234
235
|
("height", self.height),
|
|
235
236
|
("mature", self.mature),
|
|
237
|
+
("representation", self.representation),
|
|
236
238
|
):
|
|
237
239
|
if value is not None:
|
|
238
240
|
data[key] = value
|
|
@@ -270,6 +272,7 @@ def make_media_asset(
|
|
|
270
272
|
*,
|
|
271
273
|
extension: str | None = None,
|
|
272
274
|
mature: bool = False,
|
|
275
|
+
representation: str | None = None,
|
|
273
276
|
) -> MediaAsset:
|
|
274
277
|
ext = (extension or "").lstrip(".").lower() or None
|
|
275
278
|
if ext == "txt":
|
|
@@ -285,4 +288,5 @@ def make_media_asset(
|
|
|
285
288
|
extension=ext,
|
|
286
289
|
mime_type=_MIME_BY_EXT.get(ext) or _DEFAULT_MIME[kind],
|
|
287
290
|
mature=mature,
|
|
291
|
+
representation=representation,
|
|
288
292
|
)
|
|
@@ -24,6 +24,7 @@ from .errors import (
|
|
|
24
24
|
ParseError,
|
|
25
25
|
)
|
|
26
26
|
from .oauth import OAuthSession
|
|
27
|
+
from .wix_media import append_media_token, bounded_fullview_url
|
|
27
28
|
|
|
28
29
|
logger = logging.getLogger(__name__)
|
|
29
30
|
|
|
@@ -337,12 +338,13 @@ def is_blurred(url: str) -> bool:
|
|
|
337
338
|
return "blur_" in url
|
|
338
339
|
|
|
339
340
|
def additional_media_urls(init_data: dict) -> list:
|
|
340
|
-
"""
|
|
341
|
+
"""Media URLs of a multimedia deviation's extra pages
|
|
341
342
|
(``deviation.extended.additionalMedia``, each entry nests its Wix
|
|
342
343
|
descriptor under ``media``). Mirrors DeviantDrop's pick order: a playable
|
|
343
344
|
video ``types[].b`` (best quality) wins over ``baseUri`` — a video page's
|
|
344
|
-
baseUri is its poster — then
|
|
345
|
-
fullview
|
|
345
|
+
baseUri is its poster — then a verified bounded fullview or the bare file.
|
|
346
|
+
A fullview is a display representation, not an uploaded original.
|
|
347
|
+
A page whose only representation is a small preview is
|
|
346
348
|
skipped rather than saved as a fake original.
|
|
347
349
|
|
|
348
350
|
Mature pages the server still returns with a ``blur_`` censored image are
|
|
@@ -378,6 +380,12 @@ def _descriptor_media_url(media: dict) -> Optional[str]:
|
|
|
378
380
|
if videos:
|
|
379
381
|
best = max(videos, key=lambda item: _VIDEO_QUALITY_RANK.get(item.get("q"), 0))
|
|
380
382
|
return _append_token(str(best["b"]), media.get("token"))
|
|
383
|
+
bounded = bounded_fullview_url(media)
|
|
384
|
+
if bounded:
|
|
385
|
+
logger.warning(
|
|
386
|
+
"Using DA fullview display image for an additional page, not the uploaded original"
|
|
387
|
+
)
|
|
388
|
+
return bounded
|
|
381
389
|
base = media.get("baseUri")
|
|
382
390
|
if isinstance(base, str) and base and _FILE_EXT_RE.search(base.split("?", 1)[0]):
|
|
383
391
|
return _append_token(base, media.get("token"))
|
|
@@ -399,7 +407,7 @@ def _append_token(url: str, raw_token: Any) -> str:
|
|
|
399
407
|
token = raw_token[0] if isinstance(raw_token, list) and raw_token else raw_token
|
|
400
408
|
if not token:
|
|
401
409
|
return url
|
|
402
|
-
return
|
|
410
|
+
return append_media_token(url, token)
|
|
403
411
|
|
|
404
412
|
|
|
405
413
|
__all__ = ["OfficialApiClient", "OriginalDownload", "resolve_uuid", "deviation_init", "deviation_uuid", "additional_media_urls"]
|
|
@@ -15,6 +15,7 @@ from .api import ActionType, DeviantArtAPI
|
|
|
15
15
|
from .errors import MediaUnavailableError, ParseError
|
|
16
16
|
from .errors import AuthenticationError, NetworkError
|
|
17
17
|
from .models import Deviation, DownloadItem, literature_text
|
|
18
|
+
from .wix_media import bounded_fullview_url
|
|
18
19
|
from .official_api import (
|
|
19
20
|
OfficialApiClient,
|
|
20
21
|
additional_media_urls,
|
|
@@ -152,7 +153,10 @@ class WebProvider:
|
|
|
152
153
|
media_url=media_url,
|
|
153
154
|
extension=_extension(deviation, media_url),
|
|
154
155
|
mature=deviation.is_mature,
|
|
155
|
-
metadata={"quality": self.quality
|
|
156
|
+
metadata={"quality": self.quality, **(
|
|
157
|
+
{"representation": "fullview", "original_available": False}
|
|
158
|
+
if media_url == bounded_fullview_url(deviation.media or {}) else {}
|
|
159
|
+
)},
|
|
156
160
|
)
|
|
157
161
|
|
|
158
162
|
|
|
@@ -251,6 +255,11 @@ class OfficialProvider:
|
|
|
251
255
|
return
|
|
252
256
|
main = self._item(self.client.deviation(uuid))
|
|
253
257
|
yield main
|
|
258
|
+
bounded_urls = {
|
|
259
|
+
bounded_fullview_url(entry.get("media") or {})
|
|
260
|
+
for entry in ((init_data.get("deviation", {}).get("extended") or {}).get("additionalMedia") or [])
|
|
261
|
+
if isinstance(entry, dict) and isinstance(entry.get("media"), dict)
|
|
262
|
+
}
|
|
254
263
|
for index, url in enumerate(additional_media_urls(init_data), start=1):
|
|
255
264
|
item = DownloadItem(
|
|
256
265
|
artwork_id=f"{main.artwork_id}-{index}",
|
|
@@ -261,7 +270,10 @@ class OfficialProvider:
|
|
|
261
270
|
extension=_ext_from_url(url),
|
|
262
271
|
published_at=main.published_at,
|
|
263
272
|
mature=main.mature,
|
|
264
|
-
metadata={**main.metadata, "index": index
|
|
273
|
+
metadata={**main.metadata, "index": index, **(
|
|
274
|
+
{"representation": "fullview", "original_available": False}
|
|
275
|
+
if url in bounded_urls else {}
|
|
276
|
+
)},
|
|
265
277
|
)
|
|
266
278
|
yield item
|
|
267
279
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Bounded Wix URL selection, shared with DeviantDrop's verified 2026-10-29 fix.
|
|
2
|
+
|
|
3
|
+
See tests/fixtures/deviantart-bounded-media-evidence.md. Like DAViewer/DAKit,
|
|
4
|
+
keep display representations distinct from uploaded originals. Token payloads
|
|
5
|
+
are selection hints only, never proof of authentication or permission.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from urllib.parse import parse_qsl, urlsplit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def append_media_token(url: str, token: str) -> str:
|
|
15
|
+
if not token or any(key == "token" for key, _ in parse_qsl(urlsplit(url).query, keep_blank_values=True)):
|
|
16
|
+
return url
|
|
17
|
+
return f"{url}{'&' if '?' in url else '?'}token={token}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def bounded_fullview_url(media: dict) -> str | None:
|
|
21
|
+
"""Return the supplied fullview only for a bounded static-image signature."""
|
|
22
|
+
try:
|
|
23
|
+
base = media.get("baseUri", "")
|
|
24
|
+
path = urlsplit(base).path
|
|
25
|
+
if not re.search(r"\.(?:png|jpe?g|webp|avif)$", path, re.I):
|
|
26
|
+
return None
|
|
27
|
+
tokens = media.get("token")
|
|
28
|
+
token = tokens[0] if isinstance(tokens, list) else tokens
|
|
29
|
+
payload = token.split(".")[1]
|
|
30
|
+
claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
|
|
31
|
+
groups = claims.get("obj")
|
|
32
|
+
if not isinstance(groups, list) or not any(
|
|
33
|
+
isinstance(group, list) and any(
|
|
34
|
+
isinstance(rule, dict) and rule.get("path") == path and any(
|
|
35
|
+
isinstance(rule.get(key), str) and re.fullmatch(r"<=\d+", rule[key])
|
|
36
|
+
for key in ("width", "height")
|
|
37
|
+
) for rule in group
|
|
38
|
+
) for group in groups
|
|
39
|
+
):
|
|
40
|
+
return None
|
|
41
|
+
types = media.get("types") or []
|
|
42
|
+
# Video sources take precedence over static posters.
|
|
43
|
+
if any(isinstance(t, dict) and t.get("t") == "video" and t.get("b") for t in types):
|
|
44
|
+
return None
|
|
45
|
+
full = next((t for t in types if isinstance(t, dict) and t.get("t") == "fullview"), {})
|
|
46
|
+
if full.get("r") not in (None, 0):
|
|
47
|
+
return None # Only index 0 was independently verified.
|
|
48
|
+
url = full.get("b")
|
|
49
|
+
if not url and full.get("c"):
|
|
50
|
+
url = base + full["c"].replace("<prettyName>", media.get("prettyName") or "image")
|
|
51
|
+
if not url:
|
|
52
|
+
return None
|
|
53
|
+
return append_media_token(url, token)
|
|
54
|
+
except (ValueError, TypeError, AttributeError, IndexError, KeyError):
|
|
55
|
+
return None
|
|
@@ -24,6 +24,7 @@ da_downloader/official_api.py
|
|
|
24
24
|
da_downloader/path.py
|
|
25
25
|
da_downloader/provider.py
|
|
26
26
|
da_downloader/targets.py
|
|
27
|
+
da_downloader/wix_media.py
|
|
27
28
|
devart_dl.egg-info/PKG-INFO
|
|
28
29
|
devart_dl.egg-info/SOURCES.txt
|
|
29
30
|
devart_dl.egg-info/dependency_links.txt
|
|
@@ -32,6 +33,7 @@ devart_dl.egg-info/requires.txt
|
|
|
32
33
|
devart_dl.egg-info/top_level.txt
|
|
33
34
|
tests/test_api.py
|
|
34
35
|
tests/test_archive.py
|
|
36
|
+
tests/test_bounded_media.py
|
|
35
37
|
tests/test_cli.py
|
|
36
38
|
tests/test_conformance.py
|
|
37
39
|
tests/test_download.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devart-dl"
|
|
7
|
-
version = "4.4.
|
|
7
|
+
version = "4.4.3"
|
|
8
8
|
description = "专业的 DeviantArt 作品批量下载工具 - Professional DeviantArt batch downloader with anti-ban features"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Real captured CDN 403 regression; provenance is beside the fixture."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import copy
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from da_downloader.api import DeviantArtAPI
|
|
12
|
+
from da_downloader.models import Deviation, DownloadItem, make_media_asset
|
|
13
|
+
from da_downloader.official_api import _descriptor_media_url, additional_media_urls
|
|
14
|
+
from da_downloader.provider import OfficialProvider, WebProvider
|
|
15
|
+
from da_downloader.targets import TargetParser
|
|
16
|
+
from da_downloader.wix_media import append_media_token, bounded_fullview_url
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_existing_query_and_signature_are_preserved():
|
|
20
|
+
assert append_media_token("https://example.test/a.jpg?download=1", "fresh") == "https://example.test/a.jpg?download=1&token=fresh"
|
|
21
|
+
signed = "https://example.test/a.jpg?download=1&token=existing"
|
|
22
|
+
assert append_media_token(signed, "fresh") == signed
|
|
23
|
+
|
|
24
|
+
SAMPLES = json.loads((Path(__file__).parent / "fixtures/deviantart-bounded-media.json").read_text(encoding="utf-8-sig"))["samples"]
|
|
25
|
+
DESCRIPTORS = [m for d in SAMPLES for m in [d["media"], *[e["media"] for e in d["extended"]["additionalMedia"]]]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize("media", DESCRIPTORS)
|
|
29
|
+
def test_captured_bounded_media_selects_fullview_in_both_paths(media, caplog):
|
|
30
|
+
full = next(t for t in media["types"] if t["t"] == "fullview")
|
|
31
|
+
expected = media["baseUri"] + full["c"].replace("<prettyName>", media["prettyName"]) + "?token=" + media["token"][0]
|
|
32
|
+
api = DeviantArtAPI({}, {})
|
|
33
|
+
assert api._get_full_view_url(media, media["baseUri"], media["token"][0], media["prettyName"]) == expected
|
|
34
|
+
assert _descriptor_media_url(media) == expected
|
|
35
|
+
assert "not the uploaded original" in caplog.text
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_extra_page_is_not_lost_or_replaced_by_small_preview():
|
|
39
|
+
result = additional_media_urls({"deviation": SAMPLES[0]})
|
|
40
|
+
assert len(result) == 1
|
|
41
|
+
assert "-fullview.jpg?token=" in result[0]
|
|
42
|
+
assert "-pre.jpg" not in result[0]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.parametrize("token", [None, [], "opaque", "a.bad.c"])
|
|
46
|
+
def test_unknown_token_preserves_original_selection(token):
|
|
47
|
+
media = {**SAMPLES[1]["media"], "token": token}
|
|
48
|
+
assert bounded_fullview_url(media) is None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_unbounded_original_gif_and_video_stay_unchanged():
|
|
52
|
+
media = copy.deepcopy(SAMPLES[1]["media"])
|
|
53
|
+
payload = media["token"][0].split(".")[1]
|
|
54
|
+
claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
|
|
55
|
+
claims["obj"][0][0].pop("width")
|
|
56
|
+
claims["obj"][0][0].pop("height")
|
|
57
|
+
payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip("=")
|
|
58
|
+
media["token"] = [f"test.{payload}.REDACTED"]
|
|
59
|
+
assert bounded_fullview_url(media) is None
|
|
60
|
+
assert _descriptor_media_url(media).startswith(media["baseUri"] + "?token=")
|
|
61
|
+
gif = {**media, "baseUri": media["baseUri"].replace(".jpg", ".gif")}
|
|
62
|
+
assert _descriptor_media_url(gif).startswith(gif["baseUri"] + "?token=")
|
|
63
|
+
video = {**SAMPLES[1]["media"], "types": [{"t": "video", "b": "https://example.test/movie.mp4", "q": "1080p"}]}
|
|
64
|
+
assert _descriptor_media_url(video).startswith("https://example.test/movie.mp4")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_no_fullview_does_not_substitute_preview():
|
|
68
|
+
media = copy.deepcopy(SAMPLES[1]["media"])
|
|
69
|
+
media["types"] = [t for t in media["types"] if t["t"] != "fullview"]
|
|
70
|
+
assert bounded_fullview_url(media) is None
|
|
71
|
+
assert _descriptor_media_url(media).startswith(media["baseUri"] + "?token=")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_original_endpoint_still_wins_and_fullview_metadata_is_explicit(monkeypatch):
|
|
75
|
+
media = SAMPLES[1]["media"]
|
|
76
|
+
deviation = Deviation(deviation_id="1403090181", title="Untitled", author="lol1729", url="https://www.deviantart.com/lol1729/art/Untitled-1403090181", media=media, is_downloadable=True, is_mature=False, deviation_type="image")
|
|
77
|
+
api = DeviantArtAPI({}, {})
|
|
78
|
+
monkeypatch.setattr(api, "_get_original_download_url", lambda _: "https://example.test/original.jpg")
|
|
79
|
+
assert api.get_download_url(deviation, "o") == "https://example.test/original.jpg"
|
|
80
|
+
item = WebProvider(api, quality="f")._to_item(deviation)
|
|
81
|
+
assert item.metadata["representation"] == "fullview"
|
|
82
|
+
assert item.metadata["original_available"] is False
|
|
83
|
+
assert item.extension == "jpg"
|
|
84
|
+
asset = make_media_asset(item.artwork_id, 0, item.media_url, extension=item.extension, representation=item.metadata["representation"])
|
|
85
|
+
assert asset.to_dict()["representation"] == "fullview"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_additional_page_metadata_does_not_claim_the_main_original_quality(monkeypatch):
|
|
89
|
+
import da_downloader.provider as provider_mod
|
|
90
|
+
|
|
91
|
+
sample = SAMPLES[0]
|
|
92
|
+
uuid = sample["extended"]["deviationUuid"]
|
|
93
|
+
monkeypatch.setattr(provider_mod, "deviation_init", lambda *a, **kw: {"deviation": sample})
|
|
94
|
+
provider = OfficialProvider(SimpleNamespace(deviation=lambda _: {}), quality="o")
|
|
95
|
+
main = DownloadItem(artwork_id=uuid, url="https://example.test/art", title="Work", author="Artist", media_url="https://example.test/original.png", extension="png", metadata={"quality": "o"})
|
|
96
|
+
monkeypatch.setattr(provider, "_item", lambda _: main)
|
|
97
|
+
items = list(provider.resolve(TargetParser.parse("1402511705")))
|
|
98
|
+
assert len(items) == 2
|
|
99
|
+
assert items[0] is main
|
|
100
|
+
assert "representation" not in main.metadata
|
|
101
|
+
assert items[1].artwork_id == uuid + "-1"
|
|
102
|
+
assert items[1].metadata["representation"] == "fullview"
|
|
103
|
+
assert items[1].metadata["original_available"] is False
|
|
104
|
+
assert items[1].extension == "jpg"
|
|
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
|