Repository: nicklaw5/helix
Pull request #84 (2021-04-21 21:44:29)

Add "Get Followed Streams" feature
New feature implemented by Twitch the 2021‑04‑20


Patch:
diff --git a/README.md b/README.md
index 461d205..42b698d 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ looking for the Twitch API docs, see the [Twitch Developer website](https://dev.
 - [ ] Search Channels
 - [ ] Get Stream Key
 - [x] Get Streams
+- [x] Get Followed Streams
 - [x] Create Stream Marker
 - [x] Get Stream Markers
 - [x] Get Broadcaster Subscriptions
diff --git a/docs/streams_docs.md b/docs/streams_docs.md
index 386b1b9..a03e4b3 100644
--- a/docs/streams_docs.md
+++ b/docs/streams_docs.md
@@ -22,3 +22,26 @@ if err != nil {
 
 fmt.Printf("%+v\n", resp)
 ```
+
+## Get Followed Streams
+
+This is an example of how to get followed streams.
+
+```go
+client, err := helix.NewClient(&helix.Options{
+    ClientID: "your-client-id",
+})
+if err != nil {
+    // handle error
+}
+
+resp, err := client.GetFollowedStream(&helix.FollowedStreamsParams{
+    UserID: "123456",
+})
+if err != nil {
+    // handle error
+}
+
+fmt.Printf("%+v\n", resp)
+```
+
diff --git a/streams.go b/streams.go
index 17f53e1..1dee8b5 100644
--- a/streams.go
+++ b/streams.go
@@ -9,6 +9,7 @@ type Stream struct {
 	UserLogin    string    `json:"user_login"`
 	UserName     string    `json:"user_name"`
 	GameID       string    `json:"game_id"`
+	GameName     string    `json:"game_name"`
 	TagIDs       []string  `json:"tag_ids"`
 	IsMature     bool      `json:"is_mature"`
 	Type         string    `json:"type"`
@@ -58,3 +59,33 @@ func (c *Client) GetStreams(params *StreamsParams) (*StreamsResponse, error) {
 
 	return streams, nil
 }
+
+// FollowedStreamsParams ...
+type FollowedStreamsParams struct {
+	After      string   `query:"after"`
+	Before     string   `query:"before"`
+	First      int      `query:"first,20"`   // Limit 100
+	UserID     string   `query:"user_id"`
+}
+
+// GetFollowedStream : Gets information about active streams belonging to channels
+// that the authenticated user follows. Streams are returned sorted by number of
+// current viewers, in descending order. Across multiple pages of results, there
+// may be duplicate or missing streams, as viewers join and leave streams.
+//
+// Required scope: user:read:follows
+func (c *Client) GetFollowedStream(params *FollowedStreamsParams) (*StreamsResponse, error) {
+	resp, err := c.get("/streams/followed", &ManyStreams{}, params)
+	if err != nil {
+		return nil, err
+	}
+
+	streams := &StreamsResponse{}
+	resp.HydrateResponseCommon(&streams.ResponseCommon)
+	streams.Data.Streams = resp.Data.(*ManyStreams).Streams
+	streams.Data.Pagination = resp.Data.(*ManyStreams).Pagination
+
+	return streams, nil
+}
+
+


Test patch:
diff --git a/streams_test.go b/streams_test.go
index 042b16b..e192f1b 100644
--- a/streams_test.go
+++ b/streams_test.go
@@ -18,7 +18,7 @@ func TestGetStreams(t *testing.T) {
 			http.StatusOK,
 			&Options{ClientID: "my-client-id"},
 			2,
-			`{"data":[{"id":"27833742640","user_id":"19571641","user_login":"ninja","user_name":"Ninja","game_id":"33214","tag_ids":[],"type":"live","title":"I have lost my voice D: | twitter.com/Ninja","viewer_count":72124,"started_at":"2018-03-06T15:07:45Z","language":"en","thumbnail_url":"https://static-cdn.jtvnw.net/previews-ttv/live_user_ninja-{width}x{height}.jpg","is_mature": false},{"id":"27834185424","user_id":"17337557","user_name":"DrDisrespect","game_id":"33214","tag_ids":[],"type":"live","title":"Turbo Treehouses || @DrDisRespect","viewer_count":29687,"started_at":"2018-03-06T16:05:00Z","language":"en","thumbnail_url":"https://static-cdn.jtvnw.net/previews-ttv/live_user_drdisrespectlive-{width}x{height}.jpg","is_mature": true}],"pagination":{"cursor":"eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6Mn19"}}`,
+			`{"data":[{"id":"27833742640","user_id":"19571641","user_login":"ninja","user_name":"Ninja","game_id":"33214","game_name":"Tekken","tag_ids":[],"type":"live","title":"I have lost my voice D: | twitter.com/Ninja","viewer_count":72124,"started_at":"2018-03-06T15:07:45Z","language":"en","thumbnail_url":"https://static-cdn.jtvnw.net/previews-ttv/live_user_ninja-{width}x{height}.jpg","is_mature": false},{"id":"27834185424","user_id":"17337557","user_name":"DrDisrespect","game_id":"33214","game_name":"Tekken","tag_ids":[],"type":"live","title":"Turbo Treehouses || @DrDisRespect","viewer_count":29687,"started_at":"2018-03-06T16:05:00Z","language":"en","thumbnail_url":"https://static-cdn.jtvnw.net/previews-ttv/live_user_drdisrespectlive-{width}x{height}.jpg","is_mature": true}],"pagination":{"cursor":"eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6Mn19"}}`,
 		},
 		{
 			http.StatusBadRequest,
@@ -55,4 +55,98 @@ func TestGetStreams(t *testing.T) {
 			t.Errorf("expected \"%d\" streams, got \"%d\"", testCase.First, len(resp.Data.Streams))
 		}
 	}
+
+    // Test with HTTP Failure
+    options := &Options{
+        ClientID: "my-client-id",
+        HTTPClient: &badMockHTTPClient{
+            newMockHandler(0, "", nil),
+        },
+    }
+
+    c := &Client{
+        opts: options,
+    }
+
+    _, err := c.GetStreams(&StreamsParams{
+        Language: []string{"en"},
+    })
+    if err == nil {
+        t.Error("expected error but got nil")
+    }
+
+    if err.Error() != "Failed to execute API request: Oops, that's bad :(" {
+        t.Error("expected error does match return error")
+    }
+}
+
+func TestGetFollowedStreams(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		statusCode int
+		options    *Options
+		UserID     string
+		respBody   string
+	}{
+		{
+			http.StatusOK,
+			&Options{ClientID: "my-client-id"},
+			"123456789",
+			`{"data":[{"id":"27833742640","user_id":"19571641","user_login":"ninja","user_name":"Ninja","game_id":"33214","game_name":"Tekken","tag_ids":[],"type":"live","title":"I have lost my voice D: | twitter.com/Ninja","viewer_count":72124,"started_at":"2018-03-06T15:07:45Z","language":"en","thumbnail_url":"https://static-cdn.jtvnw.net/previews-ttv/live_user_ninja-{width}x{height}.jpg","is_mature": false},{"id":"27834185424","user_id":"17337557","user_name":"DrDisrespect","game_id":"33214","game_name":"Tekken","tag_ids":[],"type":"live","title":"Turbo Treehouses || @DrDisRespect","viewer_count":29687,"started_at":"2018-03-06T16:05:00Z","language":"en","thumbnail_url":"https://static-cdn.jtvnw.net/previews-ttv/live_user_drdisrespectlive-{width}x{height}.jpg","is_mature": true}],"pagination":{"cursor":"eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6Mn19"}}`,
+		},
+		{
+			http.StatusBadRequest,
+			&Options{ClientID: "my-client-id"},
+			"",
+			`{"error":"Bad Request","status":400,"message":"Missing required parameter \"user_id\""}`,
+		},
+	}
+
+	for _, testCase := range testCases {
+		c := newMockClient(testCase.options, newMockHandler(testCase.statusCode, testCase.respBody, nil))
+
+		resp, err := c.GetFollowedStream(&FollowedStreamsParams{
+            UserID: testCase.UserID,
+		})
+		if err != nil {
+			t.Error(err)
+		}
+
+		// Test Bad Request Responses
+		if resp.StatusCode == http.StatusBadRequest {
+			firstErrStr := "Missing required parameter \"user_id\""
+			if resp.ErrorMessage != firstErrStr {
+				t.Errorf("expected error message to be \"%s\", got \"%s\"", firstErrStr, resp.ErrorMessage)
+			}
+			continue
+		}
+
+		if resp.StatusCode != testCase.statusCode {
+			t.Errorf("expected status code to be \"%d\", got \"%d\"", testCase.statusCode, resp.StatusCode)
+		}
+	}
+
+    // Test with HTTP Failure
+    options := &Options{
+        ClientID: "my-client-id",
+        HTTPClient: &badMockHTTPClient{
+            newMockHandler(0, "", nil),
+        },
+    }
+
+    c := &Client{
+        opts: options,
+    }
+
+    _, err := c.GetFollowedStream(&FollowedStreamsParams{
+        UserID: "123456",
+    })
+    if err == nil {
+        t.Error("expected error but got nil")
+    }
+
+    if err.Error() != "Failed to execute API request: Oops, that's bad :(" {
+        t.Error("expected error does match return error")
+    }
 }