Skip to content

Commit 6de76fc

Browse files
authored
Merge pull request #3223 from bastimeyer/plugins/twitch/fix-metadata-api-response-handling
plugins.twitch: fix metadata API response handling
2 parents 710787b + 67fb0b4 commit 6de76fc

File tree

2 files changed

+111
-8
lines changed

2 files changed

+111
-8
lines changed

src/streamlink/plugins/twitch.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,39 @@ class Twitch(Plugin):
396396
)
397397
)
398398

399+
_schema_metadata_empty = validate.transform(lambda _: (None,) * 3)
400+
_schema_metadata_channel = validate.Schema(
401+
{
402+
"stream": validate.any(
403+
validate.all(
404+
{"channel": {
405+
"display_name": validate.text,
406+
"game": validate.text,
407+
"status": validate.text
408+
}},
409+
validate.get("channel"),
410+
validate.transform(lambda ch: (ch["display_name"], ch["status"], ch["game"]))
411+
),
412+
validate.all(None, _schema_metadata_empty)
413+
)
414+
},
415+
validate.get("stream")
416+
)
417+
_schema_metadata_video = validate.Schema(validate.any(
418+
validate.all(
419+
{
420+
"title": validate.text,
421+
"game": validate.text,
422+
"channel": validate.all(
423+
{"display_name": validate.text},
424+
validate.get("display_name")
425+
)
426+
},
427+
validate.transform(lambda data: (data["channel"], data["title"], data["game"]))
428+
),
429+
validate.all({}, _schema_metadata_empty)
430+
))
431+
399432
@classmethod
400433
def stream_weight(cls, key):
401434
weight = QUALITY_WEIGHTS.get(key)
@@ -410,17 +443,11 @@ def can_handle_url(cls, url):
410443

411444
def _get_metadata(self):
412445
if self.video_id:
413-
api_res = self.api.videos(self.video_id)
414-
self.title = api_res["title"]
415-
self.author = api_res["channel"]["display_name"]
416-
self.category = api_res["game"]
446+
(self.author, self.title, self.category) = self.api.videos(self.video_id, schema=self._schema_metadata_video)
417447
elif self.clip_name:
418448
self._get_clips()
419449
elif self._channel:
420-
api_res = self.api.streams(self.channel_id)["stream"]["channel"]
421-
self.title = api_res["status"]
422-
self.author = api_res["display_name"]
423-
self.category = api_res["game"]
450+
(self.author, self.title, self.category) = self.api.streams(self.channel_id, schema=self._schema_metadata_channel)
424451

425452
def get_title(self):
426453
if self.title is None:

tests/plugins/test_twitch.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from tests.mock import MagicMock, call, patch
88

99
from streamlink import Streamlink
10+
from streamlink.plugin import PluginError
1011
from streamlink.plugins.twitch import Twitch, TwitchHLSStream
1112

1213

@@ -274,6 +275,81 @@ def test_hls_low_latency_no_prefetch_disable_ads_has_preroll(self, mock_log):
274275
])
275276

276277

278+
class TestTwitchMetadata(unittest.TestCase):
279+
def setUp(self):
280+
self.mock = requests_mock.Mocker()
281+
self.mock.start()
282+
283+
def tearDown(self):
284+
self.mock.stop()
285+
286+
def subject(self, url):
287+
session = Streamlink()
288+
Twitch.bind(session, "tests.plugins.test_twitch")
289+
plugin = Twitch(url)
290+
return plugin.get_author(), plugin.get_title(), plugin.get_category()
291+
292+
def subject_channel(self, data=True, failure=False):
293+
self.mock.get(
294+
"https://api.twitch.tv/kraken/users.json?login=foo",
295+
json={"users": [{"_id": 1234}]}
296+
)
297+
self.mock.get(
298+
"https://api.twitch.tv/kraken/streams/1234.json",
299+
status_code=200 if not failure else 404,
300+
json={"stream": None} if not data else {"stream": {
301+
"channel": {
302+
"display_name": "channel name",
303+
"status": "channel status",
304+
"game": "channel game"
305+
}
306+
}}
307+
)
308+
return self.subject("https://twitch.tv/foo")
309+
310+
def subject_video(self, data=True, failure=False):
311+
self.mock.get(
312+
"https://api.twitch.tv/kraken/videos/1337.json",
313+
status_code=200 if not failure else 404,
314+
json={} if not data else {
315+
"title": "video title",
316+
"game": "video game",
317+
"channel": {
318+
"display_name": "channel name"
319+
}
320+
}
321+
)
322+
return self.subject("https://twitch.tv/videos/1337")
323+
324+
def test_metadata_channel_exists(self):
325+
author, title, category = self.subject_channel()
326+
self.assertEqual(author, "channel name")
327+
self.assertEqual(title, "channel status")
328+
self.assertEqual(category, "channel game")
329+
330+
def test_metadata_channel_missing(self):
331+
metadata = self.subject_channel(data=False)
332+
self.assertEqual(metadata, (None, None, None))
333+
334+
def test_metadata_channel_invalid(self):
335+
with self.assertRaises(PluginError):
336+
self.subject_channel(failure=True)
337+
338+
def test_metadata_video_exists(self):
339+
author, title, category = self.subject_video()
340+
self.assertEqual(author, "channel name")
341+
self.assertEqual(title, "video title")
342+
self.assertEqual(category, "video game")
343+
344+
def test_metadata_video_missing(self):
345+
metadata = self.subject_video(data=False)
346+
self.assertEqual(metadata, (None, None, None))
347+
348+
def test_metadata_video_invalid(self):
349+
with self.assertRaises(PluginError):
350+
self.subject_video(failure=True)
351+
352+
277353
@patch("streamlink.plugins.twitch.log")
278354
class TestTwitchReruns(unittest.TestCase):
279355
log_call = call("Reruns were disabled by command line option")

0 commit comments

Comments
 (0)