summaryrefslogtreecommitdiffstats
path: root/gallery_dl/extractor/twitter.py
diff options
context:
space:
mode:
Diffstat (limited to 'gallery_dl/extractor/twitter.py')
-rw-r--r--gallery_dl/extractor/twitter.py217
1 files changed, 112 insertions, 105 deletions
diff --git a/gallery_dl/extractor/twitter.py b/gallery_dl/extractor/twitter.py
index cf759e0..ad5bfc6 100644
--- a/gallery_dl/extractor/twitter.py
+++ b/gallery_dl/extractor/twitter.py
@@ -693,6 +693,28 @@ class TwitterHashtagExtractor(TwitterExtractor):
yield Message.Queue, url, data
+class TwitterCommunityExtractor(TwitterExtractor):
+ """Extractor for a Twitter community"""
+ subcategory = "community"
+ pattern = BASE_PATTERN + r"/i/communities/(\d+)"
+ example = "https://twitter.com/i/communities/12345"
+
+ def tweets(self):
+ if self.textonly:
+ return self.api.community_tweets_timeline(self.user)
+ return self.api.community_media_timeline(self.user)
+
+
+class TwitterCommunitiesExtractor(TwitterExtractor):
+ """Extractor for followed Twitter communities"""
+ subcategory = "communities"
+ pattern = BASE_PATTERN + r"/([^/?#]+)/communities/?$"
+ example = "https://twitter.com/i/communities"
+
+ def tweets(self):
+ return self.api.communities_main_page_timeline(self.user)
+
+
class TwitterEventExtractor(TwitterExtractor):
"""Extractor for Tweets from a Twitter Event"""
subcategory = "event"
@@ -881,15 +903,19 @@ class TwitterAPI():
self.headers = {
"Accept": "*/*",
- "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejR"
- "COuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu"
- "4FA33AGWWjCpTnA",
+ "Referer": "https://twitter.com/",
+ "content-type": "application/json",
"x-guest-token": None,
"x-twitter-auth-type": "OAuth2Session" if auth_token else None,
+ "x-csrf-token": csrf_token,
"x-twitter-client-language": "en",
"x-twitter-active-user": "yes",
- "x-csrf-token": csrf_token,
- "Referer": "https://twitter.com/",
+ "Sec-Fetch-Dest": "empty",
+ "Sec-Fetch-Mode": "cors",
+ "Sec-Fetch-Site": "same-origin",
+ "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejR"
+ "COuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu"
+ "4FA33AGWWjCpTnA",
}
self.params = {
"include_profile_interstitial_type": "1",
@@ -933,78 +959,54 @@ class TwitterAPI():
"collab_control,vibe",
}
self.features = {
- "hidden_profile_likes_enabled": False,
+ "hidden_profile_likes_enabled": True,
+ "hidden_profile_subscriptions_enabled": True,
"responsive_web_graphql_exclude_directive_enabled": True,
"verified_phone_label_enabled": False,
- "subscriptions_verification_info_verified_since_enabled": True,
"highlights_tweets_tab_ui_enabled": True,
+ "responsive_web_twitter_article_notes_tab_enabled": True,
"creator_subscriptions_tweet_preview_api_enabled": True,
"responsive_web_graphql_"
"skip_user_profile_image_extensions_enabled": False,
"responsive_web_graphql_timeline_navigation_enabled": True,
}
self.features_pagination = {
- "rweb_lists_timeline_redesign_enabled": True,
"responsive_web_graphql_exclude_directive_enabled": True,
"verified_phone_label_enabled": False,
"creator_subscriptions_tweet_preview_api_enabled": True,
"responsive_web_graphql_timeline_navigation_enabled": True,
"responsive_web_graphql_skip_user_profile_"
"image_extensions_enabled": False,
+ "c9s_tweet_anatomy_moderator_badge_enabled": True,
"tweetypie_unmention_optimization_enabled": True,
"responsive_web_edit_tweet_api_enabled": True,
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
"view_counts_everywhere_api_enabled": True,
"longform_notetweets_consumption_enabled": True,
+ "responsive_web_twitter_article_tweet_consumption_enabled": True,
"tweet_awards_web_tipping_enabled": False,
"freedom_of_speech_not_reach_fetch_enabled": True,
"standardized_nudges_misinfo": True,
"tweet_with_visibility_results_prefer_gql_"
- "limited_actions_policy_enabled": False,
- "interactive_text_enabled": True,
- "responsive_web_text_conversations_enabled": False,
+ "limited_actions_policy_enabled": True,
+ "rweb_video_timestamps_enabled": True,
"longform_notetweets_rich_text_read_enabled": True,
- "longform_notetweets_inline_media_enabled": False,
+ "longform_notetweets_inline_media_enabled": True,
+ "responsive_web_media_download_video_enabled": True,
"responsive_web_enhance_cards_enabled": False,
}
def tweet_result_by_rest_id(self, tweet_id):
- endpoint = "/graphql/2ICDjqPd81tulZcYrtpTuQ/TweetResultByRestId"
+ endpoint = "/graphql/MWY3AO9_I3rcP_L2A4FR4A/TweetResultByRestId"
+ variables = {
+ "tweetId": tweet_id,
+ "withCommunity": False,
+ "includePromotedContent": False,
+ "withVoice": False,
+ }
params = {
- "variables": self._json_dumps({
- "tweetId": tweet_id,
- "withCommunity": False,
- "includePromotedContent": False,
- "withVoice": False,
- }),
- "features": self._json_dumps({
- "creator_subscriptions_tweet_preview_api_enabled": True,
- "tweetypie_unmention_optimization_enabled": True,
- "responsive_web_edit_tweet_api_enabled": True,
- "graphql_is_translatable_rweb_tweet_is_translatable_enabled":
- True,
- "view_counts_everywhere_api_enabled": True,
- "longform_notetweets_consumption_enabled": True,
- "responsive_web_twitter_article_tweet_consumption_enabled":
- False,
- "tweet_awards_web_tipping_enabled": False,
- "freedom_of_speech_not_reach_fetch_enabled": True,
- "standardized_nudges_misinfo": True,
- "tweet_with_visibility_results_prefer_gql_"
- "limited_actions_policy_enabled": True,
- "longform_notetweets_rich_text_read_enabled": True,
- "longform_notetweets_inline_media_enabled": True,
- "responsive_web_graphql_exclude_directive_enabled": True,
- "verified_phone_label_enabled": False,
- "responsive_web_media_download_video_enabled": False,
- "responsive_web_graphql_skip_user_profile_"
- "image_extensions_enabled": False,
- "responsive_web_graphql_timeline_navigation_enabled": True,
- "responsive_web_enhance_cards_enabled": False,
- }),
- "fieldToggles": self._json_dumps({
- "withArticleRichContentState": False,
- }),
+ "variables": self._json_dumps(variables),
+ "features" : self._json_dumps(self.features_pagination),
}
tweet = self._call(endpoint, params)["data"]["tweetResult"]["result"]
if "tweet" in tweet:
@@ -1021,7 +1023,7 @@ class TwitterAPI():
return tweet
def tweet_detail(self, tweet_id):
- endpoint = "/graphql/JlLZj42Ltr2qwjasw-l5lQ/TweetDetail"
+ endpoint = "/graphql/B9_KmbkLhXt6jRwGjJrweg/TweetDetail"
variables = {
"focalTweetId": tweet_id,
"referrer": "profile",
@@ -1037,7 +1039,7 @@ class TwitterAPI():
endpoint, variables, ("threaded_conversation_with_injections_v2",))
def user_tweets(self, screen_name):
- endpoint = "/graphql/-AY51QoFpVf-w7TxjQ6lpw/UserTweets"
+ endpoint = "/graphql/5ICa5d9-AitXZrIA3H-4MQ/UserTweets"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1049,7 +1051,7 @@ class TwitterAPI():
return self._pagination_tweets(endpoint, variables)
def user_tweets_and_replies(self, screen_name):
- endpoint = "/graphql/urrCZMyyIh1FkSFi2cdPUA/UserTweetsAndReplies"
+ endpoint = "/graphql/UtLStR_BnYUGD7Q453UXQg/UserTweetsAndReplies"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1061,7 +1063,7 @@ class TwitterAPI():
return self._pagination_tweets(endpoint, variables)
def user_media(self, screen_name):
- endpoint = "/graphql/lo965xQZdN2-eSM1Jc-W_A/UserMedia"
+ endpoint = "/graphql/tO4LMUYAZbR4T0SqQ85aAw/UserMedia"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1073,28 +1075,8 @@ class TwitterAPI():
}
return self._pagination_tweets(endpoint, variables)
- def user_media_legacy(self, screen_name):
- endpoint = "/graphql/nRybED9kRbN-TOWioHq1ng/UserMedia"
- variables = {
- "userId": self._user_id_by_screen_name(screen_name),
- "count": 100,
- "includePromotedContent": False,
- "withSuperFollowsUserFields": True,
- "withBirdwatchPivots": False,
- "withSuperFollowsTweetFields": True,
- "withClientEventToken": False,
- "withBirdwatchNotes": False,
- "withVoice": True,
- "withV2Timeline": False,
- "__fs_interactive_text": False,
- "__fs_dont_mention_me_view_api_enabled": False,
- }
- return self._pagination_tweets(
- endpoint, variables, ("user", "result", "timeline", "timeline"),
- features=False)
-
def user_likes(self, screen_name):
- endpoint = "/graphql/6JET1d0iHsIzW0Zjs3OOwQ/Likes"
+ endpoint = "/graphql/9s8V6sUI8fZLDiN-REkAxA/Likes"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1107,9 +1089,10 @@ class TwitterAPI():
return self._pagination_tweets(endpoint, variables)
def user_bookmarks(self):
- endpoint = "/graphql/YNtYqNuki6_oiVwx0uP8mQ/Bookmarks"
+ endpoint = "/graphql/cQxQgX8MJYjWwC0dxpyfYg/Bookmarks"
variables = {
"count": 100,
+ "includePromotedContent": False,
}
features = self.features_pagination.copy()
features["graphql_timeline_v2_bookmark_timeline"] = True
@@ -1118,7 +1101,7 @@ class TwitterAPI():
features=features)
def list_latest_tweets_timeline(self, list_id):
- endpoint = "/graphql/ZBbXrl37E6za5ml-DIpmgg/ListLatestTweetsTimeline"
+ endpoint = "/graphql/HjsWc-nwwHKYwHenbHm-tw/ListLatestTweetsTimeline"
variables = {
"listId": list_id,
"count": 100,
@@ -1127,22 +1110,54 @@ class TwitterAPI():
endpoint, variables, ("list", "tweets_timeline", "timeline"))
def search_timeline(self, query):
- endpoint = "/graphql/7jT5GT59P8IFjgxwqnEdQw/SearchTimeline"
+ endpoint = "/graphql/fZK7JipRHWtiZsTodhsTfQ/SearchTimeline"
variables = {
"rawQuery": query,
- "count": 20,
+ "count": 100,
+ "querySource": "",
"product": "Latest",
- "withDownvotePerspective": False,
- "withReactionsMetadata": False,
- "withReactionsPerspective": False,
}
- features = self.features_pagination.copy()
- features["blue_business_profile_image_shape_enabled"] = False
- features["vibe_api_enabled"] = True
+
return self._pagination_tweets(
endpoint, variables,
- ("search_by_raw_query", "search_timeline", "timeline"),
- features=features)
+ ("search_by_raw_query", "search_timeline", "timeline"))
+
+ def community_tweets_timeline(self, community_id):
+ endpoint = "/graphql/7B2AdxSuC-Er8qUr3Plm_w/CommunityTweetsTimeline"
+ variables = {
+ "communityId": community_id,
+ "count": 100,
+ "displayLocation": "Community",
+ "rankingMode": "Recency",
+ "withCommunity": True,
+ }
+ return self._pagination_tweets(
+ endpoint, variables,
+ ("communityResults", "result", "ranked_community_timeline",
+ "timeline"))
+
+ def community_media_timeline(self, community_id):
+ endpoint = "/graphql/qAGUldfcIoMv5KyAyVLYog/CommunityMediaTimeline"
+ variables = {
+ "communityId": community_id,
+ "count": 100,
+ "withCommunity": True,
+ }
+ return self._pagination_tweets(
+ endpoint, variables,
+ ("communityResults", "result", "community_media_timeline",
+ "timeline"))
+
+ def communities_main_page_timeline(self, screen_name):
+ endpoint = ("/graphql/GtOhw2mstITBepTRppL6Uw"
+ "/CommunitiesMainPageTimeline")
+ variables = {
+ "count": 100,
+ "withCommunity": True,
+ }
+ return self._pagination_tweets(
+ endpoint, variables,
+ ("viewer", "communities_timeline", "timeline"))
def live_event_timeline(self, event_id):
endpoint = "/2/live_event/timeline/{}.json".format(event_id)
@@ -1160,21 +1175,8 @@ class TwitterAPI():
return (self._call(endpoint, params)
["twitter_objects"]["live_events"][event_id])
- def list_by_rest_id(self, list_id):
- endpoint = "/graphql/AmCdeFUvlrKAO96yHr-GCg/ListByRestId"
- params = {
- "variables": self._json_dumps({
- "listId": list_id,
- }),
- "features": self._json_dumps(self.features),
- }
- try:
- return self._call(endpoint, params)["data"]["list"]
- except KeyError:
- raise exception.NotFoundError("list")
-
def list_members(self, list_id):
- endpoint = "/graphql/a_ZQomd3MMk1crWkeiQBPg/ListMembers"
+ endpoint = "/graphql/BQp2IEYkgxuSxqbTAr1e1g/ListMembers"
variables = {
"listId": list_id,
"count": 100,
@@ -1184,7 +1186,7 @@ class TwitterAPI():
endpoint, variables, ("list", "members_timeline", "timeline"))
def user_following(self, screen_name):
- endpoint = "/graphql/JPZiqKjET7_M1r5Tlr8pyA/Following"
+ endpoint = "/graphql/PAnE9toEjRfE-4tozRcsfw/Following"
variables = {
"userId": self._user_id_by_screen_name(screen_name),
"count": 100,
@@ -1194,9 +1196,8 @@ class TwitterAPI():
@memcache(keyarg=1)
def user_by_rest_id(self, rest_id):
- endpoint = "/graphql/1YAM811Q8Ry4XyPpJclURQ/UserByRestId"
- features = self.features.copy()
- features["blue_business_profile_image_shape_enabled"] = True
+ endpoint = "/graphql/tD8zKvQzwY3kdx5yz6YmOw/UserByRestId"
+ features = self.features
params = {
"variables": self._json_dumps({
"userId": rest_id,
@@ -1208,13 +1209,18 @@ class TwitterAPI():
@memcache(keyarg=1)
def user_by_screen_name(self, screen_name):
- endpoint = "/graphql/XA6F1nJELYg65hxOC2Ekmg/UserByScreenName"
+ endpoint = "/graphql/k5XapwcSikNsEsILW5FvgA/UserByScreenName"
+ features = self.features.copy()
+ features["subscriptions_verification_info_"
+ "is_identity_verified_enabled"] = True
+ features["subscriptions_verification_info_"
+ "verified_since_enabled"] = True
params = {
"variables": self._json_dumps({
"screen_name": screen_name,
"withSafetyModeUserFields": True,
}),
- "features": self._json_dumps(self.features),
+ "features": self._json_dumps(features),
}
return self._call(endpoint, params)["data"]["user"]["result"]
@@ -1486,7 +1492,8 @@ class TwitterAPI():
if esw("tweet-"):
tweets.append(entry)
- elif esw("profile-grid-"):
+ elif esw(("profile-grid-",
+ "communities-grid-")):
if "content" in entry:
tweets.extend(entry["content"]["items"])
else: