diff --git a/docs/supportedsites.md b/docs/supportedsites.md index 93975e59..43150f13 100644 --- a/docs/supportedsites.md +++ b/docs/supportedsites.md @@ -1132,7 +1132,7 @@ Consider all listed sites to potentially be NSFW. Twitter https://x.com/ - Avatars, Backgrounds, Bookmarks, Communities, Events, Followers, Followed Users, Hashtags, Highlights, Home Feed, individual Images, User Profile Information, Likes, Lists, List Members, Media Timelines, Quotes, Search Results, Timelines, Tweets, User Profiles + Avatars, Backgrounds, Bookmarks, Communities, Events, Followers, Followed Users, Hashtags, Highlights, Home Feed, individual Images, User Profile Information, Likes, Lists, List Members, Media Timelines, Notifications, Quotes, Search Results, Timelines, Tweets, User Profiles Cookies diff --git a/gallery_dl/extractor/twitter.py b/gallery_dl/extractor/twitter.py index 739400b4..cc3812ec 100644 --- a/gallery_dl/extractor/twitter.py +++ b/gallery_dl/extractor/twitter.py @@ -521,10 +521,13 @@ class TwitterExtractor(Extractor): except KeyError: pass - core = user.get("core") or user - legacy = user.get("legacy") or user - lget = legacy.get + if "core" in user: + core = user["core"] + legacy = user["legacy"] + else: + core = legacy = user + lget = legacy.get if lget("withheld_scope"): self.log.warning("'%s'", lget("description")) @@ -533,14 +536,9 @@ class TwitterExtractor(Extractor): "id" : text.parse_int(uid), "name" : core.get("screen_name"), "nick" : core.get("name"), - "location" : user["location"].get("location"), "date" : self.parse_datetime( core["created_at"], "%a %b %d %H:%M:%S %z %Y"), - "verified" : user["verification"]["verified"], - "protected" : user["privacy"]["protected"], "profile_banner" : lget("profile_banner_url", ""), - "profile_image" : user["avatar"].get("image_url", "").replace( - "_normal.", "."), "favourites_count": lget("favourites_count"), "followers_count" : lget("followers_count"), "friends_count" : lget("friends_count"), @@ -549,6 +547,19 @@ class TwitterExtractor(Extractor): "statuses_count" : lget("statuses_count"), } + if "core" in user: + udata["location"] = user["location"].get("location") + udata["verified"] = user["verification"]["verified"] + udata["protected"] = user["privacy"]["protected"] + udata["profile_image"] = user["avatar"].get( + "image_url", "").replace("_normal.", ".") + else: + udata["location"] = user["location"] + udata["verified"] = user["verified"] + udata["protected"] = user["protected"] + udata["profile_image"] = user["profile_image_url_https"].replace( + "_normal.", ".") + descr = legacy["description"] if urls := entities["description"].get("urls"): for url in urls: @@ -667,8 +678,7 @@ class TwitterExtractor(Extractor): class TwitterHomeExtractor(TwitterExtractor): """Extractor for Twitter home timelines""" subcategory = "home" - pattern = (BASE_PATTERN + - r"/(?:home(?:/fo(?:llowing|r[-_ ]?you()))?|i/timeline)/?$") + pattern = BASE_PATTERN + r"/home(?:/fo(?:llowing|r[-_ ]?you()))?/?$" example = "https://x.com/home" def tweets(self): @@ -677,6 +687,16 @@ class TwitterHomeExtractor(TwitterExtractor): return self.api.home_timeline() +class TwitterNotificationsExtractor(TwitterExtractor): + """Extractor for Twitter notifications timelines""" + subcategory = "notifications" + pattern = BASE_PATTERN + r"/(?:notifications|i/timeline())" + example = "https://x.com/notifications" + + def tweets(self): + return self.api.notifications_devicefollow() + + class TwitterSearchExtractor(TwitterExtractor): """Extractor for Twitter search results""" subcategory = "search" @@ -1226,18 +1246,17 @@ class TwitterAPI(): "include_mute_edge": "1", "include_can_dm": "1", "include_can_media_tag": "1", - "include_ext_has_nft_avatar": "1", "include_ext_is_blue_verified": "1", "include_ext_verified_type": "1", + "include_ext_profile_image_shape": "1", "skip_status": "1", "cards_platform": "Web-12", "include_cards": "1", "include_ext_alt_text": "true", - "include_ext_limited_action_results": "false", + "include_ext_limited_action_results": "true", "include_quote_count": "true", "include_reply_count": "1", "tweet_mode": "extended", - "include_ext_collab_control": "true", "include_ext_views": "true", "include_entities": "true", "include_user_entities": "true", @@ -1247,16 +1266,11 @@ class TwitterAPI(): "include_ext_trusted_friends_metadata": "true", "send_error_codes": "true", "simple_quoted_tweet": "true", - "q": None, - "count": "100", - "query_source": None, "cursor": None, - "pc": None, - "spelling_corrections": None, - "include_ext_edit_control": "true", - "ext": "mediaStats,highlightedLabel,hasNftAvatar,voiceInfo," - "enrichments,superFollowMetadata,unmentionInfo,editControl," - "collab_control,vibe", + "count": "20", + "ext": "mediaStats,highlightedLabel,parodyCommentaryFanLabel," + "voiceInfo,birdwatchPivot,superFollowMetadata," + "unmentionInfo,editControl,article", } self.features = { "hidden_profile_subscriptions_enabled": True, @@ -1576,7 +1590,7 @@ class TwitterAPI(): params["timeline_id"] = "recap" params["urt"] = "true" params["get_annotations"] = "true" - return self._pagination_legacy(endpoint, params) + return self._pagination_rest(endpoint, params) def live_event(self, event_id): endpoint = f"/1.1/live_event/1/{event_id}/timeline.json" @@ -1604,6 +1618,12 @@ class TwitterAPI(): return self._pagination_users( endpoint, variables, ("list", "members_timeline", "timeline")) + def notifications_devicefollow(self): + endpoint = "/2/notifications/device_follow.json" + params = self.params.copy() + params["count"] = self.extractor.config("limit", 50) + return self._pagination_rest(endpoint, params) + def user_followers(self, screen_name): endpoint = "/graphql/i6PPdIMm1MO7CpAqjau7sw/Followers" variables = { @@ -1797,7 +1817,7 @@ class TwitterAPI(): raise exception.AbortExtraction( f"{response.status_code} {response.reason} ({errors})") - def _pagination_legacy(self, endpoint, params): + def _pagination_rest(self, endpoint, params): extr = self.extractor if cursor := extr._init_cursor(): params["cursor"] = cursor diff --git a/test/results/twitter.py b/test/results/twitter.py index 4daf9b0f..87739c67 100644 --- a/test/results/twitter.py +++ b/test/results/twitter.py @@ -802,9 +802,14 @@ The Washington Post writes, "Three weeks after the toxic train derailment in Ohi "#class" : twitter.TwitterHomeExtractor, }, +{ + "#url" : "https://x.com/notifications", + "#class" : twitter.TwitterNotificationsExtractor, +}, + { "#url" : "https://x.com/i/timeline", - "#class" : twitter.TwitterHomeExtractor, + "#class" : twitter.TwitterNotificationsExtractor, }, )