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.py81
1 files changed, 61 insertions, 20 deletions
diff --git a/gallery_dl/extractor/twitter.py b/gallery_dl/extractor/twitter.py
index 10db974..7b9a2e4 100644
--- a/gallery_dl/extractor/twitter.py
+++ b/gallery_dl/extractor/twitter.py
@@ -510,13 +510,13 @@ class TwitterTimelineExtractor(TwitterExtractor):
if not self.textonly:
# try to search for media-only tweets
tweet = None
- for tweet in self.api.search_adaptive(query + " filter:links"):
+ for tweet in self.api.search_timeline(query + " filter:links"):
yield tweet
if tweet is not None:
return
# yield unfiltered search results
- yield from self.api.search_adaptive(query)
+ yield from self.api.search_timeline(query)
def _select_tweet_source(self):
strategy = self.config("strategy")
@@ -693,7 +693,7 @@ class TwitterSearchExtractor(TwitterExtractor):
except KeyError:
pass
- return self.api.search_adaptive(query)
+ return self.api.search_timeline(query)
class TwitterHashtagExtractor(TwitterExtractor):
@@ -929,16 +929,15 @@ Your reaction.""",
def _tweets_single(self, tweet_id):
tweets = []
- for tweet in self.api.tweet_detail(tweet_id):
- if tweet["rest_id"] == tweet_id or \
- tweet.get("_retweet_id_str") == tweet_id:
- if self._user_obj is None:
- self._assign_user(tweet["core"]["user_results"]["result"])
- tweets.append(tweet)
+ tweet = self.api.tweet_result_by_rest_id(tweet_id)
+ self._assign_user(tweet["core"]["user_results"]["result"])
- tweet_id = tweet["legacy"].get("quoted_status_id_str")
- if not tweet_id:
- break
+ while True:
+ tweets.append(tweet)
+ tweet_id = tweet["legacy"].get("quoted_status_id_str")
+ if not tweet_id:
+ break
+ tweet = self.api.tweet_result_by_rest_id(tweet_id)
return tweets
@@ -1087,8 +1086,8 @@ class TwitterAPI():
auth_token = cookies.get("auth_token", domain=cookiedomain)
search = extractor.config("search-endpoint")
- if search == "graphql" or not auth_token and search in ("auto", None):
- self.search_adaptive = self.search_timeline
+ if search == "rest":
+ self.search_timeline = self.search_adaptive
self.headers = {
"Accept": "*/*",
@@ -1179,6 +1178,46 @@ class TwitterAPI():
"responsive_web_enhance_cards_enabled": False,
}
+ def tweet_result_by_rest_id(self, tweet_id):
+ endpoint = "/graphql/2ICDjqPd81tulZcYrtpTuQ/TweetResultByRestId"
+ 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,
+ }),
+ }
+ return self._call(endpoint, params)["data"]["tweetResult"]["result"]
+
def tweet_detail(self, tweet_id):
endpoint = "/graphql/JlLZj42Ltr2qwjasw-l5lQ/TweetDetail"
variables = {
@@ -1439,6 +1478,9 @@ class TwitterAPI():
if response.status_code == 429:
# rate limit exceeded
+ if self.extractor.config("ratelimit") == "abort":
+ raise exception.StopExtraction("Rate limit exceeded")
+
until = response.headers.get("x-rate-limit-reset")
seconds = None if until else 60
self.extractor.wait(until=until, seconds=seconds)
@@ -1592,7 +1634,9 @@ class TwitterAPI():
if entry["entryId"].startswith("cursor-bottom-"):
cursor = entry["content"]["value"]
if entries is None:
- raise KeyError()
+ if not cursor:
+ return
+ entries = ()
except LookupError:
extr.log.debug(data)
@@ -1730,7 +1774,7 @@ class TwitterAPI():
"features" : self._json_dumps(self.features_pagination)}
while True:
- cursor = entry = stop = None
+ cursor = entry = None
params["variables"] = self._json_dumps(variables)
data = self._call(endpoint, params)["data"]
@@ -1759,11 +1803,8 @@ class TwitterAPI():
yield user
elif entry["entryId"].startswith("cursor-bottom-"):
cursor = entry["content"]["value"]
- elif instr["type"] == "TimelineTerminateTimeline":
- if instr["direction"] == "Bottom":
- stop = True
- if stop or not cursor or not entry:
+ if not cursor or cursor.startswith(("-1|", "0|")) or not entry:
return
variables["cursor"] = cursor