From a49dcab419e464ad158a56f1cc61198d332e6cb9 Mon Sep 17 00:00:00 2001 From: Ivan Habunek Date: Wed, 9 Jun 2021 15:07:07 +0200 Subject: [PATCH] Fix clips download by fetching access token fixes #64 --- twitchdl/commands/clips.py | 3 ++- twitchdl/commands/download.py | 37 ++++++++++++++++++++++++++++------- twitchdl/twitch.py | 30 ++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/twitchdl/commands/clips.py b/twitchdl/commands/clips.py index 0d4f9b0..6b84360 100644 --- a/twitchdl/commands/clips.py +++ b/twitchdl/commands/clips.py @@ -3,6 +3,7 @@ import re from os import path from twitchdl import twitch, utils +from twitchdl.commands.download import get_clip_authenticated_url from twitchdl.download import download_file from twitchdl.exceptions import ConsoleError from twitchdl.output import print_out, print_clip, print_json @@ -79,7 +80,7 @@ def _clips_download(args): for clips, _ in generator: for clip in clips["edges"]: clip = clip["node"] - url = clip["videoQualities"][0]["sourceURL"] + url = get_clip_authenticated_url(clip["slug"], "source") target = _clip_target_filename(clip) if path.exists(target): diff --git a/twitchdl/commands/download.py b/twitchdl/commands/download.py index 5656807..1502c83 100644 --- a/twitchdl/commands/download.py +++ b/twitchdl/commands/download.py @@ -7,7 +7,7 @@ import tempfile from os import path from pathlib import Path -from urllib.parse import urlparse +from urllib.parse import urlparse, urlencode from twitchdl import twitch, utils from twitchdl.download import download_file, download_files @@ -139,21 +139,21 @@ def download(args): raise ConsoleError("Invalid input: {}".format(args.video)) -def _get_clip_url(clip, args): +def _get_clip_url(clip, quality): qualities = clip["videoQualities"] # Quality given as an argument - if args.quality: - if args.quality == "source": + if quality: + if quality == "source": return qualities[0]["sourceURL"] - selected_quality = args.quality.rstrip("p") # allow 720p as well as 720 + selected_quality = quality.rstrip("p") # allow 720p as well as 720 for q in qualities: if q["quality"] == selected_quality: return q["sourceURL"] available = ", ".join([str(q["quality"]) for q in qualities]) - msg = "Quality '{}' not found. Available qualities are: {}".format(args.quality, available) + msg = "Quality '{}' not found. Available qualities are: {}".format(quality, available) raise ConsoleError(msg) # Ask user to select quality @@ -167,6 +167,23 @@ def _get_clip_url(clip, args): return selected_quality["sourceURL"] +def get_clip_authenticated_url(slug, quality): + print_out("Fetching access token...") + access_token = twitch.get_clip_access_token(slug) + + if not access_token: + raise ConsoleError("Access token not found for slug '{}'".format(slug)) + + url = _get_clip_url(access_token, quality) + + query = urlencode({ + "sig": access_token["playbackAccessToken"]["signature"], + "token": access_token["playbackAccessToken"]["value"], + }) + + return "{}?{}".format(url, query) + + def _download_clip(slug, args): print_out("Looking up clip...") clip = twitch.get_clip(slug) @@ -174,6 +191,12 @@ def _download_clip(slug, args): if not clip: raise ConsoleError("Clip '{}' not found".format(slug)) + print_out("Fetching access token...") + access_token = twitch.get_clip_access_token(slug) + + if not access_token: + raise ConsoleError("Access token not found for slug '{}'".format(slug)) + print_out("Found: {} by {}, playing {} ({})".format( clip["title"], clip["broadcaster"]["displayName"], @@ -181,7 +204,7 @@ def _download_clip(slug, args): utils.format_duration(clip["durationSeconds"]) )) - url = _get_clip_url(clip, args) + url = get_clip_authenticated_url(slug, args.quality) print_out("Selected URL: {}".format(url)) target = _clip_target_filename(clip) diff --git a/twitchdl/twitch.py b/twitchdl/twitch.py index 57bdb7b..316dc10 100644 --- a/twitchdl/twitch.py +++ b/twitchdl/twitch.py @@ -51,6 +51,16 @@ def kraken_get(url, params={}, headers={}): return authenticated_get(url, params, headers) +def gql_post(query): + url = "https://gql.twitch.tv/gql" + response = authenticated_post(url, data=query).json() + + if "errors" in response: + raise GQLError(response["errors"]) + + return response + + def gql_query(query): url = "https://gql.twitch.tv/gql" response = authenticated_post(url, json={"query": query}).json() @@ -133,6 +143,26 @@ def get_clip(slug): return response["data"]["clip"] +def get_clip_access_token(slug): + query = """ + {{ + "operationName": "VideoAccessToken_Clip", + "variables": {{ + "slug": "{slug}" + }}, + "extensions": {{ + "persistedQuery": {{ + "version": 1, + "sha256Hash": "36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11" + }} + }} + }} + """ + + response = gql_post(query.format(slug=slug).strip()) + return response["data"]["clip"] + + def get_channel_clips(channel_id, period, limit, after=None): """ List channel clips.