Skip to main content

kick_api/models/
followed_channel.rs

1use serde::Deserialize;
2
3/// Paginated response from the followed channels endpoint.
4///
5/// **⚠️ Unofficial API** — This uses Kick's internal v2 API, not the public
6/// API. It may break without notice.
7#[derive(Debug, Clone, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub struct FollowedChannelsResponse {
10    /// Cursor for fetching the next page. `None` when there are no more results.
11    #[serde(default)]
12    pub next_cursor: Option<u64>,
13
14    /// The list of followed channels.
15    #[serde(default)]
16    pub channels: Vec<FollowedChannel>,
17}
18
19/// A followed channel from Kick's unofficial v2 API.
20///
21/// Returned inside [`FollowedChannelsResponse`] by
22/// [`fetch_followed_channels`](crate::fetch_followed_channels).
23///
24/// **⚠️ Unofficial API** — This uses Kick's internal v2 API, not the public
25/// API. It may break without notice.
26#[derive(Debug, Clone, Deserialize)]
27pub struct FollowedChannel {
28    /// Whether the channel is currently live
29    #[serde(default)]
30    pub is_live: bool,
31
32    /// Profile picture URL
33    #[serde(default)]
34    pub profile_picture: Option<String>,
35
36    /// Channel URL slug (lowercase)
37    #[serde(default)]
38    pub channel_slug: Option<String>,
39
40    /// Current viewer count (0 if offline)
41    #[serde(default)]
42    pub viewer_count: u64,
43
44    /// Category name (e.g. "Just Chatting", "IRL"). Empty string if offline.
45    #[serde(default)]
46    pub category_name: Option<String>,
47
48    /// Display username
49    #[serde(default)]
50    pub user_username: Option<String>,
51
52    /// Current stream title (`None` if offline)
53    #[serde(default)]
54    pub session_title: Option<String>,
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_deserialize_followed_channels_response() {
63        let json = r##"{
64            "nextCursor": 5,
65            "channels": [
66                {
67                    "is_live": true,
68                    "profile_picture": "https://files.kick.com/images/user/57253/profile_image/thumb.webp",
69                    "channel_slug": "knut",
70                    "viewer_count": 151,
71                    "category_name": "IRL",
72                    "user_username": "Knut",
73                    "session_title": "NPC Show + Iron World Fit Week expo"
74                },
75                {
76                    "is_live": false,
77                    "profile_picture": "https://files.kick.com/images/user/73899717/profile_image/thumb.webp",
78                    "channel_slug": "anxstasia",
79                    "viewer_count": 0,
80                    "category_name": "",
81                    "user_username": "anxstasia",
82                    "session_title": null
83                }
84            ]
85        }"##;
86
87        let resp: FollowedChannelsResponse = serde_json::from_str(json).unwrap();
88
89        assert_eq!(resp.next_cursor, Some(5));
90        assert_eq!(resp.channels.len(), 2);
91
92        // Live channel
93        let ch = &resp.channels[0];
94        assert!(ch.is_live);
95        assert_eq!(ch.channel_slug, Some("knut".into()));
96        assert_eq!(ch.viewer_count, 151);
97        assert_eq!(ch.category_name, Some("IRL".into()));
98        assert_eq!(ch.user_username, Some("Knut".into()));
99        assert!(ch.session_title.is_some());
100        assert!(ch.profile_picture.is_some());
101
102        // Offline channel
103        let ch2 = &resp.channels[1];
104        assert!(!ch2.is_live);
105        assert_eq!(ch2.channel_slug, Some("anxstasia".into()));
106        assert_eq!(ch2.viewer_count, 0);
107        assert!(ch2.session_title.is_none());
108    }
109
110    #[test]
111    fn test_deserialize_empty_followed_response() {
112        let json = r##"{"nextCursor": null, "channels": []}"##;
113        let resp: FollowedChannelsResponse = serde_json::from_str(json).unwrap();
114        assert!(resp.next_cursor.is_none());
115        assert!(resp.channels.is_empty());
116    }
117
118    #[test]
119    fn test_deserialize_minimal_followed_channel() {
120        let json = r##"{"channels": [{"is_live": false}]}"##;
121        let resp: FollowedChannelsResponse = serde_json::from_str(json).unwrap();
122        assert_eq!(resp.channels.len(), 1);
123        assert!(!resp.channels[0].is_live);
124        assert!(resp.channels[0].channel_slug.is_none());
125        assert_eq!(resp.channels[0].viewer_count, 0);
126    }
127}