Fix clips download by fetching access token

fixes #64
This commit is contained in:
Ivan Habunek 2021-06-09 15:07:07 +02:00
parent 0dd04a7e2d
commit a49dcab419
No known key found for this signature in database
GPG Key ID: CDBD63C43A30BB95
3 changed files with 62 additions and 8 deletions

View File

@ -3,6 +3,7 @@ import re
from os import path from os import path
from twitchdl import twitch, utils from twitchdl import twitch, utils
from twitchdl.commands.download import get_clip_authenticated_url
from twitchdl.download import download_file from twitchdl.download import download_file
from twitchdl.exceptions import ConsoleError from twitchdl.exceptions import ConsoleError
from twitchdl.output import print_out, print_clip, print_json from twitchdl.output import print_out, print_clip, print_json
@ -79,7 +80,7 @@ def _clips_download(args):
for clips, _ in generator: for clips, _ in generator:
for clip in clips["edges"]: for clip in clips["edges"]:
clip = clip["node"] clip = clip["node"]
url = clip["videoQualities"][0]["sourceURL"] url = get_clip_authenticated_url(clip["slug"], "source")
target = _clip_target_filename(clip) target = _clip_target_filename(clip)
if path.exists(target): if path.exists(target):

View File

@ -7,7 +7,7 @@ import tempfile
from os import path from os import path
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse, urlencode
from twitchdl import twitch, utils from twitchdl import twitch, utils
from twitchdl.download import download_file, download_files from twitchdl.download import download_file, download_files
@ -139,21 +139,21 @@ def download(args):
raise ConsoleError("Invalid input: {}".format(args.video)) raise ConsoleError("Invalid input: {}".format(args.video))
def _get_clip_url(clip, args): def _get_clip_url(clip, quality):
qualities = clip["videoQualities"] qualities = clip["videoQualities"]
# Quality given as an argument # Quality given as an argument
if args.quality: if quality:
if args.quality == "source": if quality == "source":
return qualities[0]["sourceURL"] 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: for q in qualities:
if q["quality"] == selected_quality: if q["quality"] == selected_quality:
return q["sourceURL"] return q["sourceURL"]
available = ", ".join([str(q["quality"]) for q in qualities]) 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) raise ConsoleError(msg)
# Ask user to select quality # Ask user to select quality
@ -167,6 +167,23 @@ def _get_clip_url(clip, args):
return selected_quality["sourceURL"] return selected_quality["sourceURL"]
def get_clip_authenticated_url(slug, quality):
print_out("<dim>Fetching access token...</dim>")
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): def _download_clip(slug, args):
print_out("<dim>Looking up clip...</dim>") print_out("<dim>Looking up clip...</dim>")
clip = twitch.get_clip(slug) clip = twitch.get_clip(slug)
@ -174,6 +191,12 @@ def _download_clip(slug, args):
if not clip: if not clip:
raise ConsoleError("Clip '{}' not found".format(slug)) raise ConsoleError("Clip '{}' not found".format(slug))
print_out("<dim>Fetching access token...</dim>")
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: <green>{}</green> by <yellow>{}</yellow>, playing <blue>{}</blue> ({})".format( print_out("Found: <green>{}</green> by <yellow>{}</yellow>, playing <blue>{}</blue> ({})".format(
clip["title"], clip["title"],
clip["broadcaster"]["displayName"], clip["broadcaster"]["displayName"],
@ -181,7 +204,7 @@ def _download_clip(slug, args):
utils.format_duration(clip["durationSeconds"]) utils.format_duration(clip["durationSeconds"])
)) ))
url = _get_clip_url(clip, args) url = get_clip_authenticated_url(slug, args.quality)
print_out("<dim>Selected URL: {}</dim>".format(url)) print_out("<dim>Selected URL: {}</dim>".format(url))
target = _clip_target_filename(clip) target = _clip_target_filename(clip)

View File

@ -51,6 +51,16 @@ def kraken_get(url, params={}, headers={}):
return authenticated_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): def gql_query(query):
url = "https://gql.twitch.tv/gql" url = "https://gql.twitch.tv/gql"
response = authenticated_post(url, json={"query": query}).json() response = authenticated_post(url, json={"query": query}).json()
@ -133,6 +143,26 @@ def get_clip(slug):
return response["data"]["clip"] 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): def get_channel_clips(channel_id, period, limit, after=None):
""" """
List channel clips. List channel clips.