diff --git a/docs/supportedsites.md b/docs/supportedsites.md
index 2c3b18db..8beeb107 100644
--- a/docs/supportedsites.md
+++ b/docs/supportedsites.md
@@ -1052,7 +1052,7 @@ Consider all listed sites to potentially be NSFW.
| Tumblr |
https://www.tumblr.com/ |
- Days, Likes, Posts, Search Results, Tag Searches, User Profiles |
+ Days, Followers, Followed Users, Likes, Posts, Search Results, Tag Searches, User Profiles |
OAuth |
diff --git a/gallery_dl/extractor/tumblr.py b/gallery_dl/extractor/tumblr.py
index d9f1ea28..46507c47 100644
--- a/gallery_dl/extractor/tumblr.py
+++ b/gallery_dl/extractor/tumblr.py
@@ -171,6 +171,11 @@ class TumblrExtractor(Extractor):
post["count"] = len(posts)
yield msg, url, post
+ def items_blogs(self):
+ for blog in self.blogs():
+ blog["_extractor"] = TumblrUserExtractor
+ yield Message.Queue, blog["url"], blog
+
def posts(self):
"""Return an iterable containing all relevant posts"""
@@ -345,6 +350,30 @@ class TumblrLikesExtractor(TumblrExtractor):
return self.api.likes(self.blog)
+class TumblrFollowingExtractor(TumblrExtractor):
+ """Extractor for a Tumblr user's followed blogs"""
+ subcategory = "following"
+ pattern = BASE_PATTERN + r"/following"
+ example = "https://www.tumblr.com/BLOG/following"
+
+ items = TumblrExtractor.items_blogs
+
+ def blogs(self):
+ return self.api.following(self.blog)
+
+
+class TumblrFollowersExtractor(TumblrExtractor):
+ """Extractor for a Tumblr user's followers"""
+ subcategory = "followers"
+ pattern = BASE_PATTERN + r"/followers"
+ example = "https://www.tumblr.com/BLOG/followers"
+
+ items = TumblrExtractor.items_blogs
+
+ def blogs(self):
+ return self.api.followers(self.blog)
+
+
class TumblrSearchExtractor(TumblrExtractor):
"""Extractor for a Tumblr search"""
subcategory = "search"
@@ -420,6 +449,14 @@ class TumblrAPI(oauth.OAuth1API):
yield from posts
params["before"] = posts[-1]["liked_timestamp"]
+ def following(self, blog):
+ endpoint = f"/v2/blog/{blog}/following"
+ return self._pagination_blogs(endpoint)
+
+ def followers(self, blog):
+ endpoint = f"/v2/blog/{blog}/followers"
+ return self._pagination_blogs(endpoint)
+
def search(self, query, params, mode="top", post_type=None):
"""Retrieve search results"""
endpoint = "/v2/timeline/search"
@@ -556,3 +593,21 @@ class TumblrAPI(oauth.OAuth1API):
params["before"] = None
if params["offset"] >= data["total_posts"]:
return
+
+ def _pagination_blogs(self, endpoint, params=None):
+ if params is None:
+ params = {}
+ if self.api_key:
+ params["api_key"] = self.api_key
+ params["limit"] = 20
+ params["offset"] = text.parse_int(params.get("offset"), 0)
+
+ while True:
+ data = self._call(endpoint, params)
+
+ blogs = data["blogs"]
+ yield from blogs
+
+ params["offset"] = params["offset"] + params["limit"]
+ if params["offset"] >= data["total_blogs"]:
+ return
diff --git a/test/results/tumblr.py b/test/results/tumblr.py
index cbf2ec23..71da2de5 100644
--- a/test/results/tumblr.py
+++ b/test/results/tumblr.py
@@ -403,4 +403,28 @@ __tests__ = (
"#class" : tumblr.TumblrSearchExtractor,
},
+{
+ "#url" : "https://www.tumblr.com/mikf123/following",
+ "#class" : tumblr.TumblrFollowingExtractor,
+ "#results" : (
+ "https://smarties-art.tumblr.com/",
+ "https://demo.tumblr.com/",
+ ),
+
+ "can_show_badges": True,
+ "description" : str,
+ "name" : str,
+ "title" : str,
+ "tumblrmart_accessories": {},
+ "updated" : int,
+ "url" : str,
+ "uuid" : str,
+},
+
+{
+ "#url" : "https://www.tumblr.com/mikf123/followers",
+ "#class" : tumblr.TumblrFollowersExtractor,
+ "#exception": exception.AuthorizationError,
+},
+
)