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.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 ;; Records18
twitch-clip19
twitch-clip?20
twitch-clip-id21
twitch-clip-url22
twitch-clip-creator-name23
twitch-clip-video-id24
twitch-clip-game-id25
twitch-clip-title26
twitch-clip-view-count27
twitch-clip-duration29
twitch-video30
twitch-video?31
twitch-video-id32
twitch-video-user-id33
twitch-video-title34
twitch-video-type35
twitch-video-duration36
twitch-video-view-count37
twitch-video-created-at39
;; Parsing40
parse-clip41
parse-video43
;; API functions44
twitch-followers45
twitch-subscribers46
twitch-clips47
twitch-create-clip48
twitch-videos49
twitch-extension-analytics50
twitch-game-analytics)52
(begin54
;; ---------------------------------------------------------------55
;; Records56
;; ---------------------------------------------------------------58
(define-struct twitch-clip59
(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-video69
(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
;; Parsing79
;; ---------------------------------------------------------------81
(define (parse-clip data)82
(twitch-clip83
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-video94
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 functions104
;; ---------------------------------------------------------------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 cursor113
;;; user-id: check if specific user follows114
(define (twitch-followers client broadcaster-id . rest)115
(let ((opts (if (null? rest) #{} (car rest))))116
(let* ((params (list117
(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-append127
(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 cursor144
;;; user-id: check if specific user is subscribed145
(define (twitch-subscribers client broadcaster-id . rest)146
(let ((opts (if (null? rest) #{} (car rest))))147
(let* ((params (list148
(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-append158
(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 broadcaster174
;;; game-id: filter by game175
;;; id: specific clip ID or list of IDs176
;;; started-at: ISO 8601 start date177
;;; ended-at: ISO 8601 end date178
;;; first: number (1-100, default: 20)179
;;; after: pagination cursor180
(define (twitch-clips client . rest)181
(let ((opts (if (null? rest) #{} (car rest))))182
(let* ((params (list183
(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-append199
(twitch-api-url client "clips") query-str))200
(url (let ((clip-id (dict-ref opts id: #f)))201
(if clip-id202
(string-append base-url203
(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-append216
(twitch-api-url client "clips")217
(build-query-string218
(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 user231
;;; game-id: filter by game232
;;; id: specific video ID or list of IDs233
;;; 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 cursor238
(define (twitch-videos client . rest)239
(let ((opts (if (null? rest) #{} (car rest))))240
(let* ((params (list241
(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-append259
(twitch-api-url client "videos") query-str))260
(url (let ((vid-id (dict-ref opts id: #f)))261
(if vid-id262
(string-append base-url263
(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 path273
;;; and the ID parameter key.274
(define (twitch-analytics client endpoint id-key opts)275
(let* ((params (list276
(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-key287
(dict-ref opts id: #f))288
(cons "type"289
(dict-ref opts type: #f))))290
(url (string-append291
(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
))