From ca22cb14877cedcf493f5dbc956140e22d89b3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20F=C3=A4hrmann?= Date: Tue, 12 Aug 2025 21:14:24 +0200 Subject: [PATCH] [tumblr] add 'following' & 'followers' extractors (#8018) --- docs/supportedsites.md | 2 +- gallery_dl/extractor/tumblr.py | 55 ++++++++++++++++++++++++++++++++++ test/results/tumblr.py | 24 +++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) 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, +}, + )