A tool to retrieve information from Twitch.
1"""Module with helper functions to interact with Twitch API."""
2
3import logging
4import urllib
5
6import httpx
7
8from .settings import settings
9
10logger = logging.getLogger(__name__)
11
12
13# Common
14# --------------------------------------------------------------------------------------
15
16
17def raise_for_status(response: httpx.Response) -> None:
18 """Unify the handle of errors in API requests."""
19 if not response.is_success:
20 logger.error(
21 f"Error calling to {response.request.url}:\n"
22 f"{response.content.decode('utf-8')}"
23 )
24 response.raise_for_status()
25
26
27# Twitch ID
28# --------------------------------------------------------------------------------------
29
30
31def twitch_id_client() -> httpx.Client:
32 """Create a simple client to access Twitch ID API."""
33 base_url = "https://id.twitch.tv"
34 return httpx.Client(base_url=base_url)
35
36
37def retrieve_token(code: str) -> dict:
38 """Retrieve the token information from Twitch using the given code."""
39 logger.debug(f"Obtaining token information using code {code}")
40 params = {
41 "client_id": settings.client_id,
42 "client_secret": settings.client_secret,
43 "code": code,
44 "grant_type": "authorization_code",
45 "redirect_uri": settings.redirect_uri,
46 }
47 with twitch_id_client() as client:
48 response = client.post("/oauth2/token", params=params)
49 raise_for_status(response)
50
51 return response.json()
52
53
54def retrieve_authorize_url() -> str:
55 """Create and return the authorize URL."""
56 authorize_url = "https://id.twitch.tv/oauth2/authorize"
57 params = {
58 "client_id": settings.client_id,
59 "redirect_uri": settings.redirect_uri,
60 "response_type": "code",
61 "scope": settings.scopes,
62 }
63 return f"{authorize_url}?{urllib.parse.urlencode(params)}"
64
65
66def validate_access_token(access_token: str) -> bool:
67 """Validate the given access token."""
68 headers = {"Authorization": f"OAuth {access_token}"}
69 with twitch_id_client() as client:
70 response = client.get("/oauth2/validate", headers=headers)
71 return response.status_code == 200
72
73
74def refresh_access_token(refresh_token: str) -> dict:
75 """Refresh the access token."""
76 params = {
77 "grant_type": "refresh_token",
78 "refresh_token": refresh_token,
79 "client_id": settings.client_id,
80 "client_secret": settings.client_secret,
81 }
82
83 with twitch_id_client() as client:
84 response = client.post("/oauth2/token", params=params)
85 raise_for_status(response)
86
87 data = response.json()
88 return data
89
90
91# Twitch API
92# --------------------------------------------------------------------------------------
93
94
95def twitch_api_client(access_token: str) -> httpx.Client:
96 """Create a simple client to access Twitch API."""
97 base_url = "https://api.twitch.tv"
98 headers = {
99 "Client-ID": settings.client_id,
100 "Authorization": f"Bearer {access_token}",
101 }
102 return httpx.Client(base_url=base_url, headers=headers)
103
104
105def retrieve_user(access_token: str) -> dict:
106 """Retrieve the user information."""
107 with twitch_api_client(access_token) as client:
108 response = client.get("/helix/users")
109 raise_for_status(response)
110
111 data = response.json()
112 return data["data"][0]
113
114
115def retrieve_followed_channels(access_token: str, user_id: str) -> list[dict]:
116 """Retrieve the list of followed channels."""
117 page_size = 100
118 params: dict[str, str | int] = {
119 "user_id": user_id,
120 "first": page_size,
121 }
122
123 followed: list[dict] = []
124 total: int | None = None
125
126 while total is None or len(followed) > total:
127 with twitch_api_client(access_token) as client:
128 response = client.get("/helix/channels/followed", params=params)
129 raise_for_status(response)
130 data = response.json()
131
132 if total is None:
133 total = data["total"]
134
135 followed.extend(data["data"])
136
137 cursor = data["pagination"].get("cursor")
138 if cursor:
139 params["after"] = cursor
140
141 return followed
142
143
144def retrieve_followed_streams(access_token: str, user_id: str) -> list[dict]:
145 """Retrieve the list of followed streams."""
146 page_size = 100
147 params: dict[str, str | int] = {
148 "user_id": user_id,
149 "first": page_size,
150 }
151
152 streams: list[dict] = []
153 first_query: bool = True
154 cursor: str | None = None
155
156 while first_query or cursor:
157 with twitch_api_client(access_token) as client:
158 response = client.get("/helix/streams/followed", params=params)
159 raise_for_status(response)
160 data = response.json()
161
162 if first_query:
163 first_query = False
164
165 streams.extend(data["data"])
166
167 cursor = data["pagination"].get("cursor")
168 if cursor:
169 params["after"] = cursor
170
171 return streams
172
173
174def retrieve_live_streams(
175 access_token: str,
176 language: list[str] | None = None,
177 size: int | None = None,
178) -> list[dict]:
179 """Retrieve the list of live stream, sorted by viewers.
180
181 It doesn't relay in pagination, because the number of viewers can change between
182 calls, and therefore, it could generate duplicated results.
183 """
184 params: dict[str, str | int | list[str]] = {
185 "type": "live",
186 }
187 if language:
188 params["language"] = language
189 if size:
190 if size > 100:
191 logger.warning("Twitch API supports maximum of 100 items.")
192 params["first"] = size
193
194 with twitch_api_client(access_token) as client:
195 response = client.get("/helix/streams", params=params)
196 raise_for_status(response)
197 data = response.json()
198
199 return data["data"]