devart-dl 4.3.1__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.1/devart_dl.egg-info → devart_dl-4.3.2}/PKG-INFO +1 -1
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/official_api.py +53 -18
- {devart_dl-4.3.1 → devart_dl-4.3.2/devart_dl.egg-info}/PKG-INFO +1 -1
- {devart_dl-4.3.1 → devart_dl-4.3.2}/pyproject.toml +1 -1
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_official_provider.py +82 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/.env.example +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/LICENSE +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/MANIFEST.in +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/README.en.md +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/README.md +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/bin/devart-dl +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/bin/install.sh +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/__init__.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/api.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/archive.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/auth.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/cli.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/da_cookie_login.mjs +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/download.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/errors.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/http.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/manager.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/models.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/oauth.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/path.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/provider.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/da_downloader/targets.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/devart_dl.egg-info/SOURCES.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/devart_dl.egg-info/dependency_links.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/devart_dl.egg-info/entry_points.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/devart_dl.egg-info/requires.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/devart_dl.egg-info/top_level.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/requirements.txt +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/setup.cfg +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/setup.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_api.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_archive.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_cli.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_conformance.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_download.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_http_downloader.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_manager.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_path.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_provider.py +0 -0
- {devart_dl-4.3.1 → devart_dl-4.3.2}/tests/test_targets.py +0 -0
|
@@ -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"]
|
|
@@ -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"
|
|
@@ -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
|
|
File without changes
|