twitch-dl/twitchdl/twitch.py

322 lines
7.4 KiB
Python
Raw Normal View History

"""
Twitch API access.
"""
2018-01-25 10:09:20 +00:00
import requests
from twitchdl import CLIENT_ID
2019-08-12 13:14:13 +00:00
from twitchdl.exceptions import ConsoleError
2018-01-25 10:09:20 +00:00
2020-05-17 12:35:33 +00:00
class GQLError(Exception):
def __init__(self, errors):
super().__init__("GraphQL query failed")
self.errors = errors
def authenticated_get(url, params={}, headers={}):
headers['Client-ID'] = CLIENT_ID
2018-01-25 10:09:20 +00:00
response = requests.get(url, params, headers=headers)
if 400 <= response.status_code < 500:
2019-08-12 13:14:13 +00:00
data = response.json()
# TODO: this does not look nice in the console since data["message"]
# can contain a JSON encoded object.
2019-08-12 13:14:13 +00:00
raise ConsoleError(data["message"])
2018-01-25 10:09:20 +00:00
response.raise_for_status()
return response
2020-04-11 14:07:17 +00:00
def authenticated_post(url, data=None, json=None, headers={}):
headers['Client-ID'] = CLIENT_ID
response = requests.post(url, data=data, json=json, headers=headers)
if response.status_code == 400:
data = response.json()
raise ConsoleError(data["message"])
response.raise_for_status()
return response
def kraken_get(url, params={}, headers={}):
"""
Add accept header required by kraken API v5.
see: https://discuss.dev.twitch.tv/t/change-in-access-to-deprecated-kraken-twitch-apis/22241
"""
headers["Accept"] = "application/vnd.twitchtv.v5+json"
return authenticated_get(url, params, headers)
2020-05-17 11:48:48 +00:00
def gql_query(query):
url = "https://gql.twitch.tv/gql"
2020-05-17 12:35:33 +00:00
response = authenticated_post(url, json={"query": query}).json()
if "errors" in response:
raise GQLError(response["errors"])
return response
2020-05-17 11:48:48 +00:00
2021-01-14 20:38:56 +00:00
def get_video_legacy(video_id):
2018-01-25 10:09:20 +00:00
"""
https://dev.twitch.tv/docs/v5/reference/videos#get-video
"""
2020-04-11 14:07:17 +00:00
url = "https://api.twitch.tv/kraken/videos/{}".format(video_id)
2018-01-25 10:09:20 +00:00
return kraken_get(url).json()
2018-01-25 10:09:20 +00:00
2021-01-14 20:38:56 +00:00
VIDEO_FIELDS = """
id
title
publishedAt
broadcastType
lengthSeconds
game {
name
}
creator {
login
displayName
}
"""
def get_video(video_id):
query = """
{{
video(id: "{video_id}") {{
{fields}
}}
}}
"""
query = query.format(video_id=video_id, fields=VIDEO_FIELDS)
response = gql_query(query)
return response["data"]["video"]
2020-04-11 14:07:17 +00:00
def get_clip(slug):
query = """
{{
clip(slug: "{}") {{
id
2021-01-14 20:38:56 +00:00
slug
2020-04-11 14:07:17 +00:00
title
createdAt
2021-01-14 20:38:56 +00:00
viewCount
2020-04-11 14:07:17 +00:00
durationSeconds
2021-01-14 20:38:56 +00:00
url
videoQualities {{
frameRate
quality
sourceURL
}}
2020-04-11 14:07:17 +00:00
game {{
2021-01-14 20:38:56 +00:00
id
2020-04-11 14:07:17 +00:00
name
}}
broadcaster {{
displayName
2021-01-14 20:38:56 +00:00
login
2020-04-11 14:07:17 +00:00
}}
}}
}}
"""
2020-05-17 11:48:48 +00:00
response = gql_query(query.format(slug))
return response["data"]["clip"]
2020-04-11 14:07:17 +00:00
2020-09-03 06:49:41 +00:00
def get_channel_clips(channel_id, period, limit, after=None):
"""
List channel clips.
At the time of writing this:
* filtering by game name returns an error
* sorting by anything but VIEWS_DESC or TRENDING returns an error
* sorting by VIEWS_DESC and TRENDING returns the same results
* there is no totalCount
"""
query = """
{{
user(login: "{channel_id}") {{
2020-11-10 08:21:37 +00:00
clips(first: {limit}, after: "{after}", criteria: {{ period: {period}, sort: VIEWS_DESC }}) {{
2020-09-03 06:49:41 +00:00
pageInfo {{
hasNextPage
hasPreviousPage
}}
edges {{
cursor
node {{
id
slug
title
createdAt
viewCount
durationSeconds
url
videoQualities {{
frameRate
quality
sourceURL
}}
game {{
id
name
}}
broadcaster {{
2021-01-14 20:38:56 +00:00
displayName
login
2020-09-03 06:49:41 +00:00
}}
}}
}}
}}
}}
}}
"""
query = query.format(**{
"channel_id": channel_id,
"after": after if after else "",
"limit": limit,
"period": period.upper(),
})
response = gql_query(query)
2020-11-10 11:40:11 +00:00
user = response["data"]["user"]
if not user:
raise ConsoleError("Channel {} not found".format(channel_id))
2020-09-03 06:49:41 +00:00
return response["data"]["user"]["clips"]
def channel_clips_generator(channel_id, period, limit):
cursor = ""
while True:
clips = get_channel_clips(
channel_id, period, limit, after=cursor)
if not clips["edges"]:
break
has_next = clips["pageInfo"]["hasNextPage"]
cursor = clips["edges"][-1]["cursor"] if has_next else None
yield clips, has_next
if not cursor:
break
2020-05-17 12:35:33 +00:00
def get_channel_videos(channel_id, limit, sort, type="archive", game_ids=[], after=None):
query = """
{{
2020-05-17 12:41:11 +00:00
user(login: "{channel_id}") {{
videos(
first: {limit},
type: {type},
sort: {sort},
after: "{after}",
options: {{
gameIDs: {game_ids}
}}
) {{
totalCount
pageInfo {{
hasNextPage
}}
edges {{
cursor
node {{
id
title
publishedAt
broadcastType
lengthSeconds
game {{
name
}}
creator {{
2021-01-14 20:38:56 +00:00
login
displayName
2020-05-17 12:41:11 +00:00
}}
}}
}}
}}
}}
}}
2018-01-25 10:09:20 +00:00
"""
query = query.format(**{
"channel_id": channel_id,
2020-05-17 12:35:33 +00:00
"game_ids": game_ids,
2020-09-03 06:49:41 +00:00
"after": after if after else "",
2018-01-25 10:09:20 +00:00
"limit": limit,
"sort": sort.upper(),
2020-05-17 11:35:51 +00:00
"type": type.upper(),
})
2020-05-17 11:48:48 +00:00
response = gql_query(query)
return response["data"]["user"]["videos"]
2018-01-25 10:09:20 +00:00
2020-05-17 12:35:33 +00:00
def channel_videos_generator(channel_id, limit, sort, type, game_ids=None):
2020-09-03 06:49:41 +00:00
cursor = ""
2020-05-17 11:35:51 +00:00
while True:
2020-05-17 12:35:33 +00:00
videos = get_channel_videos(
channel_id, limit, sort, type, game_ids=game_ids, after=cursor)
if not videos["edges"]:
break
2020-05-17 12:41:11 +00:00
has_next = videos["pageInfo"]["hasNextPage"]
cursor = videos["edges"][-1]["cursor"] if has_next else None
2020-05-17 11:35:51 +00:00
2020-05-17 12:41:11 +00:00
yield videos, has_next
2020-05-17 11:35:51 +00:00
if not cursor:
break
2018-01-25 10:09:20 +00:00
def get_access_token(video_id):
2020-04-11 14:07:17 +00:00
url = "https://api.twitch.tv/api/vods/{}/access_token".format(video_id)
2018-01-25 10:09:20 +00:00
return authenticated_get(url).json()
def get_playlists(video_id, access_token):
2019-08-23 10:36:05 +00:00
"""
For a given video return a playlist which contains possible video qualities.
"""
2018-01-25 10:09:20 +00:00
url = "http://usher.twitch.tv/vod/{}".format(video_id)
response = requests.get(url, params={
"nauth": access_token['token'],
"nauthsig": access_token['sig'],
"allow_source": "true",
"player": "twitchweb",
})
response.raise_for_status()
2019-08-23 10:36:05 +00:00
return response.content.decode('utf-8')
2020-05-17 12:35:33 +00:00
def get_game_id(name):
query = """
{{
game(name: "{}") {{
id
}}
}}
"""
response = gql_query(query.format(name.strip()))
game = response["data"]["game"]
if game:
return game["id"]