Repository: streamlink/streamlink

Issue:
--twitch-disable-reruns error
<!--
Thanks for reporting a bug!
USE THE TEMPLATE. Otherwise your bug report may be rejected.

First, see the contribution guidelines:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Also check the list of open and closed bug reports:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Bug Report

<!-- Replace [ ] with [x] in order to check the box -->
- [x] This is a bug report and I have read the contribution guidelines.


### Description

<!-- Explain the bug as thoroughly as you can. Don't leave out information which is necessary for us to reproduce and debug this issue. -->
Reruns disabling seems to be working but this error accurs in cmd
![cmd_ZJg3To8rUo](https://user-images.githubusercontent.com/51099894/73242786-f5af9680-41b6-11ea-973e-aaf14b37f472.png)

### Expected / Actual behavior

<!-- What do you expect to happen, and what is actually happening? -->


### Reproduction steps / Explicit stream URLs to test

<!-- How can we reproduce this? Please note the exact steps below using the list format supplied. If you need more steps please add them. -->

1. ...
2. ...
3. ...


### Log output

<!--
TEXT LOG OUTPUT IS REQUIRED for a bug report!
Use the `--loglevel debug` parameter and avoid using parameters which suppress log output.
https://streamlink.github.io/cli.html#cmdoption-l

Make sure to **remove usernames and passwords**
You can copy the output to https://gist.github.com/ or paste it below.
-->

```
REPLACE THIS TEXT WITH THE LOG OUTPUT
```


### Additional comments, screenshots, etc.



[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)



Discussion:
Oh boy, here we go... Looks like the [validation scheme implemented in #2724](https://github.com/streamlink/streamlink/pull/2724/commits/d3130ca464234d8f0a05874b31c9e5a08b75b75c#diff-195bebeb65dac1f6a8e94f1957a95ec1R103) is a bit too strict for the returned Twitch API response. According to Twitch's website (which runs off of their GQL API, and not the kraken or helix REST APIs), the channel you've posted (welovegames) is currently streaming a rerun of a previous broadcast. But when looking at the API response from the kraken API, the stream properties `stream_type` and `broadcast_platform` which are used for checking the status don't indicate a rerun. See here:

```bash
$ curl -s \
  -H "Accept: application/vnd.twitchtv.v5+json" \
  -H "Client-ID: phiay4sq36lfv9zu7cbqwz2ndnesfd8" \
  "https://api.twitch.tv/kraken/streams/30814134" \
  | jq
```
```json
{
  "stream": {
    "_id": 36780183456,
    "game": "Just Chatting",
    "broadcast_platform": "other",
    "community_id": "",
    "community_ids": null,
    "viewers": 0,
    "video_height": 720,
    "average_fps": 60,
    "delay": 0,
    "created_at": "2020-01-28T01:39:27Z",
    "is_playlist": false,
    "stream_type": "live",
    "preview": {
      "small": "https://static-cdn.jtvnw.net/previews-ttv/live_user_welovegames-80x45.jpg",
      "medium": "https://static-cdn.jtvnw.net/previews-ttv/live_user_welovegames-320x180.jpg",
      "large": "https://static-cdn.jtvnw.net/previews-ttv/live_user_welovegames-640x360.jpg",
      "template": "https://static-cdn.jtvnw.net/previews-ttv/live_user_welovegames-{width}x{height}.jpg"
    },
    "channel": {
      "mature": false,
      "status": "BB",
      "broadcaster_language": "ru",
      "broadcaster_software": "watch_party_rerun",
      "display_name": "WELOVEGAMES",
      "game": "Just Chatting",
      "language": "ru",
      "_id": 30814134,
      "name": "welovegames",
      "created_at": "2012-05-26T11:18:26.294212Z",
      "updated_at": "2020-01-28T07:12:34.107982Z",
      "partner": true,
      "logo": "https://static-cdn.jtvnw.net/jtv_user_pictures/welovegames-profile_image-15afef2e74a108da-300x300.png",
      "video_banner": null,
      "profile_banner": "https://static-cdn.jtvnw.net/jtv_user_pictures/welovegames-profile_banner-5700d3683ec069c3-480.png",
      "profile_banner_background_color": "",
      "url": "https://www.twitch.tv/welovegames",
      "views": 74717818,
      "followers": 387170,
      "broadcaster_type": "partner",
      "description": "vk.com/welovegames",
      "private_video": false,
      "privacy_options_enabled": false
    }
  }
}
```

What's unexpected here is that `broadcast_platform` is set to `other`, which makes the validation scheme fail. As I've mentioned [in my review of #2722](https://github.com/streamlink/streamlink/pull/2722/files#r352730608), these two properties are super inconsistent and are a result of Twitch not documenting properly and changing stuff several times throughout the recent years. If you take a look at my comment over there, you can see in the added API response log that the `broadcast_platform` property can indeed be set to `rerun`, which means it has to be validated here in addition to `stream_type`, because as you can also see, `stream_type` is set to `live` in the API response posted above, even though the documentation says that it should not be `live` for offline stream reruns. What's surprising though is that there's an additional `channel.broadcaster_software` property which is set to `watch_party_rerun`. It's a total mess and checking this third property makes it even more confusing as it's not documented as well.
https://dev.twitch.tv/docs/v5/reference/streams#get-stream-by-user

So how can we check whether a stream is a rerun now? As I've said, checking for `stream_type` alone is not sufficient and checking for `broadcast_platform==rerun` is also not correct. Checking for other values like `other` is a bit weird though, because the value can also be `ps3` for channels streaming from a console. I guess we'll have to add this additional third property to the validation scheme and also make it less strict. :weary: 
> Oh boy, here we go... Looks like the [validation scheme implemented in #2724](https://github.com/streamlink/streamlink/pull/2724/commits/d3130ca464234d8f0a05874b31c9e5a08b75b75c#diff-195bebeb65dac1f6a8e94f1957a95ec1R103) is a bit too strict for the returned Twitch API response. According to Twitch's website (which runs off of their GQL API, and not the kraken or helix REST APIs), the channel you've posted (welovegames) is currently streaming a rerun of a previous broadcast. But when looking at the API response from the kraken API, the stream properties `stream_type` and `broadcast_platform` which are used for checking the status don't indicate a rerun. See here:
> 
> ```shell
> $ curl -s \
>   -H "Accept: application/vnd.twitchtv.v5+json" \
>   -H "Client-ID: phiay4sq36lfv9zu7cbqwz2ndnesfd8" \
>   "https://api.twitch.tv/kraken/streams/30814134" \
>   | jq
> ```
> 
> ```json
> {
>   "stream": {
>     "_id": 36780183456,
>     "game": "Just Chatting",
>     "broadcast_platform": "other",
>     "community_id": "",
>     "community_ids": null,
>     "viewers": 0,
>     "video_height": 720,
>     "average_fps": 60,
>     "delay": 0,
>     "created_at": "2020-01-28T01:39:27Z",
>     "is_playlist": false,
>     "stream_type": "live",
>     "preview": {
>       "small": "https://static-cdn.jtvnw.net/previews-ttv/live_user_welovegames-80x45.jpg",
>       "medium": "https://static-cdn.jtvnw.net/previews-ttv/live_user_welovegames-320x180.jpg",
>       "large": "https://static-cdn.jtvnw.net/previews-ttv/live_user_welovegames-640x360.jpg",
>       "template": "https://static-cdn.jtvnw.net/previews-ttv/live_user_welovegames-{width}x{height}.jpg"
>     },
>     "channel": {
>       "mature": false,
>       "status": "BB",
>       "broadcaster_language": "ru",
>       "broadcaster_software": "watch_party_rerun",
>       "display_name": "WELOVEGAMES",
>       "game": "Just Chatting",
>       "language": "ru",
>       "_id": 30814134,
>       "name": "welovegames",
>       "created_at": "2012-05-26T11:18:26.294212Z",
>       "updated_at": "2020-01-28T07:12:34.107982Z",
>       "partner": true,
>       "logo": "https://static-cdn.jtvnw.net/jtv_user_pictures/welovegames-profile_image-15afef2e74a108da-300x300.png",
>       "video_banner": null,
>       "profile_banner": "https://static-cdn.jtvnw.net/jtv_user_pictures/welovegames-profile_banner-5700d3683ec069c3-480.png",
>       "profile_banner_background_color": "",
>       "url": "https://www.twitch.tv/welovegames",
>       "views": 74717818,
>       "followers": 387170,
>       "broadcaster_type": "partner",
>       "description": "vk.com/welovegames",
>       "private_video": false,
>       "privacy_options_enabled": false
>     }
>   }
> }
> ```
> 
> What's unexpected here is that `broadcast_platform` is set to `other`, which makes the validation scheme fail. As I've mentioned [in my review of #2722](https://github.com/streamlink/streamlink/pull/2722/files#r352730608), these two properties are super inconsistent and are a result of Twitch not documenting properly and changing stuff several times throughout the recent years. If you take a look at my comment over there, you can see in the added API response log that the `broadcast_platform` property can indeed be set to `rerun`, which means it has to be validated here in addition to `stream_type`, because as you can also see, `stream_type` is set to `live` in the API response posted above, even though the documentation says that it should not be `live` for offline stream reruns. What's surprising though is that there's an additional `channel.broadcaster_software` property which is set to `watch_party_rerun`. It's a total mess and checking this third property makes it even more confusing as it's not documented as well.
> https://dev.twitch.tv/docs/v5/reference/streams#get-stream-by-user
> 
> So how can we check whether a stream is a rerun now? As I've said, checking for `stream_type` alone is not sufficient and checking for `broadcast_platform==rerun` is also not correct. Checking for other values like `other` is a bit weird though, because the value can also be `ps3` for channels streaming from a console. I guess we'll have to add this additional third property to the validation scheme and also make it less strict. 😩

Ok. Thanks for your work.


Resolving patch:
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -99,8 +99,11 @@
 _stream_schema = validate.Schema(
     {
         "stream": validate.any(None, {
-            "stream_type": validate.any("live", "rerun"),
-            "broadcast_platform": validate.any("live", "rerun")
+            "stream_type": validate.text,
+            "broadcast_platform": validate.text,
+            "channel": validate.any(None, {
+                "broadcaster_software": validate.text
+            })
         })
     },
     validate.get("stream")
@@ -709,7 +712,11 @@ def _check_for_host(self):
     def _check_for_rerun(self):
         stream = self.api.streams(self.channel_id, schema=_stream_schema)
 
-        return stream and (stream["stream_type"] == "rerun" or stream["broadcast_platform"] == "rerun")
+        return stream and (
+            stream["stream_type"] != "live"
+            or stream["broadcast_platform"] == "rerun"
+            or stream["channel"] and stream["channel"]["broadcaster_software"] == "watch_party_rerun"
+        )
 
     def _get_hls_streams(self, stream_type="live"):
         log.debug("Getting {0} HLS streams for {1}".format(stream_type, self.channel))


Test patch:
diff --git a/tests/plugins/test_twitch.py b/tests/plugins/test_twitch.py
--- a/tests/plugins/test_twitch.py
+++ b/tests/plugins/test_twitch.py
@@ -208,8 +208,11 @@ def start(self, *mocked, **params):
             mocked_stream = mock.get(
                 "https://api.twitch.tv/kraken/streams/1234.json",
                 json={"stream": None} if params.pop("offline", False) else {"stream": {
-                    "stream_type": params.pop("stream_type", "rerun"),
-                    "broadcast_platform": params.pop("broadcast_platform", "rerun")
+                    "stream_type": params.pop("stream_type", "live"),
+                    "broadcast_platform": params.pop("broadcast_platform", "live"),
+                    "channel": {
+                        "broadcaster_software": params.pop("broadcaster_software", "")
+                    }
                 }}
             )
 
@@ -220,13 +223,21 @@ def start(self, *mocked, **params):
             try:
                 streams = plugin.streams()
             except TestTwitchReruns.StopError:
-                streams = None
+                streams = True
                 pass
 
             return streams, mocked_users, mocked_stream, mocked[0]
 
-    def test_disable_reruns(self, mock_log):
-        streams, api_users, api_stream, access_token = self.start(stream_type="rerun", broadcast_platform="rerun")
+    def test_disable_reruns_live(self, mock_log):
+        streams, api_users, api_stream, access_token = self.start()
+        self.assertTrue(api_users.called_once)
+        self.assertTrue(api_stream.called_once)
+        self.assertTrue(access_token.called_once)
+        self.assertTrue(streams)
+        self.assertNotIn(self.log_call, mock_log.info.call_args_list)
+
+    def test_disable_reruns_not_live(self, mock_log):
+        streams, api_users, api_stream, access_token = self.start(stream_type="rerun")
         self.assertTrue(api_users.called_once)
         self.assertTrue(api_stream.called_once)
         self.assertFalse(access_token.called)
@@ -249,20 +260,20 @@ def test_disable_reruns_mixed2(self, mock_log):
         self.assertDictEqual(streams, {})
         self.assertIn(self.log_call, mock_log.info.call_args_list)
 
-    def test_disable_reruns_live(self, mock_log):
-        streams, api_users, api_stream, access_token = self.start(stream_type="live", broadcast_platform="live")
+    def test_disable_reruns_broadcaster_software(self, mock_log):
+        streams, api_users, api_stream, access_token = self.start(broadcaster_software="watch_party_rerun")
         self.assertTrue(api_users.called_once)
         self.assertTrue(api_stream.called_once)
         self.assertTrue(access_token.called_once)
-        self.assertIsNone(streams)
-        self.assertNotIn(self.log_call, mock_log.info.call_args_list)
+        self.assertDictEqual(streams, {})
+        self.assertIn(self.log_call, mock_log.info.call_args_list)
 
     def test_disable_reruns_offline(self, mock_log):
         streams, api_users, api_stream, access_token = self.start(offline=True)
         self.assertTrue(api_users.called_once)
         self.assertTrue(api_stream.called_once)
         self.assertTrue(access_token.called_once)
-        self.assertIsNone(streams)
+        self.assertTrue(streams)
         self.assertNotIn(self.log_call, mock_log.info.call_args_list)
 
     def test_enable_reruns(self, mock_log):
@@ -270,5 +281,5 @@ def test_enable_reruns(self, mock_log):
         self.assertFalse(api_users.called)
         self.assertFalse(api_stream.called)
         self.assertTrue(access_token.called_once)
-        self.assertIsNone(streams)
+        self.assertTrue(streams)
         self.assertNotIn(self.log_call, mock_log.info.call_args_list)