AtlatestRepositorysigil-twitch

sigil-twitch / tree / src / twitchanalytics.sgl

1;;; (twitch analytics) - Twitch analytics, followers, subscribers,
2;;; clips, and videos.
3;;;
4;;; Provides access to follower/subscriber data, clip management,
5;;; VOD/highlight retrieval, and extension/game analytics.
6
7(define-library (twitch analytics)
8 (import (sigil core)
9 (sigil dict)
10 (sigil string)
11 (sigil struct)
12 (sigil json)
13 (only (sigil http) build-query-string)
14 (sigil http client)
15 (twitch))
17 (export ;; Records
18 twitch-clip
19 twitch-clip?
20 twitch-clip-id
21 twitch-clip-url
22 twitch-clip-creator-name
23 twitch-clip-video-id
24 twitch-clip-game-id
25 twitch-clip-title
26 twitch-clip-view-count
27 twitch-clip-duration
29 twitch-video
30 twitch-video?
31 twitch-video-id
32 twitch-video-user-id
33 twitch-video-title
34 twitch-video-type
35 twitch-video-duration
36 twitch-video-view-count
37 twitch-video-created-at
39 ;; Parsing
40 parse-clip
41 parse-video
43 ;; API functions
44 twitch-followers
45 twitch-subscribers
46 twitch-clips
47 twitch-create-clip
48 twitch-videos
49 twitch-extension-analytics
50 twitch-game-analytics)
52 (begin
54 ;; ---------------------------------------------------------------
55 ;; Records
56 ;; ---------------------------------------------------------------
58 (define-struct twitch-clip
59 (id)
60 (url default: "")
61 (creator-name default: "")
62 (video-id default: "")
63 (game-id default: "")
64 (title default: "")
65 (view-count default: 0)
66 (duration default: 0))
68 (define-struct twitch-video
69 (id)
70 (user-id default: "")
71 (title default: "")
72 (type default: "")
73 (duration default: "")
74 (view-count default: 0)
75 (created-at default: #f))
77 ;; ---------------------------------------------------------------
78 ;; Parsing
79 ;; ---------------------------------------------------------------
81 (define (parse-clip data)
82 (twitch-clip
83 id: (dict-ref data id:)
84 url: (dict-ref data url: "")
85 creator-name: (dict-ref data creator_name: "")
86 video-id: (dict-ref data video_id: "")
87 game-id: (dict-ref data game_id: "")
88 title: (dict-ref data title: "")
89 view-count: (dict-ref data view_count: 0)
90 duration: (dict-ref data duration: 0)))
92 (define (parse-video data)
93 (twitch-video
94 id: (dict-ref data id:)
95 user-id: (dict-ref data user_id: "")
96 title: (dict-ref data title: "")
97 type: (dict-ref data type: "")
98 duration: (dict-ref data duration: "")
99 view-count: (dict-ref data view_count: 0)
100 created-at: (dict-ref data created_at: #f)))
102 ;; ---------------------------------------------------------------
103 ;; API functions
104 ;; ---------------------------------------------------------------
106 ;;; Get followers for a channel.
107 ;;; Requires moderator:read:followers scope for individual records.
108 ;;; Always returns total count.
109 ;;;
110 ;;; Optional opts dict:
111 ;;; first: number (1-100, default: 20)
112 ;;; after: pagination cursor
113 ;;; user-id: check if specific user follows
114 (define (twitch-followers client broadcaster-id . rest)
115 (let ((opts (if (null? rest) #{} (car rest))))
116 (let* ((params (list
117 (cons "broadcaster_id" broadcaster-id)
118 (cons "first"
119 (if (dict-ref opts first: #f)
120 (number->string (dict-ref opts first:))
121 #f))
122 (cons "after"
123 (dict-ref opts after: #f))
124 (cons "user_id"
125 (dict-ref opts user-id: #f))))
126 (url (string-append
127 (twitch-api-url client "channels" "followers")
128 (build-query-string params)))
129 (data (twitch-get/json client url)))
130 #{ total: (dict-ref data total: 0)
131 data: (map (lambda (item)
132 #{ user-id: (dict-ref item user_id: "")
133 user-name: (dict-ref item user_name: "")
134 followed-at: (dict-ref item followed_at: #f) })
135 (array->list (dict-ref data data: #[])))
136 cursor: (parse-pagination data) })))
138 ;;; Get subscribers for a channel.
139 ;;; Requires channel:read:subscriptions scope.
140 ;;;
141 ;;; Optional opts dict:
142 ;;; first: number (1-100, default: 20)
143 ;;; after: pagination cursor
144 ;;; user-id: check if specific user is subscribed
145 (define (twitch-subscribers client broadcaster-id . rest)
146 (let ((opts (if (null? rest) #{} (car rest))))
147 (let* ((params (list
148 (cons "broadcaster_id" broadcaster-id)
149 (cons "first"
150 (if (dict-ref opts first: #f)
151 (number->string (dict-ref opts first:))
152 #f))
153 (cons "after"
154 (dict-ref opts after: #f))
155 (cons "user_id"
156 (dict-ref opts user-id: #f))))
157 (url (string-append
158 (twitch-api-url client "subscriptions")
159 (build-query-string params)))
160 (data (twitch-get/json client url)))
161 #{ total: (dict-ref data total: 0)
162 data: (map (lambda (item)
163 #{ user-id: (dict-ref item user_id: "")
164 user-name: (dict-ref item user_name: "")
165 tier: (dict-ref item tier: "")
166 is-gift: (dict-ref item is_gift: #f) })
167 (array->list (dict-ref data data: #[])))
168 cursor: (parse-pagination data) })))
170 ;;; Get clips for a broadcaster, game, or specific clip IDs.
171 ;;;
172 ;;; Optional opts dict:
173 ;;; broadcaster-id: filter by broadcaster
174 ;;; game-id: filter by game
175 ;;; id: specific clip ID or list of IDs
176 ;;; started-at: ISO 8601 start date
177 ;;; ended-at: ISO 8601 end date
178 ;;; first: number (1-100, default: 20)
179 ;;; after: pagination cursor
180 (define (twitch-clips client . rest)
181 (let ((opts (if (null? rest) #{} (car rest))))
182 (let* ((params (list
183 (cons "broadcaster_id"
184 (dict-ref opts broadcaster-id: #f))
185 (cons "game_id"
186 (dict-ref opts game-id: #f))
187 (cons "started_at"
188 (dict-ref opts started-at: #f))
189 (cons "ended_at"
190 (dict-ref opts ended-at: #f))
191 (cons "first"
192 (if (dict-ref opts first: #f)
193 (number->string (dict-ref opts first:))
194 #f))
195 (cons "after"
196 (dict-ref opts after: #f))))
197 (query-str (build-query-string params))
198 (base-url (string-append
199 (twitch-api-url client "clips") query-str))
200 (url (let ((clip-id (dict-ref opts id: #f)))
201 (if clip-id
202 (string-append base-url
203 (if (string=? query-str "") "?" "&")
204 (build-repeated-params "id"
205 (ensure-list clip-id)))
206 base-url)))
207 (data (twitch-get/json client url))
208 (items (dict-ref data data: #[])))
209 (map parse-clip (array->list items)))))
211 ;;; Create a clip from a live stream or recent VOD.
212 ;;; Requires clips:edit scope.
213 ;;; Returns a dict with id: and edit-url:.
214 (define (twitch-create-clip client broadcaster-id)
215 (let* ((url (string-append
216 (twitch-api-url client "clips")
217 (build-query-string
218 (list (cons "broadcaster_id" broadcaster-id)))))
219 (data (twitch-post/json client url #{}))
220 (items (dict-ref data data: #[])))
221 (if (> (array-length items) 0)
222 (let ((item (array-ref items 0)))
223 #{ id: (dict-ref item id:)
224 edit-url: (dict-ref item edit_url: "") })
225 #f)))
227 ;;; Get videos (VODs, highlights, uploads) for a user or game.
228 ;;;
229 ;;; Optional opts dict:
230 ;;; user-id: filter by user
231 ;;; game-id: filter by game
232 ;;; id: specific video ID or list of IDs
233 ;;; type: "all", "archive", "highlight", "upload" (default: "all")
234 ;;; sort: "time", "trending", "views" (default: "time")
235 ;;; period: "all", "day", "week", "month" (default: "all")
236 ;;; first: number (1-100, default: 20)
237 ;;; after: pagination cursor
238 (define (twitch-videos client . rest)
239 (let ((opts (if (null? rest) #{} (car rest))))
240 (let* ((params (list
241 (cons "user_id"
242 (dict-ref opts user-id: #f))
243 (cons "game_id"
244 (dict-ref opts game-id: #f))
245 (cons "type"
246 (dict-ref opts type: #f))
247 (cons "sort"
248 (dict-ref opts sort: #f))
249 (cons "period"
250 (dict-ref opts period: #f))
251 (cons "first"
252 (if (dict-ref opts first: #f)
253 (number->string (dict-ref opts first:))
254 #f))
255 (cons "after"
256 (dict-ref opts after: #f))))
257 (query-str (build-query-string params))
258 (base-url (string-append
259 (twitch-api-url client "videos") query-str))
260 (url (let ((vid-id (dict-ref opts id: #f)))
261 (if vid-id
262 (string-append base-url
263 (if (string=? query-str "") "?" "&")
264 (build-repeated-params "id"
265 (ensure-list vid-id)))
266 base-url)))
267 (data (twitch-get/json client url))
268 (items (dict-ref data data: #[])))
269 (map parse-video (array->list items)))))
271 ;;; Internal helper for analytics endpoints (extensions/games).
272 ;;; Both share the same structure, differing only in endpoint path
273 ;;; and the ID parameter key.
274 (define (twitch-analytics client endpoint id-key opts)
275 (let* ((params (list
276 (cons "started_at"
277 (dict-ref opts started-at: #f))
278 (cons "ended_at"
279 (dict-ref opts ended-at: #f))
280 (cons "first"
281 (if (dict-ref opts first: #f)
282 (number->string (dict-ref opts first:))
283 #f))
284 (cons "after"
285 (dict-ref opts after: #f))
286 (cons id-key
287 (dict-ref opts id: #f))
288 (cons "type"
289 (dict-ref opts type: #f))))
290 (url (string-append
291 (twitch-api-url client "analytics" endpoint)
292 (build-query-string params))))
293 (twitch-get/json client url)))
295 ;;; Get extension analytics.
296 ;;; Requires analytics:read:extensions scope.
297 ;;; Returns raw response with download URLs for CSV reports.
298 (define (twitch-extension-analytics client . rest)
299 (twitch-analytics client "extensions" "extension_id"
300 (if (null? rest) #{} (car rest))))
302 ;;; Get game analytics.
303 ;;; Requires analytics:read:games scope.
304 ;;; Returns raw response with download URLs for CSV reports.
305 (define (twitch-game-analytics client . rest)
306 (twitch-analytics client "games" "game_id"
307 (if (null? rest) #{} (car rest))))
309 ))