Repository: streamlink/streamlink
Pull request #3929 (2021-08-21 20:10:06)

plugins.twitch: remove kraken API calls
**plugins.twitch: replace remaining kraken API calls**

Twitch's kraken API will be shut down on 2022-02-28.

Replace all remaining kraken API calls with GraphQL API calls, as helix
requires user authentication for every API request and it doesn't make
sense adding (back) user auth for extracting simple stream and video
metadata, like channel name, stream/video title and game name.

The GraphQL API is already being used for various endpoints which don't
exist on any of Twitch's public APIs.

- Remove anything related to `_channel_id`.
  This was previously needed for requesting channel metadata.
- Replace `channel` property method with regular property.
  This was previously needed for extracting channel names from video IDs.
- Remove `TwitchAPI.channel_from_*` methods
- Rephrase debug log messages when extracting HLS stream URLs
- Update metadata and hosting tests

----

**plugins.twitch: refactor TwitchAPI class methods**

- remove methods for public API calls
- remove Streamlink's Twitch client-ID, as it's unneeded now
- clean up

----

**Existing live stream**
```
$ streamlink -l debug --title '{author} - {category} - {title}' twitch.tv/esl_csgo best
[plugins.twitch][debug] Getting live HLS streams for esl_csgo
[cli][info] Starting player: mpv
[cli.output][debug] Opening subprocess: mpv "--force-media-title=ESL_CSGO - Counter-Strike: Global Offensive - LIVE: Complexity vs. forZe - ESL Pro League Season 14 - Group B " -
```

**Existing VOD**
```
$ streamlink -l debug --title '{author} - {category} - {title}' twitch.tv/videos/1088547163 best
[plugins.twitch][debug] Getting HLS streams for video ID 1088547163
[cli][info] Starting player: mpv
[cli.output][debug] Opening subprocess: mpv "--force-media-title=ESL_CSGO - Counter-Strike: Global Offensive - CS:GO - Astralis vs. Virtus.pro [Overpass] Map 1 - IEM Cologne 2021 - Quarterfinal" -
```

