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.py192
1 files changed, 137 insertions, 55 deletions
diff --git a/gallery_dl/extractor/deviantart.py b/gallery_dl/extractor/deviantart.py
index 900fde8..b4ac742 100644
--- a/gallery_dl/extractor/deviantart.py
+++ b/gallery_dl/extractor/deviantart.py
@@ -41,6 +41,7 @@ class DeviantartExtractor(Extractor):
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.group = False
self.api = None
@@ -66,8 +67,6 @@ class DeviantartExtractor(Extractor):
def items(self):
self.api = DeviantartOAuthAPI(self)
- if not self.api.refresh_token_key:
- self._fetch_premium = self._fetch_premium_notoken
if self.user:
profile = self.api.user_profile(self.user)
@@ -78,7 +77,6 @@ class DeviantartExtractor(Extractor):
else:
self.user = profile["user"]["username"]
- yield Message.Version, 1
for deviation in self.deviations():
if isinstance(deviation, tuple):
url, data = deviation
@@ -86,8 +84,10 @@ class DeviantartExtractor(Extractor):
continue
if "premium_folder_data" in deviation:
- if not self._fetch_premium(deviation):
+ data = self._fetch_premium(deviation)
+ if not data:
continue
+ deviation.update(data)
self.prepare(deviation)
yield Message.Directory, deviation
@@ -117,7 +117,7 @@ class DeviantartExtractor(Extractor):
content = self.api.deviation_download(deviation["deviationid"])
yield self.commit(deviation, content)
- if "videos" in deviation:
+ if "videos" in deviation and deviation["videos"]:
video = max(deviation["videos"],
key=lambda x: text.parse_int(x["quality"][:-1]))
yield self.commit(deviation, video)
@@ -163,6 +163,12 @@ class DeviantartExtractor(Extractor):
deviation["date"] = text.parse_timestamp(
deviation["published_time"])
+ if self.comments:
+ deviation["comments"] = (
+ self.api.comments_deviation(deviation["deviationid"])
+ if deviation["stats"]["comments"] else ()
+ )
+
# filename metadata
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
deviation["index_base36"] = util.bencode(deviation["index"], alphabet)
@@ -307,39 +313,48 @@ class DeviantartExtractor(Extractor):
self.wait(seconds=180)
def _fetch_premium(self, deviation):
- cache = self._premium_cache
-
- if deviation["deviationid"] not in cache:
+ try:
+ return self._premium_cache[deviation["deviationid"]]
+ except KeyError:
+ pass
- # check accessibility
+ # check accessibility
+ if self.api.refresh_token_key:
dev = self.api.deviation(deviation["deviationid"], False)
has_access = dev["premium_folder_data"]["has_access"]
-
- if has_access:
- self.log.info("Fetching premium folder data")
- else:
- self.log.warning("Unable to access premium content (type: %s)",
- dev["premium_folder_data"]["type"])
- # fill cache
- for dev in self.api.gallery(
- deviation["author"]["username"],
- deviation["premium_folder_data"]["gallery_id"],
- public=False,
- ):
- cache[dev["deviationid"]] = dev if has_access else None
-
- data = cache[deviation["deviationid"]]
- if data:
- deviation.update(data)
- return True
- return False
-
- def _fetch_premium_notoken(self, deviation):
- if not self._premium_cache:
+ username = dev["author"]["username"]
+ folder = dev["premium_folder_data"]
+
+ if not has_access and folder["type"] == "watchers" and \
+ self.config("auto-watch"):
+ if self.api.user_friends_watch(username):
+ has_access = True
+ self.log.info(
+ "Watching %s for premium folder access", username)
+ else:
+ self.log.warning(
+ "Error when trying to watch %s. "
+ "Try again with a new refresh-token", username)
+ else:
self.log.warning(
"Unable to access premium content (no refresh-token)")
- self._premium_cache = True
- return False
+ self._fetch_premium = lambda _: None
+ return None
+
+ if has_access:
+ self.log.info("Fetching premium folder data")
+ else:
+ self.log.warning("Unable to access premium content (type: %s)",
+ folder["type"])
+ self._fetch_premium = lambda _: None
+ return None
+
+ # fill cache
+ cache = self._premium_cache
+ for dev in self.api.gallery(
+ username, folder["gallery_id"], public=False):
+ cache[dev["deviationid"]] = dev
+ return cache[deviation["deviationid"]]
class DeviantartUserExtractor(DeviantartExtractor):
@@ -754,6 +769,30 @@ class DeviantartPopularExtractor(DeviantartExtractor):
deviation["popular"] = self.popular
+class DeviantartTagExtractor(DeviantartExtractor):
+ """Extractor for deviations from tag searches"""
+ subcategory = "tag"
+ 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,
+ })
+
+ def __init__(self, match):
+ DeviantartExtractor.__init__(self, match)
+ self.tag = text.unquote(match.group(1))
+
+ def deviations(self):
+ return self.api.browse_tags(self.tag, self.offset)
+
+ def prepare(self, deviation):
+ DeviantartExtractor.prepare(self, deviation)
+ deviation["search_tags"] = self.tag
+
+
class DeviantartWatchExtractor(DeviantartExtractor):
"""Extractor for Deviations from watched users"""
subcategory = "watch"
@@ -795,7 +834,9 @@ class DeviantartDeviationExtractor(DeviantartExtractor):
"exception": exception.NotFoundError,
}),
(("https://www.deviantart.com/myria-moon/art/Aime-Moi-261986576"), {
+ "options": (("comments", True),),
"pattern": r"https://api-da\.wixmp\.com/_api/download/file",
+ "keyword": {"comments": list},
}),
# wixmp URL rewrite
(("https://www.deviantart.com/citizenfresh/art/Hverarond-789295466"), {
@@ -890,15 +931,9 @@ class DeviantartScrapsExtractor(DeviantartExtractor):
)
cookiedomain = ".deviantart.com"
cookienames = ("auth", "auth_secure", "userinfo")
- _warning = True
def deviations(self):
eclipse_api = DeviantartEclipseAPI(self)
- if self._warning:
- DeviantartScrapsExtractor._warning = False
- if not self._check_cookies(self.cookienames):
- self.log.warning(
- "No session cookies set: Unable to fetch mature scraps.")
for obj in eclipse_api.gallery_scraps(self.user, self.offset):
deviation = obj["deviation"]
@@ -924,7 +959,6 @@ class DeviantartFollowingExtractor(DeviantartExtractor):
def items(self):
eclipse_api = DeviantartEclipseAPI(self)
- yield Message.Version, 1
for user in eclipse_api.user_watching(self.user, self.offset):
url = "{}/{}".format(self.root, user["username"])
user["_extractor"] = DeviantartUserExtractor
@@ -1003,6 +1037,17 @@ class DeviantartOAuthAPI():
}
return self._pagination(endpoint, params)
+ def browse_tags(self, tag, offset=0):
+ """ Browse a tag """
+ endpoint = "browse/tags"
+ params = {
+ "tag" : tag,
+ "offset" : offset,
+ "limit" : 50,
+ "mature_content": self.mature,
+ }
+ return self._pagination(endpoint, params)
+
def browse_user_journals(self, username, offset=0):
"""Yield all journal entries of a specific user"""
endpoint = "browse/user/journals"
@@ -1023,7 +1068,14 @@ class DeviantartOAuthAPI():
endpoint = "collections/folders"
params = {"username": username, "offset": offset, "limit": 50,
"mature_content": self.mature}
- return self._pagination_folders(endpoint, params)
+ return self._pagination_list(endpoint, params)
+
+ def comments_deviation(self, deviation_id, offset=0):
+ """Fetch comments posted on a deviation"""
+ endpoint = "comments/deviation/" + deviation_id
+ params = {"maxdepth": "5", "offset": offset, "limit": 50,
+ "mature_content": self.mature}
+ return self._pagination_list(endpoint, params=params, key="thread")
def deviation(self, deviation_id, public=True):
"""Query and return info about a single Deviation"""
@@ -1039,13 +1091,13 @@ class DeviantartOAuthAPI():
"""Get extended content of a single Deviation"""
endpoint = "deviation/content"
params = {"deviationid": deviation_id}
- return self._call(endpoint, params, public=public)
+ return self._call(endpoint, params=params, public=public)
def deviation_download(self, deviation_id, public=True):
"""Get the original file download (if allowed)"""
endpoint = "deviation/download/" + deviation_id
params = {"mature_content": self.mature}
- return self._call(endpoint, params, public=public)
+ return self._call(endpoint, params=params, public=public)
def deviation_metadata(self, deviations):
""" Fetch deviation metadata for a set of deviations"""
@@ -1056,7 +1108,7 @@ class DeviantartOAuthAPI():
for num, deviation in enumerate(deviations)
)
params = {"mature_content": self.mature}
- return self._call(endpoint, params)["metadata"]
+ return self._call(endpoint, params=params)["metadata"]
def gallery(self, username, folder_id, offset=0, extend=True, public=True):
"""Yield all Deviation-objects contained in a gallery folder"""
@@ -1078,7 +1130,7 @@ class DeviantartOAuthAPI():
endpoint = "gallery/folders"
params = {"username": username, "offset": offset, "limit": 50,
"mature_content": self.mature}
- return self._pagination_folders(endpoint, params)
+ return self._pagination_list(endpoint, params)
@memcache(keyarg=1)
def user_profile(self, username):
@@ -1086,6 +1138,29 @@ class DeviantartOAuthAPI():
endpoint = "user/profile/" + username
return self._call(endpoint, fatal=False)
+ def user_friends_watch(self, username):
+ """Watch a user"""
+ endpoint = "user/friends/watch/" + username
+ data = {
+ "watch[friend]" : "0",
+ "watch[deviations]" : "0",
+ "watch[journals]" : "0",
+ "watch[forum_threads]": "0",
+ "watch[critiques]" : "0",
+ "watch[scraps]" : "0",
+ "watch[activity]" : "0",
+ "watch[collections]" : "0",
+ "mature_content" : self.mature,
+ }
+ return self._call(
+ endpoint, method="POST", data=data, public=False, fatal=False)
+
+ def user_friends_unwatch(self, username):
+ """Unwatch a user"""
+ endpoint = "user/friends/unwatch/" + username
+ return self._call(
+ endpoint, method="POST", public=False, fatal=False)
+
def authenticate(self, refresh_token_key):
"""Authenticate the application by requesting an access token"""
self.headers["Authorization"] = \
@@ -1117,16 +1192,18 @@ class DeviantartOAuthAPI():
refresh_token_key, data["refresh_token"])
return "Bearer " + data["access_token"]
- def _call(self, endpoint, params=None, fatal=True, public=True):
+ def _call(self, endpoint, fatal=True, public=True, **kwargs):
"""Call an API endpoint"""
url = "https://www.deviantart.com/api/v1/oauth2/" + endpoint
+ kwargs["fatal"] = None
+
while True:
if self.delay:
time.sleep(self.delay)
self.authenticate(None if public else self.refresh_token_key)
- response = self.extractor.request(
- url, headers=self.headers, params=params, fatal=None)
+ kwargs["headers"] = self.headers
+ response = self.extractor.request(url, **kwargs)
data = response.json()
status = response.status_code
@@ -1151,14 +1228,14 @@ class DeviantartOAuthAPI():
return data
def _pagination(self, endpoint, params,
- extend=True, public=True, unpack=False):
+ extend=True, public=True, unpack=False, key="results"):
warn = True
while True:
- data = self._call(endpoint, params, public=public)
- if "results" not in data:
+ data = self._call(endpoint, params=params, public=public)
+ if key not in data:
self.log.error("Unexpected API response: %s", data)
return
- results = data["results"]
+ results = data[key]
if unpack:
results = [item["journal"] for item in results
@@ -1183,11 +1260,16 @@ class DeviantartOAuthAPI():
if not data["has_more"]:
return
- params["offset"] = data["next_offset"]
+ if "next_cursor" in data:
+ params["offset"] = None
+ params["cursor"] = data["next_cursor"]
+ else:
+ params["offset"] = data["next_offset"]
+ params["cursor"] = None
- def _pagination_folders(self, endpoint, params):
+ def _pagination_list(self, endpoint, params, key="results"):
result = []
- result.extend(self._pagination(endpoint, params, False))
+ result.extend(self._pagination(endpoint, params, False, key=key))
return result
def _metadata(self, deviations):