AtlatestRepositorytube
1
;;; (tube youtube) - YouTube-specific operations wrapping sigil-youtube.2
;;;3
;;; Upload with metadata, video listing with stats, analytics dashboard,4
;;; livestream management, and playlist management.5
;;;6
;;; Supports two authentication modes:7
;;; 1. Direct access token via YOUTUBE_ACCESS_TOKEN env var8
;;; 2. OAuth via YOUTUBE_CLIENT_ID/SECRET with auto-managed token storage10
(define-library (tube youtube)11
(import (sigil core)12
(sigil string)13
(sigil dict)14
(sigil struct)15
(sigil json)16
(sigil time)17
(youtube)18
(youtube upload)19
(youtube analytics)20
(youtube live)21
(youtube playlist)22
(youtube oauth)23
(tube config))24
(export require-youtube-client25
tube-upload-video26
tube-list-videos27
tube-video-analytics28
tube-youtube-status29
tube-list-broadcasts30
tube-create-broadcast31
tube-go-live-youtube32
tube-list-playlists33
format-video34
format-analytics-row35
format-broadcast36
format-playlist37
;; Re-exported from (youtube) for CLI use38
youtube-video-id39
youtube-video-title)40
(begin42
;;; Build a youtube-client from config.43
;;;44
;;; Priority:45
;;; 1. If YOUTUBE_ACCESS_TOKEN is set, use it directly (legacy mode)46
;;; 2. If YOUTUBE_CLIENT_ID/SECRET are set, use OAuth with stored tokens47
;;; (auto-refreshes expired tokens)48
;;; 3. If YOUTUBE_API_KEY is set, use it for read-only access49
;;; 4. Otherwise, raise an error50
(define (require-youtube-client config)51
(let ((token (tube-config-youtube-access-token config))52
(key (tube-config-youtube-api-key config))53
(client-id (tube-config-youtube-client-id config))54
(client-secret (tube-config-youtube-client-secret config)))55
(cond56
;; Direct token — use as-is57
(token58
(youtube-client access-token: token59
api-key: key))60
;; OAuth client credentials — load/refresh stored tokens61
((and client-id client-secret)62
(let* ((oauth-cfg (youtube-oauth-config client-id client-secret))63
(tokens (youtube-load-tokens)))64
(if (not tokens)65
(error (string-append66
"No stored YouTube tokens found. "67
"Run 'tube auth youtube' to authorize, "68
"or set YOUTUBE_ACCESS_TOKEN directly.")))69
(let-values (((client fresh-tokens)70
(youtube-ensure-client oauth-cfg tokens71
(current-second))))72
client)))73
;; API key only — read-only74
(key75
(youtube-client api-key: key))76
(else77
(error (string-append78
"No YouTube credentials set. "79
"Set YOUTUBE_CLIENT_ID and YOUTUBE_CLIENT_SECRET for OAuth, "80
"or YOUTUBE_ACCESS_TOKEN for direct token mode."))))))82
;; ============================================================83
;; Upload84
;; ============================================================86
;;; Upload a video to YouTube with metadata.87
;;; Returns the video resource dict on success.88
(define (tube-upload-video config file-data title89
. rest)90
(let* ((client (require-youtube-client config))91
(description (if (and (pair? rest) (car rest)) (car rest) ""))92
(privacy (if (and (pair? rest) (pair? (cdr rest)) (cadr rest))93
(cadr rest) "private"))94
(metadata #{ snippet: #{ title: title95
description: description96
categoryId: "28" }97
status: #{ privacyStatus: privacy } }))98
(youtube-upload-video client metadata file-data)))100
;; ============================================================101
;; Video Listing102
;; ============================================================104
;;; Format a youtube-video record as a display string.105
(define (format-video video)106
(let ((stats (youtube-video-statistics video)))107
(string-append108
(youtube-video-id video) " "109
(youtube-video-title video)110
(if stats111
(string-append112
"\n Views: " (dict-ref stats viewCount: "0")113
" Likes: " (dict-ref stats likeCount: "0")114
" Comments: " (dict-ref stats commentCount: "0"))115
""))))117
;;; List recent videos from the authenticated channel.118
;;; Returns a list of youtube-video records.119
(define (tube-list-videos config . rest)120
(let* ((client (require-youtube-client config))121
(limit (if (pair? rest) (car rest) 10))122
(channel (youtube-channel-mine client))123
(uploads-id (youtube-channel-uploads-playlist-id channel))124
(items (youtube-playlist-items client uploads-id125
max-results: limit))126
(video-ids (map youtube-playlist-item-video-id items)))127
(if (null? video-ids)128
'()129
(youtube-videos client video-ids))))131
;; ============================================================132
;; Analytics133
;; ============================================================135
;;; Format an analytics row as a display string.136
(define (format-analytics-row row)137
(string-join (map (lambda (v)138
(if (string? v) v (number->string v)))139
row)140
"\t"))142
;;; Get a YouTube analytics summary for the given date range.143
;;; Returns a dict with column-headers and rows.144
(define (tube-video-analytics config start-date end-date)145
(let ((client (require-youtube-client config)))146
(youtube-analytics-query client start-date end-date147
"views,estimatedMinutesWatched,averageViewDuration,subscribersGained"148
dimensions: "day"149
sort: "-day")))151
;; ============================================================152
;; YouTube Status153
;; ============================================================155
;;; Get YouTube channel status overview.156
;;; Returns a formatted string with subscriber count, video count,157
;;; and recent video info.158
(define (tube-youtube-status config)159
(let* ((client (require-youtube-client config))160
(channel (youtube-channel-mine client)))161
(string-append162
"YouTube Channel: " (youtube-channel-title channel) "\n"163
"Subscribers: " (number->string (youtube-channel-subscriber-count channel)) "\n"164
"Total Videos: " (number->string (youtube-channel-video-count channel)))))166
;; ============================================================167
;; Livestream168
;; ============================================================170
;;; Format a youtube-broadcast record.171
(define (format-broadcast broadcast)172
(string-append173
(youtube-broadcast-id broadcast) " "174
(youtube-broadcast-title broadcast) " "175
"[" (youtube-broadcast-lifecycle-status broadcast) "]"176
(let ((start (youtube-broadcast-scheduled-start broadcast)))177
(if start (string-append " Scheduled: " start) ""))))179
;;; List upcoming/active broadcasts.180
(define (tube-list-broadcasts config . rest)181
(let* ((client (require-youtube-client config))182
(status (if (pair? rest) (car rest) "upcoming")))183
(youtube-list-broadcasts client broadcast-status: status)))185
;;; Create a new broadcast and stream, bind them.186
;;; Returns the broadcast record.187
(define (tube-create-broadcast config title scheduled-start)188
(let* ((client (require-youtube-client config))189
(broadcast (youtube-create-broadcast client title scheduled-start190
enable-auto-start: #t191
enable-auto-stop: #t))192
(stream (youtube-create-stream client193
(string-append title " stream"))))194
(youtube-bind-broadcast client195
(youtube-broadcast-id broadcast)196
(youtube-stream-id stream))197
broadcast))199
;;; Transition a broadcast to live.200
(define (tube-go-live-youtube config broadcast-id)201
(let ((client (require-youtube-client config)))202
(youtube-transition-broadcast client broadcast-id "live")))204
;; ============================================================205
;; Playlists206
;; ============================================================208
;;; Format a youtube-playlist record.209
(define (format-playlist playlist)210
(string-append211
(youtube-playlist-id playlist) " "212
(youtube-playlist-title playlist)213
" (" (number->string (youtube-playlist-item-count playlist)) " items)"214
" [" (youtube-playlist-privacy-status playlist) "]"))216
;;; List playlists for the authenticated channel.217
(define (tube-list-playlists config)218
(let ((client (require-youtube-client config)))219
(youtube-playlists client)))221
))