Commit4e5cebfaRecorded25 Mar 2026Repositorysigil-youtube
Add sigil-oauth integration for OAuth token management
Message
Add (youtube oauth) module providing: - youtube-oauth-config: Google preset with YouTube scopes - youtube-oauth-client: build youtube-client from oauth-tokens - youtube-ensure-client: auto-refresh with token persistence - Token storage wrappers (youtube-save/load/clear-tokens) - Re-exports of core sigil-oauth symbols for convenience
Add sigil-oauth, sigil-crypto, sigil-log as dependencies. Fix youtube-test import: (sigil http request) → (sigil http). Update README with OAuth usage examples.
Changed
README.md | 31 ++++++++++++++++++++++++++++++-
dev-redirects.sgl | 5 ++++-
package.sgl | 6 +++++-
src/youtube/oauth.sgl | 149 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
test/oauth-test.sgl | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
test/youtube-test.sgl | 2 +-
6 files changed, 297 insertions(+), 4 deletions(-)Diff
README.mdmodified
@@ -25,6 +25,7 @@ analytics, livestreaming, and playlist APIs.
25
| `(youtube analytics)` | YouTube Analytics API v2 queries | 26
| `(youtube live)` | Live broadcast and stream management | 27
| `(youtube playlist)` | Playlist and playlist item CRUD |+28
| `(youtube oauth)` | OAuth 2.0 integration via sigil-oauth (token management, auto-refresh) | 29
30
## Dependencies 31
@@ -32,8 +33,12 @@ analytics, livestreaming, and playlist APIs.
33
- `sigil-json` — JSON encode/decode 34
- `sigil-http` — HTTP client 35
- `sigil-tls` — HTTPS support+36
- `sigil-crypto` — cryptographic primitives (for sigil-oauth)+37
- `sigil-log` — structured logging+38
- `sigil-oauth` — shared OAuth 2.0 client library 39
−36
All dependencies are from the [sigil](https://codeberg.org/sigil/sigil) mono-repo.+40
Core dependencies are from the [sigil](https://codeberg.org/sigil/sigil) mono-repo.+41
OAuth support is from [sigil-oauth](https://codeberg.org/sigil/sigil-oauth). 42
43
## Building 44
@@ -59,6 +64,30 @@ sigil test --redirects dev-redirects.sgl
64
(define client (youtube-client api-key: "AIza...")) 65
``` 66
+67
### OAuth 2.0 token management+68
+69
```scheme+70
(import (youtube oauth))+71
+72
;; Create an OAuth config for Google/YouTube+73
(define config+74
(youtube-oauth-config "your-client-id" "your-client-secret"))+75
+76
;; Load stored tokens (from ~/.config/sigil/youtube/tokens.json)+77
(define tokens (youtube-load-tokens))+78
+79
;; Build a client with auto-refresh+80
(let-values (((client fresh-tokens)+81
(youtube-ensure-client config tokens (current-second))))+82
;; client is ready to use, tokens are refreshed if needed+83
(youtube-channel-mine client))+84
+85
;; Run the full authorization flow (opens browser, starts callback server)+86
(define tokens+87
(oauth-run-authorization-flow config (current-second)))+88
(youtube-save-tokens tokens)+89
```+90
91
### Get channel info 92
93
```schemedev-redirects.sglmodified
@@ -3,4 +3,7 @@
3
repos: (list 4
(for-repo 5
url: "codeberg:sigil/sigil"−6
use: (from-path dir: "../sigil"))))+6
use: (from-path dir: "../sigil"))+7
(for-repo+8
url: "codeberg:sigil/sigil-oauth"+9
use: (from-path dir: "../sigil-oauth"))))package.sglmodified
@@ -4,6 +4,7 @@
4
;;; resumable uploads, analytics queries, and livestream control. 5
6
(define sigil-repo "codeberg:sigil/sigil")+7
(define oauth-repo "codeberg:sigil/sigil-oauth") 8
9
(package 10
name: "sigil-youtube"@@ -29,7 +30,10 @@
30
(from-git url: sigil-repo package: "sigil-stdlib") 31
(from-git url: sigil-repo package: "sigil-tls") 32
(from-git url: sigil-repo package: "sigil-http")−32
(from-git url: sigil-repo package: "sigil-json"))+33
(from-git url: sigil-repo package: "sigil-json")+34
(from-git url: sigil-repo package: "sigil-crypto")+35
(from-git url: sigil-repo package: "sigil-log")+36
(from-git url: oauth-repo package: "sigil-oauth")) 37
38
tasks: (list 39
(tasksrc/youtube/oauth.sgladded
@@ -0,0 +1,149 @@
+1
;;; (youtube oauth) - OAuth 2.0 integration for sigil-youtube.+2
;;;+3
;;; Provides convenience functions for creating YouTube clients+4
;;; with OAuth token management via sigil-oauth. Uses the Google+5
;;; provider preset with standard YouTube scopes.+6
;;;+7
;;; This module adds OAuth support alongside the existing manual+8
;;; token-passing API — callers can still construct a youtube-client+9
;;; directly with an access-token string.+10
+11
(define-library (youtube oauth)+12
(import (sigil core)+13
(sigil dict)+14
(sigil struct)+15
(sigil oauth)+16
(sigil oauth store)+17
(youtube))+18
+19
(export ;; Config helpers+20
youtube-oauth-config+21
+22
;; Client construction from tokens+23
youtube-oauth-client+24
+25
;; Auto-refresh: config + tokens + time → client + tokens+26
youtube-ensure-client+27
+28
;; Token storage helpers+29
youtube-save-tokens+30
youtube-load-tokens+31
youtube-clear-tokens+32
+33
;; Re-exported from (sigil oauth) for convenience+34
oauth-config+35
oauth-config?+36
oauth-config-client-id+37
oauth-config-client-secret+38
oauth-config-auth-url+39
oauth-config-token-url+40
oauth-config-scopes+41
oauth-config-redirect-uri+42
oauth-config-extra-params+43
oauth-tokens+44
oauth-tokens?+45
oauth-tokens-access-token+46
oauth-tokens-refresh-token+47
oauth-tokens-expires-at+48
oauth-tokens-token-type+49
oauth-tokens-scopes+50
oauth-ensure-valid-token+51
oauth-token-expired?+52
oauth-authorization-url+53
oauth-generate-state+54
oauth-generate-pkce+55
oauth-exchange-code+56
oauth-refresh-token)+57
+58
(begin+59
+60
;; Default YouTube/Google scopes for System Crafters use.+61
;; Covers video management, uploads, analytics, and live streaming.+62
(define youtube-default-scopes+63
(list "https://www.googleapis.com/auth/youtube"+64
"https://www.googleapis.com/auth/yt-analytics.readonly"+65
"https://www.googleapis.com/auth/yt-analytics-monetary.readonly"))+66
+67
(define youtube-service-name "youtube")+68
+69
;; ---------------------------------------------------------------+70
;; Config+71
;; ---------------------------------------------------------------+72
+73
;;; Create an oauth-config for YouTube/Google OAuth2.+74
;;;+75
;;; client-id: Google Cloud OAuth2 client ID+76
;;; client-secret: Google Cloud OAuth2 client secret+77
;;;+78
;;; Optional keyword args via opts dict:+79
;;; scopes: list of scope strings (default: youtube-default-scopes)+80
;;; redirect-uri: override redirect URI+81
(define (youtube-oauth-config client-id client-secret . rest)+82
(let ((opts (if (null? rest) #{} (car rest))))+83
(let ((scopes (dict-ref opts scopes: youtube-default-scopes)))+84
(oauth-google-config client-id client-secret scopes+85
(if (dict-ref opts redirect-uri: #f)+86
#{ redirect-uri: (dict-ref opts redirect-uri:) }+87
#{})))))+88
+89
;; ---------------------------------------------------------------+90
;; Client construction+91
;; ---------------------------------------------------------------+92
+93
;;; Build a youtube-client from an oauth-tokens record.+94
;;;+95
;;; The access token is extracted from the tokens record.+96
;;; An optional api-key can be provided for fallback read-only+97
;;; access when the OAuth token is not available.+98
(define (youtube-oauth-client tokens . rest)+99
(let ((opts (if (null? rest) #{} (car rest))))+100
(youtube-client+101
access-token: (oauth-tokens-access-token tokens)+102
api-key: (dict-ref opts api-key: #f))))+103
+104
;; ---------------------------------------------------------------+105
;; Auto-refresh+106
;; ---------------------------------------------------------------+107
+108
;;; Ensure tokens are valid (refreshing if needed) and return+109
;;; both a ready-to-use youtube-client and the (possibly updated)+110
;;; oauth-tokens record.+111
;;;+112
;;; config: oauth-config from youtube-oauth-config+113
;;; tokens: current oauth-tokens record+114
;;; current-time: unix timestamp (from current-second)+115
;;;+116
;;; Optional keyword args via opts dict:+117
;;; api-key: YouTube API key for read-only fallback+118
;;; save?: whether to persist refreshed tokens (default: #t)+119
;;;+120
;;; Returns: (values youtube-client oauth-tokens)+121
(define (youtube-ensure-client config tokens current-time . rest)+122
(let* ((opts (if (null? rest) #{} (car rest)))+123
(save? (dict-ref opts save?: #t))+124
(fresh-tokens (oauth-ensure-valid-token config tokens current-time))+125
(client (youtube-oauth-client fresh-tokens opts)))+126
;; Persist if tokens were refreshed+127
(if (and save?+128
(not (eq? fresh-tokens tokens)))+129
(youtube-save-tokens fresh-tokens))+130
(values client fresh-tokens)))+131
+132
;; ---------------------------------------------------------------+133
;; Token storage (thin wrappers)+134
;; ---------------------------------------------------------------+135
+136
;;; Save YouTube OAuth tokens to persistent storage.+137
(define (youtube-save-tokens tokens)+138
(oauth-save-tokens youtube-service-name tokens))+139
+140
;;; Load YouTube OAuth tokens from persistent storage.+141
;;; Returns oauth-tokens or #f if not found.+142
(define (youtube-load-tokens)+143
(oauth-load-tokens youtube-service-name))+144
+145
;;; Clear stored YouTube OAuth tokens.+146
(define (youtube-clear-tokens)+147
(oauth-clear-tokens youtube-service-name))+148
+149
))test/oauth-test.sgladded
@@ -0,0 +1,108 @@
+1
;;; Tests for (youtube oauth) — OAuth integration module+2
;;;+3
;;; Tests config creation, client construction from tokens,+4
;;; and the ensure-client auto-refresh flow.+5
+6
(import (sigil core)+7
(sigil dict)+8
(sigil struct)+9
(sigil test)+10
(youtube)+11
(youtube oauth))+12
+13
;; ---------------------------------------------------------------+14
;; Config tests+15
;; ---------------------------------------------------------------+16
+17
(test-group "youtube oauth config"+18
+19
(test "creates google oauth config with defaults"+20
(let ((config (youtube-oauth-config "my-client-id" "my-secret")))+21
(assert-true (oauth-config? config))+22
(assert-equal "my-client-id" (oauth-config-client-id config))+23
(assert-equal "my-secret" (oauth-config-client-secret config))+24
(assert-equal "https://accounts.google.com/o/oauth2/v2/auth"+25
(oauth-config-auth-url config))+26
(assert-equal "https://oauth2.googleapis.com/token"+27
(oauth-config-token-url config))))+28
+29
(test "default scopes include youtube and analytics"+30
(let* ((config (youtube-oauth-config "id" "secret"))+31
(scopes (oauth-config-scopes config)))+32
(assert-true (member "https://www.googleapis.com/auth/youtube" scopes))+33
(assert-true (member "https://www.googleapis.com/auth/yt-analytics.readonly"+34
scopes))))+35
+36
(test "custom scopes override defaults"+37
(let* ((config (youtube-oauth-config "id" "secret"+38
#{ scopes: (list "https://www.googleapis.com/auth/youtube.readonly") }))+39
(scopes (oauth-config-scopes config)))+40
(assert-equal 1 (length scopes))+41
(assert-equal "https://www.googleapis.com/auth/youtube.readonly"+42
(car scopes))))+43
+44
(test "custom redirect uri"+45
(let ((config (youtube-oauth-config "id" "secret"+46
#{ redirect-uri: "http://localhost:9999/cb" })))+47
(assert-equal "http://localhost:9999/cb"+48
(oauth-config-redirect-uri config))))+49
+50
(test "google extra params include access_type offline"+51
(let ((config (youtube-oauth-config "id" "secret")))+52
(assert-equal "offline"+53
(dict-ref (oauth-config-extra-params config) access_type:)))))+54
+55
;; ---------------------------------------------------------------+56
;; Client construction tests+57
;; ---------------------------------------------------------------+58
+59
(test-group "youtube oauth client"+60
+61
(test "build client from tokens"+62
(let* ((tokens (oauth-tokens access-token: "ya29.test-token"+63
refresh-token: "1//refresh"+64
expires-at: 9999999999))+65
(client (youtube-oauth-client tokens)))+66
(assert-true (youtube-client? client))+67
(assert-equal "ya29.test-token" (youtube-client-access-token client))+68
(assert-equal #f (youtube-client-api-key client))))+69
+70
(test "build client from tokens with api-key"+71
(let* ((tokens (oauth-tokens access-token: "ya29.tok"))+72
(client (youtube-oauth-client tokens #{ api-key: "AIza.key" })))+73
(assert-equal "ya29.tok" (youtube-client-access-token client))+74
(assert-equal "AIza.key" (youtube-client-api-key client)))))+75
+76
;; ---------------------------------------------------------------+77
;; Ensure-client tests+78
;; ---------------------------------------------------------------+79
+80
(test-group "youtube ensure client"+81
+82
(test "returns client when token is not expired"+83
(let* ((config (youtube-oauth-config "id" "secret"))+84
(tokens (oauth-tokens access-token: "ya29.valid"+85
refresh-token: "1//refresh"+86
expires-at: 9999999999))+87
(current-time 1000))+88
(let-values (((client new-tokens)+89
(youtube-ensure-client config tokens current-time+90
#{ save?: #f })))+91
(assert-true (youtube-client? client))+92
(assert-equal "ya29.valid" (youtube-client-access-token client))+93
;; Tokens unchanged when not expired+94
(assert-equal "ya29.valid"+95
(oauth-tokens-access-token new-tokens)))))+96
+97
(test "returns client when no expiry info"+98
(let* ((config (youtube-oauth-config "id" "secret"))+99
(tokens (oauth-tokens access-token: "ya29.noexpiry"))+100
(current-time 1000))+101
(let-values (((client new-tokens)+102
(youtube-ensure-client config tokens current-time+103
#{ save?: #f })))+104
(assert-true (youtube-client? client))+105
(assert-equal "ya29.noexpiry"+106
(youtube-client-access-token client))))))+107
+108
(run-tests)test/youtube-test.sglmodified
@@ -9,7 +9,7 @@
9
(sigil struct) 10
(sigil json) 11
(sigil test)−12
(only (sigil http request) url-encode-value build-query-string)+12
(only (sigil http) url-encode-value build-query-string) 13
(youtube)) 14
15
;; ---------------------------------------------------------------