**Existing VOD, but no metadata (Twitch's "Test-VOD")**
```
$ streamlink -l debug --title '{author} - {category} - {title}' twitch.tv/videos/0123456789 best
[plugins.twitch][debug] Getting HLS streams for video ID 0123456789
[cli][info] Starting player: mpv
[cli.output][debug] Opening subprocess: mpv "--force-media-title=Unknown Author - No Category - Unknown Title" -
```

**Offline / invalid channel**
```
$ streamlink -l debug --title '{author} - {category} - {title}' twitch.tv/achannelthatdoesnotexist best
[plugins.twitch][debug] Getting live HLS streams for achannelthatdoesnotexist
error: No playable streams found on this URL: twitch.tv/achannelthatdoesnotexist
```

**Invalid VOD**
```
$ streamlink -l debug --title '{author} - {category} - {title}' twitch.tv/videos/0 best
[plugins.twitch][debug] Getting HLS streams for video ID 0
error: No playable streams found on this URL: twitch.tv/videos/0
```

gravyboat: Looks great. Thanks @bastimeyer!

Patch:
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
index 0a1e04aa..f8d97094 100644
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -171,36 +171,24 @@ class UsherService:
 
 
 class TwitchAPI:
-    # Streamlink's client-id used for public API calls (don't steal this and register your own application on Twitch)
-    TWITCH_CLIENT_ID = "pwkzresl8kj2rdj6g7bvxl9ys1wly3j"
-    # Twitch's client-id used for private API calls (see issue #2680 for why we are doing this)
-    TWITCH_CLIENT_ID_PRIVATE = "kimne78kx3ncx6brgo4mv6wki5h1ko"
+    headers = {
+        "Client-ID": "kimne78kx3ncx6brgo4mv6wki5h1ko"
+    }
 
     def __init__(self, session):
         self.session = session
 
-    def _call(self, method="GET", subdomain="api", path="/", headers=None, private=False, data=None, **params):
-        url = "https://{0}.twitch.tv{1}".format(subdomain, path)
-        headers = headers or dict()
-        headers.update({
-            "Client-ID": self.TWITCH_CLIENT_ID if not private else self.TWITCH_CLIENT_ID_PRIVATE
-        })
-
-        return self.session.http.request(method, url, data=data, params=params, headers=headers)
-
-    def call(self, path, schema=None, **params):
-        headers = {"Accept": "application/vnd.twitchtv.v5+json"}
-        res = self._call(path=path, headers=headers, **params)
-
-        return self.session.http.json(res, schema=schema)
-
-    def call_gql(self, data, schema=None, **params):
-        res = self._call(method="POST", subdomain="gql", path="/gql", data=json.dumps(data), private=True, **params)
+    def call(self, data, schema=None):
+        res = self.session.http.post(
+            "https://gql.twitch.tv/gql",
+            data=json.dumps(data),
+            headers=self.headers
+        )
 
         return self.session.http.json(res, schema=schema)
 
-    @classmethod
-    def _gql_persisted_query(cls, operationname, sha256hash, **variables):
+    @staticmethod
+    def _gql_persisted_query(operationname, sha256hash, **variables):
         return {
             "operationName": operationname,
             "extensions": {
@@ -212,58 +200,84 @@ class TwitchAPI:
             "variables": dict(**variables)
         }
 
-    # Public API calls
-
-    def channel_from_video_id(self, video_id):
-        return self.call("/kraken/videos/{0}".format(video_id), schema=validate.Schema(
-            {"channel": {
-                "_id": validate.transform(int),
-                "name": validate.all(str, validate.transform(lambda n: n.lower()))
-            }},
-            validate.get("channel"),
-            validate.union_get("_id", "name")
+    @staticmethod
+    def parse_token(tokenstr):
+        return parse_json(tokenstr, schema=validate.Schema(
+            {"chansub": {"restricted_bitrates": validate.all(
+                [str],
+                validate.filter(lambda n: not re.match(r"(.+_)?archives|live|chunked", n))
+            )}},
+            validate.get(("chansub", "restricted_bitrates"))
         ))
 
-    def channel_from_login(self, channel):
-        return self.call("/kraken/users", login=channel, schema=validate.Schema(
-            {"users": [{
-                "_id": validate.transform(int)
-            }]},
-            validate.get(("users", 0, "_id"))
-        ))
+    # GraphQL API calls
 
     def metadata_video(self, video_id):
-        return self.call("/kraken/videos/{0}".format(video_id), schema=validate.Schema(validate.any(
-            validate.all(
-                {
-                    "title": str,
-                    "game": str,
-                    "channel": {"display_name": str}
+        query = self._gql_persisted_query(
+            "VideoMetadata",
+            "cb3b1eb2f2d2b2f65b8389ba446ec521d76c3aa44f5424a1b1d235fe21eb4806",
+            channelLogin="",  # parameter can be empty
+            videoID=video_id
+        )
+
+        return self.call(query, schema=validate.Schema(
+            {"data": {"video": {
+                "owner": {
+                    "displayName": str
                 },
-                validate.union_get(("channel", "display_name"), "title", "game")
+                "title": str,
+                "game": {
+                    "displayName": str
+                }
+            }}},
+            validate.get(("data", "video")),
+            validate.union_get(
+                ("owner", "displayName"),
+                "title",
+                ("game", "displayName")
+            )
+        ))
+
+    def metadata_channel(self, channel):
+        queries = [
+            self._gql_persisted_query(
+                "ChannelShell",
+                "c3ea5a669ec074a58df5c11ce3c27093fa38534c94286dc14b68a25d5adcbf55",
+                login=channel,
+                lcpVideosEnabled=False
             ),
-            validate.all({}, validate.transform(lambda _: (None,) * 3))
-        )))
+            self._gql_persisted_query(
+                "StreamMetadata",
+                "059c4653b788f5bdb2f5a2d2a24b0ddc3831a15079001a3d927556a96fb0517f",
+                channelLogin=channel
+            )
+        ]
 
-    def metadata_channel(self, channel_id):
-        return self.call("/kraken/streams/{0}".format(channel_id), schema=validate.Schema(
-            {"stream": validate.any(
+        return self.call(queries, schema=validate.Schema(
+            [
                 validate.all(
-                    {"channel": {
-                        "display_name": str,
-                        "game": str,
-                        "status": str
-                    }},
-                    validate.get("channel"),
-                    validate.union_get("display_name", "status", "game")
+                    {"data": {"userOrError": {
+                        "displayName": str
+                    }}}
                 ),
-                validate.all(None, validate.transform(lambda _: (None,) * 3))
-            )},
-            validate.get("stream")
+                validate.all(
+                    {"data": {"user": {
+                        "lastBroadcast": {
+                            "title": str
+                        },
+                        "stream": {"game": {
+                            "name": str
+                        }}
+                    }}}
+                )
+            ],
+            validate.union_get(
+                (0, "data", "userOrError", "displayName"),
+                (1, "data", "user", "lastBroadcast", "title"),
+                (1, "data", "user", "stream", "game", "name")
+            )
         ))
 
-    # GraphQL API calls
-
     def access_token(self, is_live, channel_or_vod):
         query = self._gql_persisted_query(
             "PlaybackAccessToken",
@@ -282,7 +296,7 @@ class TwitchAPI:
             validate.union_get("signature", "value")
         ))
 
-        return self.call_gql(query, schema=validate.Schema(
+        return self.call(query, schema=validate.Schema(
             {"data": validate.any(
                 validate.all(
                     {"streamPlaybackAccessToken": subschema},
@@ -296,16 +310,6 @@ class TwitchAPI:
             validate.get("data")
         ))
 
-    @classmethod
-    def parse_token(cls, tokenstr):
-        return parse_json(tokenstr, schema=validate.Schema(
-            {"chansub": {"restricted_bitrates": validate.all(
-                [str],
-                validate.filter(lambda n: not re.match(r"(.+_)?archives|live|chunked", n))
-            )}},
-            validate.get(("chansub", "restricted_bitrates"))
-        ))
-
     def clips(self, clipname):
         queries = [
             self._gql_persisted_query(
@@ -325,7 +329,7 @@ class TwitchAPI:
             )
         ]
 
-        return self.call_gql(queries, schema=validate.Schema([
+        return self.call(queries, schema=validate.Schema([
             validate.all(
                 {"data": {"clip": {
                     "playbackAccessToken": validate.all(
@@ -370,7 +374,7 @@ class TwitchAPI:
             channelLogin=channel
         )
 
-        return self.call_gql(query, schema=validate.Schema(
+        return self.call(query, schema=validate.Schema(
             {"data": {"user": {"stream": {"type": str}}}},
             validate.get(("data", "user", "stream"))
         ))
@@ -382,16 +386,15 @@ class TwitchAPI:
             channelLogin=channel
         )
 
-        return self.call_gql(query, schema=validate.Schema(
+        return self.call(query, schema=validate.Schema(
             {"data": {"user": {
                 "hosting": {
-                    "id": validate.transform(int),
                     "login": str,
                     "displayName": str
                 }
             }}},
             validate.get(("data", "user", "hosting")),
-            validate.union_get("id", "login", "displayName")
+            validate.union_get("login", "displayName")
         ))
 
 
@@ -458,8 +461,7 @@ class Twitch(Plugin):
         self.params = parse_query(parsed.query)
         self.subdomain = match.get("subdomain")
         self.video_id = None
-        self._channel_id = None
-        self._channel = None
+        self.channel = None
         self.clip_name = None
         self.title = None
         self.author = None
@@ -469,12 +471,12 @@ class Twitch(Plugin):
             # pop-out player
             if self.params.get("video"):
                 self.video_id = self.params["video"]
-            self._channel = self.params.get("channel")
+            self.channel = self.params.get("channel")
         elif self.subdomain == "clips":
             # clip share URL
             self.clip_name = match.get("channel")
         else:
-            self._channel = match.get("channel") and match.get("channel").lower()
+            self.channel = match.get("channel") and match.get("channel").lower()
             self.video_id = match.get("video_id") or match.get("videos_id")
             self.clip_name = match.get("clip_name")
 
@@ -497,40 +499,15 @@ class Twitch(Plugin):
         return self.category
 
     def _get_metadata(self):
-        if self.video_id:
-            (self.author, self.title, self.category) = self.api.metadata_video(self.video_id)
-        elif self.clip_name:
-            self._get_clips()
-        elif self._channel:
-            (self.author, self.title, self.category) = self.api.metadata_channel(self.channel_id)
-
-    @property
-    def channel(self):
-        if not self._channel:
-            if self.video_id:
-                self._channel_from_video_id(self.video_id)
-        return self._channel
-
-    @property
-    def channel_id(self):
-        if not self._channel_id:
-            if self._channel:
-                self._channel_from_login(self._channel)
-            elif self.video_id:
-                self._channel_from_video_id(self.video_id)
-        return self._channel_id
-
-    def _channel_from_video_id(self, video_id):
         try:
-            self._channel_id, self._channel = self.api.channel_from_video_id(video_id)
-        except PluginError:
-            raise PluginError("Unable to find video: {0}".format(video_id))
-
-    def _channel_from_login(self, channel):
-        try:
-            self._channel_id = self.api.channel_from_login(channel)
-        except PluginError:
-            raise PluginError("Unable to find channel: {0}".format(channel))
+            if self.video_id:
+                (self.author, self.title, self.category) = self.api.metadata_video(self.video_id)
+            elif self.clip_name:
+                self._get_clips()
+            elif self.channel:
+                (self.author, self.title, self.category) = self.api.metadata_channel(self.channel)
+        except (PluginError, TypeError):
+            pass
 
     def _access_token(self, is_live, channel_or_vod):
         try:
@@ -550,7 +527,7 @@ class Twitch(Plugin):
         hosted_chain = [self.channel]
         while True:
             try:
-                target_id, login, display_name = self.api.hosted_channel(self.channel)
+                login, display_name = self.api.hosted_channel(self.channel)
             except PluginError:
                 return False
 
@@ -566,8 +543,7 @@ class Twitch(Plugin):
 
             hosted_chain.append(login)
             log.info("switching to {0}".format(login))
-            self._channel_id = target_id
-            self._channel = login
+            self.channel = login
             self.author = display_name
 
     def _check_for_rerun(self):
@@ -591,7 +567,7 @@ class Twitch(Plugin):
             return
 
         # only get the token once the channel has been resolved
-        log.debug("Getting live HLS streams for {0}".format(self.channel))
+        log.debug(f"Getting live HLS streams for {self.channel}")
         self.session.http.headers.update({
             "referer": "https://player.twitch.tv",
             "origin": "https://player.twitch.tv",
@@ -602,7 +578,7 @@ class Twitch(Plugin):
         return self._get_hls_streams(url, restricted_bitrates)
 
     def _get_hls_streams_video(self):
-        log.debug("Getting video HLS streams for {0}".format(self.channel))
+        log.debug(f"Getting HLS streams for video ID {self.video_id}")
         sig, token, restricted_bitrates = self._access_token(False, self.video_id)
         url = self.usher.video(self.video_id, nauthsig=sig, nauth=token)
 
@@ -646,7 +622,7 @@ class Twitch(Plugin):
             return self._get_hls_streams_video()
         elif self.clip_name:
             return self._get_clips()
-        elif self._channel:
+        elif self.channel:
             return self._get_hls_streams_live()
 
 


Test patch:
diff --git a/tests/plugins/test_twitch.py b/tests/plugins/test_twitch.py
index 87964cc0..50e243fe 100644
--- a/tests/plugins/test_twitch.py
+++ b/tests/plugins/test_twitch.py
@@ -5,7 +5,6 @@ from unittest.mock import MagicMock, call, patch
 import requests_mock
 
 from streamlink import Streamlink
-from streamlink.plugin import PluginError
 from streamlink.plugins.twitch import Twitch, TwitchHLSStream, TwitchHLSStreamReader, TwitchHLSStreamWriter
 from tests.mixins.stream_hls import EventedHLSStreamWriter, Playlist, Segment as _Segment, Tag, TestMixinStreamHLS
 from tests.plugins import PluginCanHandleUrl
@@ -299,81 +298,127 @@ class TestTwitchHLSStream(TestMixinStreamHLS, unittest.TestCase):
 class TestTwitchMetadata(unittest.TestCase):
     def setUp(self):
         self.mock = requests_mock.Mocker()
+        self.mock.register_uri(requests_mock.ANY, requests_mock.ANY, exc=requests_mock.exceptions.InvalidRequest)
         self.mock.start()
 
     def tearDown(self):
         self.mock.stop()
 
-    def subject(self, url):
+    @staticmethod
+    def subject(url):
         session = Streamlink()
         Twitch.bind(session, "tests.plugins.test_twitch")
         plugin = Twitch(url)
         return plugin.get_author(), plugin.get_title(), plugin.get_category()
 
-    def subject_channel(self, data=True, failure=False):
-        self.mock.get(
-            "https://api.twitch.tv/kraken/users?login=foo",
-            json={"users": [{"_id": 1234}]}
+    def mock_request_channel(self, data=True):
+        return self.mock.post(
+            "https://gql.twitch.tv/gql",
+            json=[
+                {"data": {"userOrError": {"userDoesNotExist": "error"} if not data else {
+                    "displayName": "channel name"
+                }}},
+                {"data": {"user": None if not data else {
+                    "lastBroadcast": {
+                        "title": "channel status"
+                    },
+                    "stream": {"game": {
+                        "name": "channel game"
+                    }}
+                }}}
+            ]
         )
-        self.mock.get(
-            "https://api.twitch.tv/kraken/streams/1234",
-            status_code=200 if not failure else 404,
-            json={"stream": None} if not data else {"stream": {
-                "channel": {
-                    "display_name": "channel name",
-                    "status": "channel status",
-                    "game": "channel game"
-                }
-            }}
-        )
-        return self.subject("https://twitch.tv/foo")
 
-    def subject_video(self, data=True, failure=False):
-        self.mock.get(
-            "https://api.twitch.tv/kraken/videos/1337",
-            status_code=200 if not failure else 404,
-            json={} if not data else {
+    def mock_request_video(self, data=True):
+        return self.mock.post(
+            "https://gql.twitch.tv/gql",
+            json={"data": {"video": None if not data else {
                 "title": "video title",
-                "game": "video game",
-                "channel": {
-                    "display_name": "channel name"
+                "game": {
+                    "displayName": "video game"
+                },
+                "owner": {
+                    "displayName": "channel name"
                 }
-            }
+            }}}
         )
-        return self.subject("https://twitch.tv/videos/1337")
 
-    def test_metadata_channel_exists(self):
-        author, title, category = self.subject_channel()
+    def test_metadata_channel(self):
+        mock = self.mock_request_channel()
+        author, title, category = self.subject("https://twitch.tv/foo")
         self.assertEqual(author, "channel name")
         self.assertEqual(title, "channel status")
         self.assertEqual(category, "channel game")
+        self.assertEqual(mock.request_history[0].json(), [
+            {
+                "operationName": "ChannelShell",
+                "extensions": {
+                    "persistedQuery": {
+                        "version": 1,
+                        "sha256Hash": "c3ea5a669ec074a58df5c11ce3c27093fa38534c94286dc14b68a25d5adcbf55"
+                    }
+                },
+                "variables": {
+                    "login": "foo",
+                    "lcpVideosEnabled": False
+                }
+            },
+            {
+                "operationName": "StreamMetadata",
+                "extensions": {
+                    "persistedQuery": {
+                        "version": 1,
+                        "sha256Hash": "059c4653b788f5bdb2f5a2d2a24b0ddc3831a15079001a3d927556a96fb0517f"
+                    }
+                },
+                "variables": {
+                    "channelLogin": "foo"
+                }
+            }
+        ])
 
-    def test_metadata_channel_missing(self):
-        metadata = self.subject_channel(data=False)
-        self.assertEqual(metadata, (None, None, None))
-
-    def test_metadata_channel_invalid(self):
-        with self.assertRaises(PluginError):
-            self.subject_channel(failure=True)
+    def test_metadata_channel_no_data(self):
+        self.mock_request_channel(data=False)
+        author, title, category = self.subject("https://twitch.tv/foo")
+        self.assertEqual(author, None)
+        self.assertEqual(title, None)
+        self.assertEqual(category, None)
 
-    def test_metadata_video_exists(self):
-        author, title, category = self.subject_video()
+    def test_metadata_video(self):
+        mock = self.mock_request_video()
+        author, title, category = self.subject("https://twitch.tv/videos/1337")
         self.assertEqual(author, "channel name")
         self.assertEqual(title, "video title")
         self.assertEqual(category, "video game")
+        self.assertEqual(
+            mock.request_history[0].json(),
+            {
+                "operationName": "VideoMetadata",
+                "extensions": {
+                    "persistedQuery": {
+                        "version": 1,
+                        "sha256Hash": "cb3b1eb2f2d2b2f65b8389ba446ec521d76c3aa44f5424a1b1d235fe21eb4806"
+                    }
+                },
+                "variables": {
+                    "channelLogin": "",
+                    "videoID": "1337"
+                }
+            }
+        )
 
-    def test_metadata_video_missing(self):
-        metadata = self.subject_video(data=False)
-        self.assertEqual(metadata, (None, None, None))
-
-    def test_metadata_video_invalid(self):
-        with self.assertRaises(PluginError):
-            self.subject_video(failure=True)
+    def test_metadata_video_no_data(self):
+        self.mock_request_video(data=False)
+        author, title, category = self.subject("https://twitch.tv/videos/1337")
+        self.assertEqual(author, None)
+        self.assertEqual(title, None)
+        self.assertEqual(category, None)
 
 
 @patch("streamlink.plugins.twitch.log")
 class TestTwitchHosting(unittest.TestCase):
-    def subject(self, channel, hosts=None, disable=False):
+    @staticmethod
+    def subject(channel, hosts=None, disable=False):
         with requests_mock.Mocker() as mock:
             mock.register_uri(requests_mock.ANY, requests_mock.ANY, exc=requests_mock.exceptions.InvalidRequest)
             if hosts is None:
@@ -382,10 +427,9 @@ class TestTwitchHosting(unittest.TestCase):
                 mock.post("https://gql.twitch.tv/gql", response_list=[
                     {"json": {"data": {"user": {
                         "id": host[0],
-                        "hosting": None if not host[1:4] else {
-                            "id": host[1],
-                            "login": host[2],
-                            "displayName": host[3]
+                        "hosting": None if not host[1:3] else {
+                            "login": host[1],
+                            "displayName": host[2]
                         }}}}
                      } for host in hosts
                 ])
@@ -396,31 +440,28 @@ class TestTwitchHosting(unittest.TestCase):
             plugin.options.set("disable-hosting", disable)
 
             res = plugin._switch_to_hosted_channel()
-            return res, plugin.channel, plugin._channel_id, plugin.author
+            return res, plugin.channel, plugin.author
 
     def test_hosting_invalid_host_data(self, mock_log):
-        res, channel, channel_id, author = self.subject("foo")
+        res, channel, author = self.subject("foo")
         self.assertFalse(res, "Doesn't stop HLS resolve procedure")
         self.assertEqual(channel, "foo", "Doesn't switch channel")
-        self.assertEqual(channel_id, None, "Doesn't set channel id")
         self.assertEqual(author, None, "Doesn't override author metadata")
         self.assertEqual(mock_log.info.mock_calls, [], "Doesn't log anything to info")
         self.assertEqual(mock_log.error.mock_calls, [], "Doesn't log anything to error")
 
     def test_hosting_no_host_data(self, mock_log):
-        res, channel, channel_id, author = self.subject("foo", [(1,)])
+        res, channel, author = self.subject("foo", [(1,)])
         self.assertFalse(res, "Doesn't stop HLS resolve procedure")
         self.assertEqual(channel, "foo", "Doesn't switch channel")
-        self.assertEqual(channel_id, None, "Doesn't set channel id")
         self.assertEqual(author, None, "Doesn't override author metadata")
         self.assertEqual(mock_log.info.mock_calls, [], "Doesn't log anything to info")
         self.assertEqual(mock_log.error.mock_calls, [], "Doesn't log anything to error")
 
     def test_hosting_host_single(self, mock_log):
-        res, channel, channel_id, author = self.subject("foo", [(1, 2, "bar", "Bar"), (2,)])
+        res, channel, author = self.subject("foo", [(1, "bar", "Bar"), (2,)])
         self.assertFalse(res, "Doesn't stop HLS resolve procedure")
         self.assertEqual(channel, "bar", "Switches channel")
-        self.assertEqual(channel_id, 2, "Sets channel id")
         self.assertEqual(author, "Bar", "Overrides author metadata")
         self.assertEqual(mock_log.info.mock_calls, [
             call("foo is hosting bar"),
@@ -429,10 +470,9 @@ class TestTwitchHosting(unittest.TestCase):
         self.assertEqual(mock_log.error.mock_calls, [], "Doesn't log anything to error")
 
     def test_hosting_host_single_disable(self, mock_log):
-        res, channel, channel_id, author = self.subject("foo", [(1, 2, "bar", "Bar")], disable=True)
+        res, channel, author = self.subject("foo", [(1, "bar", "Bar")], disable=True)
         self.assertTrue(res, "Stops HLS resolve procedure")
         self.assertEqual(channel, "foo", "Doesn't switch channel")
-        self.assertEqual(channel_id, None, "Doesn't set channel id")
         self.assertEqual(author, None, "Doesn't override author metadata")
         self.assertEqual(mock_log.info.mock_calls, [
             call("foo is hosting bar"),
@@ -441,15 +481,14 @@ class TestTwitchHosting(unittest.TestCase):
         self.assertEqual(mock_log.error.mock_calls, [], "Doesn't log anything to error")
 
     def test_hosting_host_multiple(self, mock_log):
-        res, channel, channel_id, author = self.subject("foo", [
-            (1, 2, "bar", "Bar"),
-            (2, 3, "baz", "Baz"),
-            (3, 4, "qux", "Qux"),
+        res, channel, author = self.subject("foo", [
+            (1, "bar", "Bar"),
+            (2, "baz", "Baz"),
+            (3, "qux", "Qux"),
             (4,)
         ])
         self.assertFalse(res, "Doesn't stop HLS resolve procedure")
         self.assertEqual(channel, "qux", "Switches channel")
-        self.assertEqual(channel_id, 4, "Sets channel id")
         self.assertEqual(author, "Qux", "Overrides author metadata")
         self.assertEqual(mock_log.info.mock_calls, [
             call("foo is hosting bar"),
@@ -462,14 +501,13 @@ class TestTwitchHosting(unittest.TestCase):
         self.assertEqual(mock_log.error.mock_calls, [], "Doesn't log anything to error")
 
     def test_hosting_host_multiple_loop(self, mock_log):
-        res, channel, channel_id, author = self.subject("foo", [
-            (1, 2, "bar", "Bar"),
-            (2, 3, "baz", "Baz"),
-            (3, 1, "foo", "Foo")
+        res, channel, author = self.subject("foo", [
+            (1, "bar", "Bar"),
+            (2, "baz", "Baz"),
+            (3, "foo", "Foo")
         ])
         self.assertTrue(res, "Stops HLS resolve procedure")
         self.assertEqual(channel, "baz", "Has switched channel")
-        self.assertEqual(channel_id, 3, "Has set channel id")
         self.assertEqual(author, "Baz", "Has overridden author metadata")
         self.assertEqual(mock_log.info.mock_calls, [
             call("foo is hosting bar"),