summaryrefslogtreecommitdiffstats
path: root/gallery_dl/extractor/deviantart.py
diff options
context:
space:
mode:
Diffstat (limited to 'gallery_dl/extractor/deviantart.py')
-rw-r--r--gallery_dl/extractor/deviantart.py621
1 files changed, 168 insertions, 453 deletions
diff --git a/gallery_dl/extractor/deviantart.py b/gallery_dl/extractor/deviantart.py
index 18d9867..9421096 100644
--- a/gallery_dl/extractor/deviantart.py
+++ b/gallery_dl/extractor/deviantart.py
@@ -32,20 +32,26 @@ class DeviantartExtractor(Extractor):
root = "https://www.deviantart.com"
directory_fmt = ("{category}", "{username}")
filename_fmt = "{category}_{index}_{title}.{extension}"
- cookiedomain = None
- cookienames = ("auth", "auth_secure", "userinfo")
+ cookies_domain = None
+ cookies_names = ("auth", "auth_secure", "userinfo")
_last_request = 0
def __init__(self, match):
Extractor.__init__(self, match)
+ self.user = match.group(1) or match.group(2)
+ self.offset = 0
+
+ def _init(self):
+ self.jwt = self.config("jwt", True)
self.flat = self.config("flat", True)
self.extra = self.config("extra", False)
+ self.quality = self.config("quality", "100")
self.original = self.config("original", True)
self.comments = self.config("comments", False)
- self.user = match.group(1) or match.group(2)
+
+ self.api = DeviantartOAuthAPI(self)
self.group = False
- self.offset = 0
- self.api = None
+ self._premium_cache = {}
unwatch = self.config("auto-unwatch")
if unwatch:
@@ -54,33 +60,37 @@ class DeviantartExtractor(Extractor):
else:
self.unwatch = None
+ if self.quality:
+ self.quality = ",q_{}".format(self.quality)
+
if self.original != "image":
self._update_content = self._update_content_default
else:
self._update_content = self._update_content_image
self.original = True
- self._premium_cache = {}
- self.commit_journal = {
- "html": self._commit_journal_html,
- "text": self._commit_journal_text,
- }.get(self.config("journals", "html"))
+ journals = self.config("journals", "html")
+ if journals == "html":
+ self.commit_journal = self._commit_journal_html
+ elif journals == "text":
+ self.commit_journal = self._commit_journal_text
+ else:
+ self.commit_journal = None
def skip(self, num):
self.offset += num
return num
def login(self):
- if not self._check_cookies(self.cookienames):
- username, password = self._get_auth_info()
- if not username:
- return False
- self._update_cookies(_login_impl(self, username, password))
- return True
+ if self.cookies_check(self.cookies_names):
+ return True
- def items(self):
- self.api = DeviantartOAuthAPI(self)
+ username, password = self._get_auth_info()
+ if username:
+ self.cookies_update(_login_impl(self, username, password))
+ return True
+ def items(self):
if self.user and self.config("group", True):
profile = self.api.user_profile(self.user)
self.group = not profile
@@ -117,21 +127,36 @@ class DeviantartExtractor(Extractor):
if self.original and deviation["is_downloadable"]:
self._update_content(deviation, content)
- else:
+ elif self.jwt:
self._update_token(deviation, content)
+ elif content["src"].startswith("https://images-wixmp-"):
+ if deviation["index"] <= 790677560:
+ # https://github.com/r888888888/danbooru/issues/4069
+ intermediary, count = re.subn(
+ r"(/f/[^/]+/[^/]+)/v\d+/.*",
+ r"/intermediary\1", content["src"], 1)
+ if count:
+ deviation["_fallback"] = (content["src"],)
+ content["src"] = intermediary
+ if self.quality:
+ content["src"] = re.sub(
+ r",q_\d+", self.quality, content["src"], 1)
yield self.commit(deviation, content)
elif deviation["is_downloadable"]:
content = self.api.deviation_download(deviation["deviationid"])
+ deviation["is_original"] = True
yield self.commit(deviation, content)
if "videos" in deviation and deviation["videos"]:
video = max(deviation["videos"],
key=lambda x: text.parse_int(x["quality"][:-1]))
+ deviation["is_original"] = False
yield self.commit(deviation, video)
if "flash" in deviation:
+ deviation["is_original"] = True
yield self.commit(deviation, deviation["flash"])
if self.commit_journal:
@@ -145,6 +170,7 @@ class DeviantartExtractor(Extractor):
if journal:
if self.extra:
deviation["_journal"] = journal["html"]
+ deviation["is_original"] = True
yield self.commit_journal(deviation, journal)
if not self.extra:
@@ -222,6 +248,8 @@ class DeviantartExtractor(Extractor):
target["filename"] = deviation["filename"]
deviation["target"] = target
deviation["extension"] = target["extension"] = text.ext_from_url(name)
+ if "is_original" not in deviation:
+ deviation["is_original"] = ("/v1/" not in url)
return Message.Url, url, deviation
def _commit_journal_html(self, deviation, journal):
@@ -320,9 +348,14 @@ class DeviantartExtractor(Extractor):
yield url, folder
def _update_content_default(self, deviation, content):
- public = False if "premium_folder_data" in deviation else None
+ if "premium_folder_data" in deviation or deviation.get("is_mature"):
+ public = False
+ else:
+ public = None
+
data = self.api.deviation_download(deviation["deviationid"], public)
content.update(data)
+ deviation["is_original"] = True
def _update_content_image(self, deviation, content):
data = self.api.deviation_download(deviation["deviationid"])
@@ -330,6 +363,7 @@ class DeviantartExtractor(Extractor):
mtype = mimetypes.guess_type(url, False)[0]
if mtype and mtype.startswith("image/"):
content.update(data)
+ deviation["is_original"] = True
def _update_token(self, deviation, content):
"""Replace JWT to be able to remove width/height limits
@@ -341,6 +375,9 @@ class DeviantartExtractor(Extractor):
if not sep:
return
+ # 'images-wixmp' returns 401 errors, but just 'wixmp' still works
+ url = url.replace("//images-wixmp", "//wixmp", 1)
+
# header = b'{"typ":"JWT","alg":"none"}'
payload = (
b'{"sub":"urn:app:","iss":"urn:app:","obj":[[{"path":"/f/' +
@@ -349,11 +386,12 @@ class DeviantartExtractor(Extractor):
)
deviation["_fallback"] = (content["src"],)
+ deviation["is_original"] = True
content["src"] = (
"{}?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.{}.".format(
url,
# base64 of 'header' is precomputed as 'eyJ0eX...'
- # binascii.a2b_base64(header).rstrip(b"=\n").decode(),
+ # binascii.b2a_base64(header).rstrip(b"=\n").decode(),
binascii.b2a_base64(payload).rstrip(b"=\n").decode())
)
@@ -435,18 +473,12 @@ class DeviantartUserExtractor(DeviantartExtractor):
"""Extractor for an artist's user profile"""
subcategory = "user"
pattern = BASE_PATTERN + r"/?$"
- test = (
- ("https://www.deviantart.com/shimoda7", {
- "pattern": r"/shimoda7/gallery$",
- }),
- ("https://www.deviantart.com/shimoda7", {
- "options": (("include", "all"),),
- "pattern": r"/shimoda7/"
- r"(gallery(/scraps)?|posts(/statuses)?|favourites)$",
- "count": 5,
- }),
- ("https://shimoda7.deviantart.com/"),
- )
+ example = "https://www.deviantart.com/USER"
+
+ def initialize(self):
+ pass
+
+ skip = Extractor.skip
def items(self):
base = "{}/{}/".format(self.root, self.user)
@@ -467,84 +499,7 @@ class DeviantartGalleryExtractor(DeviantartExtractor):
subcategory = "gallery"
archive_fmt = "g_{_username}_{index}.{extension}"
pattern = BASE_PATTERN + r"/gallery(?:/all|/?\?catpath=)?/?$"
- test = (
- ("https://www.deviantart.com/shimoda7/gallery/", {
- "pattern": r"https://(images-)?wixmp-[^.]+\.wixmp\.com"
- r"/f/.+/.+\.(jpg|png)\?token=.+",
- "count": ">= 30",
- "keyword": {
- "allows_comments": bool,
- "author": {
- "type": "regular",
- "usericon": str,
- "userid": "9AE51FC7-0278-806C-3FFF-F4961ABF9E2B",
- "username": "shimoda7",
- },
- "category_path": str,
- "content": {
- "filesize": int,
- "height": int,
- "src": str,
- "transparency": bool,
- "width": int,
- },
- "da_category": str,
- "date": "type:datetime",
- "deviationid": str,
- "?download_filesize": int,
- "extension": str,
- "index": int,
- "is_deleted": bool,
- "is_downloadable": bool,
- "is_favourited": bool,
- "is_mature": bool,
- "preview": {
- "height": int,
- "src": str,
- "transparency": bool,
- "width": int,
- },
- "published_time": int,
- "stats": {
- "comments": int,
- "favourites": int,
- },
- "target": dict,
- "thumbs": list,
- "title": str,
- "url": r"re:https://www.deviantart.com/shimoda7/art/[^/]+-\d+",
- "username": "shimoda7",
- },
- }),
- # group
- ("https://www.deviantart.com/yakuzafc/gallery", {
- "pattern": r"https://www.deviantart.com/yakuzafc/gallery"
- r"/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/",
- "count": ">= 15",
- }),
- # 'folders' option (#276)
- ("https://www.deviantart.com/justatest235723/gallery", {
- "count": 3,
- "options": (("metadata", 1), ("folders", 1), ("original", 0)),
- "keyword": {
- "description": str,
- "folders": list,
- "is_watching": bool,
- "license": str,
- "tags": list,
- },
- }),
- ("https://www.deviantart.com/shimoda8/gallery/", {
- "exception": exception.NotFoundError,
- }),
-
- ("https://www.deviantart.com/shimoda7/gallery"),
- ("https://www.deviantart.com/shimoda7/gallery/all"),
- ("https://www.deviantart.com/shimoda7/gallery/?catpath=/"),
- ("https://shimoda7.deviantart.com/gallery/"),
- ("https://shimoda7.deviantart.com/gallery/all/"),
- ("https://shimoda7.deviantart.com/gallery/?catpath=/"),
- )
+ example = "https://www.deviantart.com/USER/gallery/"
def deviations(self):
if self.flat and not self.group:
@@ -559,32 +514,7 @@ class DeviantartFolderExtractor(DeviantartExtractor):
directory_fmt = ("{category}", "{username}", "{folder[title]}")
archive_fmt = "F_{folder[uuid]}_{index}.{extension}"
pattern = BASE_PATTERN + r"/gallery/([^/?#]+)/([^/?#]+)"
- test = (
- # user
- ("https://www.deviantart.com/shimoda7/gallery/722019/Miscellaneous", {
- "count": 5,
- "options": (("original", False),),
- }),
- # group
- ("https://www.deviantart.com/yakuzafc/gallery/37412168/Crafts", {
- "count": ">= 4",
- "options": (("original", False),),
- }),
- # uuid
- (("https://www.deviantart.com/shimoda7/gallery"
- "/B38E3C6A-2029-6B45-757B-3C8D3422AD1A/misc"), {
- "count": 5,
- "options": (("original", False),),
- }),
- # name starts with '_', special characters (#1451)
- (("https://www.deviantart.com/justatest235723"
- "/gallery/69302698/-test-b-c-d-e-f-"), {
- "count": 1,
- "options": (("original", False),),
- }),
- ("https://shimoda7.deviantart.com/gallery/722019/Miscellaneous"),
- ("https://yakuzafc.deviantart.com/gallery/37412168/Crafts"),
- )
+ example = "https://www.deviantart.com/USER/gallery/12345/TITLE"
def __init__(self, match):
DeviantartExtractor.__init__(self, match)
@@ -613,33 +543,7 @@ class DeviantartStashExtractor(DeviantartExtractor):
subcategory = "stash"
archive_fmt = "{index}.{extension}"
pattern = r"(?:https?://)?sta\.sh/([a-z0-9]+)"
- test = (
- ("https://sta.sh/022c83odnaxc", {
- "pattern": r"https://wixmp-[^.]+\.wixmp\.com"
- r"/f/.+/.+\.png\?token=.+",
- "content": "057eb2f2861f6c8a96876b13cca1a4b7a408c11f",
- "count": 1,
- }),
- # multiple stash items
- ("https://sta.sh/21jf51j7pzl2", {
- "options": (("original", False),),
- "count": 4,
- }),
- # downloadable, but no "content" field (#307)
- ("https://sta.sh/024t4coz16mi", {
- "pattern": r"https://wixmp-[^.]+\.wixmp\.com"
- r"/f/.+/.+\.rar\?token=.+",
- "count": 1,
- }),
- # mixed folders and images (#659)
- ("https://sta.sh/215twi387vfj", {
- "options": (("original", False),),
- "count": 4,
- }),
- ("https://sta.sh/abcdefghijkl", {
- "count": 0,
- }),
- )
+ example = "https://sta.sh/abcde"
skip = Extractor.skip
@@ -684,20 +588,7 @@ class DeviantartFavoriteExtractor(DeviantartExtractor):
directory_fmt = ("{category}", "{username}", "Favourites")
archive_fmt = "f_{_username}_{index}.{extension}"
pattern = BASE_PATTERN + r"/favourites(?:/all|/?\?catpath=)?/?$"
- test = (
- ("https://www.deviantart.com/h3813067/favourites/", {
- "options": (("metadata", True), ("flat", False)), # issue #271
- "count": 1,
- }),
- ("https://www.deviantart.com/h3813067/favourites/", {
- "content": "6a7c74dc823ebbd457bdd9b3c2838a6ee728091e",
- }),
- ("https://www.deviantart.com/h3813067/favourites/all"),
- ("https://www.deviantart.com/h3813067/favourites/?catpath=/"),
- ("https://h3813067.deviantart.com/favourites/"),
- ("https://h3813067.deviantart.com/favourites/all"),
- ("https://h3813067.deviantart.com/favourites/?catpath=/"),
- )
+ example = "https://www.deviantart.com/USER/favourites/"
def deviations(self):
if self.flat:
@@ -714,20 +605,7 @@ class DeviantartCollectionExtractor(DeviantartExtractor):
"{collection[title]}")
archive_fmt = "C_{collection[uuid]}_{index}.{extension}"
pattern = BASE_PATTERN + r"/favourites/([^/?#]+)/([^/?#]+)"
- test = (
- (("https://www.deviantart.com/pencilshadings/favourites"
- "/70595441/3D-Favorites"), {
- "count": ">= 15",
- "options": (("original", False),),
- }),
- (("https://www.deviantart.com/pencilshadings/favourites"
- "/F050486B-CB62-3C66-87FB-1105A7F6379F/3D Favorites"), {
- "count": ">= 15",
- "options": (("original", False),),
- }),
- ("https://pencilshadings.deviantart.com"
- "/favourites/70595441/3D-Favorites"),
- )
+ example = "https://www.deviantart.com/USER/favourites/12345/TITLE"
def __init__(self, match):
DeviantartExtractor.__init__(self, match)
@@ -758,24 +636,7 @@ class DeviantartJournalExtractor(DeviantartExtractor):
directory_fmt = ("{category}", "{username}", "Journal")
archive_fmt = "j_{_username}_{index}.{extension}"
pattern = BASE_PATTERN + r"/(?:posts(?:/journals)?|journal)/?(?:\?.*)?$"
- test = (
- ("https://www.deviantart.com/angrywhitewanker/posts/journals/", {
- "url": "38db2a0d3a587a7e0f9dba7ff7d274610ebefe44",
- }),
- ("https://www.deviantart.com/angrywhitewanker/posts/journals/", {
- "url": "b2a8e74d275664b1a4acee0fca0a6fd33298571e",
- "options": (("journals", "text"),),
- }),
- ("https://www.deviantart.com/angrywhitewanker/posts/journals/", {
- "count": 0,
- "options": (("journals", "none"),),
- }),
- ("https://www.deviantart.com/shimoda7/posts/"),
- ("https://www.deviantart.com/shimoda7/journal/"),
- ("https://www.deviantart.com/shimoda7/journal/?catpath=/"),
- ("https://shimoda7.deviantart.com/journal/"),
- ("https://shimoda7.deviantart.com/journal/?catpath=/"),
- )
+ example = "https://www.deviantart.com/USER/posts/journals/"
def deviations(self):
return self.api.browse_user_journals(self.user, self.offset)
@@ -788,45 +649,7 @@ class DeviantartStatusExtractor(DeviantartExtractor):
filename_fmt = "{category}_{index}_{title}_{date}.{extension}"
archive_fmt = "S_{_username}_{index}.{extension}"
pattern = BASE_PATTERN + r"/posts/statuses"
- test = (
- ("https://www.deviantart.com/t1na/posts/statuses", {
- "count": 0,
- }),
- ("https://www.deviantart.com/justgalym/posts/statuses", {
- "count": 4,
- "url": "bf4c44c0c60ff2648a880f4c3723464ad3e7d074",
- }),
- # shared deviation
- ("https://www.deviantart.com/justgalym/posts/statuses", {
- "options": (("journals", "none"),),
- "count": 1,
- "pattern": r"https://images-wixmp-\w+\.wixmp\.com/f"
- r"/[^/]+/[^.]+\.jpg\?token=",
- }),
- # shared sta.sh item
- ("https://www.deviantart.com/vanillaghosties/posts/statuses", {
- "options": (("journals", "none"), ("original", False)),
- "range": "5-",
- "count": 1,
- "keyword": {
- "index" : int,
- "index_base36": "re:^[0-9a-z]+$",
- "url" : "re:^https://sta.sh",
- },
- }),
- # "deleted" deviations in 'items'
- ("https://www.deviantart.com/AndrejSKalin/posts/statuses", {
- "options": (("journals", "none"), ("original", 0),
- ("image-filter", "deviationid[:8] == '147C8B03'")),
- "count": 2,
- "archive": False,
- "keyword": {"deviationid": "147C8B03-7D34-AE93-9241-FA3C6DBBC655"}
- }),
- ("https://www.deviantart.com/justgalym/posts/statuses", {
- "options": (("journals", "text"),),
- "url": "c8744f7f733a3029116607b826321233c5ca452d",
- }),
- )
+ example = "https://www.deviantart.com/USER/posts/statuses/"
def deviations(self):
for status in self.api.user_statuses(self.user, self.offset):
@@ -890,19 +713,7 @@ class DeviantartPopularExtractor(DeviantartExtractor):
r"(?:deviations/?)?\?order=(popular-[^/?#]+)"
r"|((?:[\w-]+/)*)(popular-[^/?#]+)"
r")/?(?:\?([^#]*))?")
- test = (
- ("https://www.deviantart.com/?order=popular-all-time", {
- "options": (("original", False),),
- "range": "1-30",
- "count": 30,
- }),
- ("https://www.deviantart.com/popular-24-hours/?q=tree+house", {
- "options": (("original", False),),
- "range": "1-30",
- "count": 30,
- }),
- ("https://www.deviantart.com/artisan/popular-all-time/?q=tree"),
- )
+ example = "https://www.deviantart.com/popular-24-hours/"
def __init__(self, match):
DeviantartExtractor.__init__(self, match)
@@ -947,11 +758,7 @@ class DeviantartTagExtractor(DeviantartExtractor):
directory_fmt = ("{category}", "Tags", "{search_tags}")
archive_fmt = "T_{search_tags}_{index}.{extension}"
pattern = r"(?:https?://)?www\.deviantart\.com/tag/([^/?#]+)"
- test = ("https://www.deviantart.com/tag/nature", {
- "options": (("original", False),),
- "range": "1-30",
- "count": 30,
- })
+ example = "https://www.deviantart.com/tag/TAG"
def __init__(self, match):
DeviantartExtractor.__init__(self, match)
@@ -970,10 +777,7 @@ class DeviantartWatchExtractor(DeviantartExtractor):
subcategory = "watch"
pattern = (r"(?:https?://)?(?:www\.)?deviantart\.com"
r"/(?:watch/deviations|notifications/watch)()()")
- test = (
- ("https://www.deviantart.com/watch/deviations"),
- ("https://www.deviantart.com/notifications/watch"),
- )
+ example = "https://www.deviantart.com/watch/deviations"
def deviations(self):
return self.api.browse_deviantsyouwatch()
@@ -983,7 +787,7 @@ class DeviantartWatchPostsExtractor(DeviantartExtractor):
"""Extractor for Posts from watched users"""
subcategory = "watch-posts"
pattern = r"(?:https?://)?(?:www\.)?deviantart\.com/watch/posts()()"
- test = ("https://www.deviantart.com/watch/posts",)
+ example = "https://www.deviantart.com/watch/posts"
def deviations(self):
return self.api.browse_posts_deviantsyouwatch()
@@ -1001,100 +805,7 @@ class DeviantartDeviationExtractor(DeviantartExtractor):
r"(?:view/|deviation/|view(?:-full)?\.php/*\?(?:[^#]+&)?id=)"
r"(\d+)" # bare deviation ID without slug
r"|(?:https?://)?fav\.me/d([0-9a-z]+)") # base36
- test = (
- (("https://www.deviantart.com/shimoda7/art/For-the-sake-10073852"), {
- "options": (("original", 0),),
- "content": "6a7c74dc823ebbd457bdd9b3c2838a6ee728091e",
- }),
- ("https://www.deviantart.com/zzz/art/zzz-1234567890", {
- "exception": exception.NotFoundError,
- }),
- (("https://www.deviantart.com/myria-moon/art/Aime-Moi-261986576"), {
- "options": (("comments", True),),
- "keyword": {"comments": list},
- "pattern": r"https://wixmp-[^.]+\.wixmp\.com"
- r"/f/.+/.+\.jpg\?token=.+",
- }),
- # wixmp URL rewrite
- (("https://www.deviantart.com/citizenfresh/art/Hverarond-789295466"), {
- "pattern": (r"https://images-wixmp-\w+\.wixmp\.com/f"
- r"/[^/]+/[^.]+\.jpg\?token="),
- }),
- # GIF (#242)
- (("https://www.deviantart.com/skatergators/art/COM-Moni-781571783"), {
- "pattern": r"https://wixmp-\w+\.wixmp\.com/f/03fd2413-efe9-4e5c-"
- r"8734-2b72605b3fbb/dcxbsnb-1bbf0b38-42af-4070-8878-"
- r"f30961955bec\.gif\?token=ey...",
- }),
- # Flash animation with GIF preview (#1731)
- ("https://www.deviantart.com/yuumei/art/Flash-Comic-214724929", {
- "pattern": r"https://wixmp-[^.]+\.wixmp\.com"
- r"/f/.+/.+\.swf\?token=.+",
- "keyword": {
- "filename": "flash_comic_tutorial_by_yuumei-d3juatd",
- "extension": "swf",
- },
- }),
- # sta.sh URLs from description (#302)
- (("https://www.deviantart.com/uotapo/art/INANAKI-Memo-590297498"), {
- "options": (("extra", 1), ("original", 0)),
- "pattern": DeviantartStashExtractor.pattern,
- "range": "2-",
- "count": 4,
- }),
- # sta.sh URL from deviation["text_content"]["body"]["features"]
- (("https://www.deviantart.com"
- "/cimar-wildehopps/art/Honorary-Vixen-859809305"), {
- "options": (("extra", 1),),
- "pattern": ("text:<!DOCTYPE html>\n|" +
- DeviantartStashExtractor.pattern),
- "count": 2,
- }),
- # journal
- ("https://www.deviantart.com/shimoda7/journal/ARTility-583755752", {
- "url": "d34b2c9f873423e665a1b8ced20fcb75951694a3",
- "pattern": "text:<!DOCTYPE html>\n",
- }),
- # journal-like post with isJournal == False (#419)
- ("https://www.deviantart.com/gliitchlord/art/brashstrokes-812942668", {
- "url": "e2e0044bd255304412179b6118536dbd9bb3bb0e",
- "pattern": "text:<!DOCTYPE html>\n",
- }),
- # /view/ URLs
- ("https://deviantart.com/view/904858796/", {
- "content": "8770ec40ad1c1d60f6b602b16301d124f612948f",
- }),
- ("http://www.deviantart.com/view/890672057", {
- "content": "1497e13d925caeb13a250cd666b779a640209236",
- }),
- ("https://www.deviantart.com/view/706871727", {
- "content": "3f62ae0c2fca2294ac28e41888ea06bb37c22c65",
- }),
- ("https://www.deviantart.com/view/1", {
- "exception": exception.NotFoundError,
- }),
- # /deviation/ (#3558)
- ("https://www.deviantart.com/deviation/817215762"),
- # fav.me (#3558)
- ("https://fav.me/ddijrpu", {
- "count": 1,
- }),
- ("https://fav.me/dddd", {
- "exception": exception.NotFoundError,
- }),
- # old-style URLs
- ("https://shimoda7.deviantart.com"
- "/art/For-the-sake-of-a-memory-10073852"),
- ("https://myria-moon.deviantart.com"
- "/art/Aime-Moi-part-en-vadrouille-261986576"),
- ("https://zzz.deviantart.com/art/zzz-1234567890"),
- # old /view/ URLs from the Wayback Machine
- ("https://www.deviantart.com/view.php?id=14864502"),
- ("http://www.deviantart.com/view-full.php?id=100842"),
-
- ("https://www.fxdeviantart.com/zzz/art/zzz-1234567890"),
- ("https://www.fxdeviantart.com/view/1234567890"),
- )
+ example = "https://www.deviantart.com/UsER/art/TITLE-12345"
skip = Extractor.skip
@@ -1105,11 +816,14 @@ class DeviantartDeviationExtractor(DeviantartExtractor):
match.group(4) or match.group(5) or id_from_base36(match.group(6))
def deviations(self):
- url = "{}/{}/{}/{}".format(
- self.root, self.user or "u", self.type or "art", self.deviation_id)
+ if self.user:
+ url = "{}/{}/{}/{}".format(
+ self.root, self.user, self.type or "art", self.deviation_id)
+ else:
+ url = "{}/view/{}/".format(self.root, self.deviation_id)
- uuid = text.extract(self._limited_request(url).text,
- '"deviationUuid\\":\\"', '\\')[0]
+ uuid = text.extr(self._limited_request(url).text,
+ '"deviationUuid\\":\\"', '\\')
if not uuid:
raise exception.NotFoundError("deviation")
return (self.api.deviation(uuid),)
@@ -1120,15 +834,9 @@ class DeviantartScrapsExtractor(DeviantartExtractor):
subcategory = "scraps"
directory_fmt = ("{category}", "{username}", "Scraps")
archive_fmt = "s_{_username}_{index}.{extension}"
- cookiedomain = ".deviantart.com"
+ cookies_domain = ".deviantart.com"
pattern = BASE_PATTERN + r"/gallery/(?:\?catpath=)?scraps\b"
- test = (
- ("https://www.deviantart.com/shimoda7/gallery/scraps", {
- "count": 12,
- }),
- ("https://www.deviantart.com/shimoda7/gallery/?catpath=scraps"),
- ("https://shimoda7.deviantart.com/gallery/?catpath=scraps"),
- )
+ example = "https://www.deviantart.com/USER/gallery/scraps"
def deviations(self):
self.login()
@@ -1143,14 +851,10 @@ class DeviantartSearchExtractor(DeviantartExtractor):
subcategory = "search"
directory_fmt = ("{category}", "Search", "{search_tags}")
archive_fmt = "Q_{search_tags}_{index}.{extension}"
- cookiedomain = ".deviantart.com"
+ cookies_domain = ".deviantart.com"
pattern = (r"(?:https?://)?www\.deviantart\.com"
r"/search(?:/deviations)?/?\?([^#]+)")
- test = (
- ("https://www.deviantart.com/search?q=tree"),
- ("https://www.deviantart.com/search/deviations?order=popular-1-week"),
- )
-
+ example = "https://www.deviantart.com/search?q=QUERY"
skip = Extractor.skip
def __init__(self, match):
@@ -1173,11 +877,6 @@ class DeviantartSearchExtractor(DeviantartExtractor):
def _search_html(self, params):
url = self.root + "/search"
- deviation = {
- "deviationId": None,
- "author": {"username": "u"},
- "isJournal": False,
- }
while True:
response = self.request(url, params=params)
@@ -1186,13 +885,15 @@ class DeviantartSearchExtractor(DeviantartExtractor):
raise exception.StopExtraction("HTTP redirect to login page")
page = response.text
- items , pos = text.rextract(page, r'\"items\":[', ']')
- cursor, pos = text.extract(page, r'\"cursor\":\"', '\\', pos)
-
- for deviation_id in items.split(","):
- deviation["deviationId"] = deviation_id
- yield deviation
+ for dev in DeviantartDeviationExtractor.pattern.findall(
+ page)[2::3]:
+ yield {
+ "deviationId": dev[3],
+ "author": {"username": dev[0]},
+ "isJournal": dev[2] == "journal",
+ }
+ cursor = text.extr(page, r'\"cursor\":\"', '\\',)
if not cursor:
return
params["cursor"] = cursor
@@ -1202,15 +903,9 @@ class DeviantartGallerySearchExtractor(DeviantartExtractor):
"""Extractor for deviantart gallery searches"""
subcategory = "gallery-search"
archive_fmt = "g_{_username}_{index}.{extension}"
- cookiedomain = ".deviantart.com"
+ cookies_domain = ".deviantart.com"
pattern = BASE_PATTERN + r"/gallery/?\?(q=[^#]+)"
- test = (
- ("https://www.deviantart.com/shimoda7/gallery?q=memory", {
- "options": (("original", 0),),
- "content": "6a7c74dc823ebbd457bdd9b3c2838a6ee728091e",
- }),
- ("https://www.deviantart.com/shimoda7/gallery?q=memory&sort=popular"),
- )
+ example = "https://www.deviantart.com/USER/gallery?q=QUERY"
def __init__(self, match):
DeviantartExtractor.__init__(self, match)
@@ -1220,14 +915,12 @@ class DeviantartGallerySearchExtractor(DeviantartExtractor):
self.login()
eclipse_api = DeviantartEclipseAPI(self)
- info = eclipse_api.user_info(self.user)
-
query = text.parse_query(self.query)
self.search = query["q"]
return self._eclipse_to_oauth(
eclipse_api, eclipse_api.galleries_search(
- info["user"]["userId"],
+ self.user,
self.search,
self.offset,
query.get("sort", "most-recent"),
@@ -1242,11 +935,7 @@ class DeviantartFollowingExtractor(DeviantartExtractor):
"""Extractor for user's watched users"""
subcategory = "following"
pattern = BASE_PATTERN + "/about#watching$"
- test = ("https://www.deviantart.com/shimoda7/about#watching", {
- "pattern": DeviantartUserExtractor.pattern,
- "range": "1-50",
- "count": 50,
- })
+ example = "https://www.deviantart.com/USER/about#watching"
def items(self):
eclipse_api = DeviantartEclipseAPI(self)
@@ -1393,7 +1082,12 @@ class DeviantartOAuthAPI():
def deviation(self, deviation_id, public=None):
"""Query and return info about a single Deviation"""
endpoint = "/deviation/" + deviation_id
+
deviation = self._call(endpoint, public=public)
+ if deviation.get("is_mature") and public is None and \
+ self.refresh_token_key:
+ deviation = self._call(endpoint, public=False)
+
if self.metadata:
self._metadata((deviation,))
if self.folders:
@@ -1549,8 +1243,12 @@ class DeviantartOAuthAPI():
return data
if not fatal and status != 429:
return None
- if data.get("error_description") == "User not found.":
+
+ error = data.get("error_description")
+ if error == "User not found.":
raise exception.NotFoundError("user or group")
+ if error == "Deviation not downloadable.":
+ raise exception.AuthorizationError()
self.log.debug(response.text)
msg = "API responded with {} {}".format(
@@ -1574,6 +1272,17 @@ class DeviantartOAuthAPI():
self.log.error(msg)
return data
+ def _switch_tokens(self, results, params):
+ if len(results) < params["limit"]:
+ return True
+
+ if not self.extractor.jwt:
+ for item in results:
+ if item.get("is_mature"):
+ return True
+
+ return False
+
def _pagination(self, endpoint, params,
extend=True, public=None, unpack=False, key="results"):
warn = True
@@ -1592,7 +1301,7 @@ class DeviantartOAuthAPI():
results = [item["journal"] for item in results
if "journal" in item]
if extend:
- if public and len(results) < params["limit"]:
+ if public and self._switch_tokens(results, params):
if self.refresh_token_key:
self.log.debug("Switching to private access token")
public = False
@@ -1600,9 +1309,10 @@ class DeviantartOAuthAPI():
elif data["has_more"] and warn:
warn = False
self.log.warning(
- "Private deviations detected! Run 'gallery-dl "
- "oauth:deviantart' and follow the instructions to "
- "be able to access them.")
+ "Private or mature deviations detected! "
+ "Run 'gallery-dl oauth:deviantart' and follow the "
+ "instructions to be able to access them.")
+
# "statusid" cannot be used instead
if results and "deviationid" in results[0]:
if self.metadata:
@@ -1711,70 +1421,70 @@ class DeviantartEclipseAPI():
self.request = self.extractor._limited_request
self.csrf_token = None
- def deviation_extended_fetch(self, deviation_id, user=None, kind=None):
- endpoint = "/da-browse/shared_api/deviation/extended_fetch"
+ def deviation_extended_fetch(self, deviation_id, user, kind=None):
+ endpoint = "/_puppy/dadeviation/init"
params = {
- "deviationid" : deviation_id,
- "username" : user,
- "type" : kind,
- "include_session": "false",
+ "deviationid" : deviation_id,
+ "username" : user,
+ "type" : kind,
+ "include_session" : "false",
+ "expand" : "deviation.related",
+ "da_minor_version": "20230710",
}
return self._call(endpoint, params)
- def gallery_scraps(self, user, offset=None):
- endpoint = "/da-user-profile/api/gallery/contents"
+ def gallery_scraps(self, user, offset=0):
+ endpoint = "/_puppy/dashared/gallection/contents"
params = {
"username" : user,
+ "type" : "gallery",
"offset" : offset,
"limit" : 24,
"scraps_folder": "true",
}
return self._pagination(endpoint, params)
- def galleries_search(self, user_id, query,
- offset=None, order="most-recent"):
- endpoint = "/shared_api/galleries/search"
+ def galleries_search(self, user, query, offset=0, order="most-recent"):
+ endpoint = "/_puppy/dashared/gallection/search"
params = {
- "userid": user_id,
- "order" : order,
- "q" : query,
- "offset": offset,
- "limit" : 24,
+ "username": user,
+ "type" : "gallery",
+ "order" : order,
+ "q" : query,
+ "offset" : offset,
+ "limit" : 24,
}
return self._pagination(endpoint, params)
def search_deviations(self, params):
- endpoint = "/da-browse/api/networkbar/search/deviations"
+ endpoint = "/_puppy/dabrowse/search/deviations"
return self._pagination(endpoint, params, key="deviations")
def user_info(self, user, expand=False):
- endpoint = "/shared_api/user/info"
+ endpoint = "/_puppy/dauserprofile/init/about"
params = {"username": user}
- if expand:
- params["expand"] = "user.stats,user.profile,user.watch"
return self._call(endpoint, params)
- def user_watching(self, user, offset=None):
- endpoint = "/da-user-profile/api/module/watching"
+ def user_watching(self, user, offset=0):
+ gruserid, moduleid = self._ids_watching(user)
+
+ endpoint = "/_puppy/gruser/module/watching"
params = {
- "username": user,
- "moduleid": self._module_id_watching(user),
- "offset" : offset,
- "limit" : 24,
+ "gruserid" : gruserid,
+ "gruser_typeid": "4",
+ "username" : user,
+ "moduleid" : moduleid,
+ "offset" : offset,
+ "limit" : 24,
}
return self._pagination(endpoint, params)
def _call(self, endpoint, params):
- url = "https://www.deviantart.com/_napi" + endpoint
- headers = {"Referer": "https://www.deviantart.com/"}
+ url = "https://www.deviantart.com" + endpoint
params["csrf_token"] = self.csrf_token or self._fetch_csrf_token()
- response = self.request(
- url, params=params, headers=headers, fatal=None)
+ response = self.request(url, params=params, fatal=None)
- if response.status_code == 404:
- raise exception.StopExtraction(
- "Your account must use the Eclipse interface.")
try:
return response.json()
except Exception:
@@ -1812,14 +1522,19 @@ class DeviantartEclipseAPI():
else:
params["offset"] = int(params["offset"]) + len(results)
- def _module_id_watching(self, user):
+ def _ids_watching(self, user):
url = "{}/{}/about".format(self.extractor.root, user)
page = self.request(url).text
- pos = page.find('\\"type\\":\\"watching\\"')
+
+ gruserid, pos = text.extract(page, ' data-userid="', '"')
+
+ pos = page.find('\\"type\\":\\"watching\\"', pos)
if pos < 0:
raise exception.NotFoundError("module")
+ moduleid = text.rextract(page, '\\"id\\":', ',', pos)[0].strip('" ')
+
self._fetch_csrf_token(page)
- return text.rextract(page, '\\"id\\":', ',', pos)[0].strip('" ')
+ return gruserid, moduleid
def _fetch_csrf_token(self, page=None):
if page is None:
@@ -1866,7 +1581,7 @@ def _login_impl(extr, username, password):
return {
cookie.name: cookie.value
- for cookie in extr.session.cookies
+ for cookie in extr.cookies
}