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
```scheme
dev-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
(task
src/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
;; ---------------------------------------------------------------