# GleeTube
[](https://hex.pm/packages/gleetube)
[](https://hexdocs.pm/gleetube/)
Type-safe Gleam client for the YouTube Data API v3. Invalid API calls fail at compile time, not runtime.
## Features
- **Type-safe parts and filters** -- each resource has its own Part and Filter types, enforced by the compiler
- **Result-based error handling** -- all API calls return `Result(Response, GleeTubeError)`, no panics
- **Pipe-first API** -- `client |> videos.list(parts, filter)` reads naturally
- **Automatic pagination** -- `list_all` fetches every page with tail-recursive accumulation
- **Pluggable HTTP transport** -- built-in `httpc` adapter, optional `hackney` adapter with proxy support
- **Full API coverage** -- 20 YouTube API resources with list, insert, update, delete, and special operations
## Installation
```sh
gleam add gleetube@1
```
## Quick Start
```gleam
import gleam/io
import gleam/option.{None}
import gleetube
import gleetube/resource/channels
pub fn main() {
let client = gleetube.new("YOUR_API_KEY")
let assert Ok(resp) =
client
|> channels.list(
parts: [channels.Snippet, channels.Statistics],
filter: channels.ById(["UC_x5XG1OV2P6uZZ5FSM9Ttw"]),
max_results: None,
page_token: None,
hl: None,
)
io.debug(resp.items)
}
```
## Type Safety
### Parts are typed per resource
Each resource defines its own Part type. Passing a `ChannelPart` to a video function is a compile error:
```gleam
import gleetube/resource/videos
// Compiles -- Snippet and Statistics are valid VideoPart variants
videos.list(client,
parts: [videos.Snippet, videos.Statistics],
filter: videos.ById(["dQw4w9WgXcQ"]),
// ...
)
```
### Filters are union types
A filter is required for list operations. The type system ensures exactly one valid filter is provided:
```gleam
import gleetube/resource/channels
// By ID
channels.list(client,
parts: [channels.Snippet],
filter: channels.ById(["UC_x5XG1OV2P6uZZ5FSM9Ttw"]),
// ...
)
// By YouTube handle
channels.list(client,
parts: [channels.Snippet],
filter: channels.ByHandle("@GoogleDevelopers"),
// ...
)
// Authenticated user's own channel (requires OAuth2)
channels.list(client,
parts: [channels.Snippet],
filter: channels.Mine,
// ...
)
```
## Usage
### Videos
```gleam
import gleam/option.{None, Some}
import gleetube/resource/videos
// List by IDs
let assert Ok(resp) =
client
|> videos.list(
parts: [videos.Snippet, videos.Statistics, videos.ContentDetails],
filter: videos.ById(["dQw4w9WgXcQ"]),
hl: None, max_height: None, max_results: None, max_width: None,
on_behalf_of_content_owner: None, page_token: None,
region_code: None, video_category_id: None,
)
// Most popular by region
let assert Ok(resp) =
client
|> videos.list(
parts: [videos.Snippet, videos.Statistics],
filter: videos.ByChart(videos.MostPopular),
hl: None, max_height: None, max_results: Some(10), max_width: None,
on_behalf_of_content_owner: None, page_token: None,
region_code: Some("US"), video_category_id: None,
)
// Rate a video (requires OAuth2)
let assert Ok(Nil) =
client |> videos.rate(video_id: "dQw4w9WgXcQ", rating: videos.Like)
```
### Search
```gleam
import gleam/option.{None, Some}
import gleetube/resource/search
let assert Ok(resp) =
client
|> search.list(
filter: search.NoFilter,
q: Some("gleam programming"),
max_results: Some(10),
order: Some(search.Relevance),
safe_search: Some(search.Moderate),
type_: Some("video"),
// remaining optional params as None ...
channel_id: None, channel_type: None, event_type: None,
location: None, location_radius: None,
on_behalf_of_content_owner: None, page_token: None,
published_after: None, published_before: None,
region_code: None, relevance_language: None,
topic_id: None, video_caption: None, video_category_id: None,
video_definition: None, video_dimension: None,
video_duration: None, video_embeddable: None,
video_license: None, video_paid_product_placement: None,
video_syndicated: None, video_type: None,
)
```
### Playlists
```gleam
import gleam/option.{None, Some}
import gleetube/resource/playlists
let assert Ok(resp) =
client
|> playlists.list(
parts: [playlists.Snippet, playlists.ContentDetails],
filter: playlists.ByChannelId("UC_x5XG1OV2P6uZZ5FSM9Ttw"),
hl: None, max_results: Some(25),
on_behalf_of_content_owner: None,
on_behalf_of_content_owner_channel: None,
page_token: None,
)
```
### Pagination
Every `list` function supports manual pagination via `page_token`. Each resource also provides `list_all` to fetch all pages automatically:
```gleam
import gleam/option.{None}
import gleetube/resource/channels
let assert Ok(all_channels) =
client
|> channels.list_all(
parts: [channels.Snippet],
filter: channels.ByHandle("@GoogleDevelopers"),
hl: None,
)
```
Limit the total number of items with `pagination.list_up_to`:
```gleam
import gleetube/pagination
let assert Ok(first_100) =
pagination.list_up_to(fetch_page, max_count: 100)
```
### Convenience API
The `gleetube/api` module provides high-level wrappers with sensible defaults:
```gleam
import gleetube/api
let assert Ok(resp) = api.get_channel_info(client, ["UC_x5XG1OV2P6uZZ5FSM9Ttw"])
let assert Ok(resp) = api.get_video_by_id(client, ["dQw4w9WgXcQ"])
let assert Ok(resp) = api.search_by_keywords(client, "gleam lang", 10)
let assert Ok(resp) = api.get_playlist_items(client, "PLRqwX-V7Uu6ZiZxtDDRCi6uhfTH4FilpH")
let assert Ok(resp) = api.get_comment_threads(client, "dQw4w9WgXcQ")
let assert Ok(resp) = api.get_i18n_languages(client)
let assert Ok(resp) = api.get_video_categories(client, "US")
```
## OAuth2
```gleam
import gleam/option
import gleam/result
import gleetube
import gleetube/oauth2
let oauth_config = oauth2.new(
client_id: "YOUR_CLIENT_ID",
client_secret: "YOUR_CLIENT_SECRET",
redirect_uri: "http://localhost:8080/callback",
)
// Generate authorization URL -- redirect the user here
let auth_url = oauth2.authorize_url(
oauth_config,
access_type: option.Some("offline"),
state: option.None,
login_hint: option.None,
prompt: option.Some(oauth2.Consent),
)
// After the user authorizes, exchange the code for a client
use client <- result.try(
gleetube.new_with_oauth(oauth_config, code: "AUTH_CODE")
)
// Refresh an expired token
use new_token <- result.try(
oauth2.refresh_token(oauth_config, refresh_token: "REFRESH_TOKEN")
)
// Revoke a token
let assert Ok(Nil) = oauth2.revoke_token(token: "TOKEN")
```
## Configuration
### Custom timeout
```gleam
import gleetube
import gleetube/auth
import gleetube/config
let client =
auth.api_key("YOUR_KEY")
|> config.new()
|> config.with_timeout(10_000)
|> gleetube.new_with_config()
```
### Proxy (hackney adapter)
```gleam
import gleetube
import gleetube/adapter/hackney_adapter
import gleetube/auth
import gleetube/config
let opts =
hackney_adapter.new()
|> hackney_adapter.with_proxy("http://proxy:8080")
|> hackney_adapter.with_proxy_auth("user", "pass")
let client =
auth.api_key("YOUR_KEY")
|> config.new()
|> config.with_transport(
hackney_adapter.transport(opts),
hackney_adapter.transport_bits(opts),
)
|> gleetube.new_with_config()
```
## Error Handling
All API calls return `Result(Response, GleeTubeError)`. Pattern match on the error variants:
```gleam
import gleam/io
import gleetube/error.{ApiError, AuthError, DecodeError, HttpError}
case videos.list(client, ...) {
Ok(resp) -> io.debug(resp.items)
Error(ApiError(status: 403, message: msg, ..)) ->
io.println("Forbidden: " <> msg)
Error(HttpError(message: msg)) ->
io.println("Network error: " <> msg)
Error(AuthError(message: msg)) ->
io.println("Auth failed: " <> msg)
Error(DecodeError(message: msg)) ->
io.println("Decode error: " <> msg)
Error(_) -> io.println("Other error")
}
```
## Supported Resources
| Resource | list | insert | update | delete | Other |
|---|:---:|:---:|:---:|:---:|---|
| Activities | o | | | | |
| Captions | o | o | o | o | download |
| Channel Banners | | | | | upload |
| Channel Sections | o | o | o | o | |
| Channels | o | | o | | |
| Comment Threads | o | | o | | |
| Comments | o | o | o | o | markAsSpam, setModerationStatus |
| I18n Languages | o | | | | |
| I18n Regions | o | | | | |
| Members | o | | | | |
| Memberships Levels | o | | | | |
| Playlist Items | o | o | o | o | |
| Playlists | o | o | o | o | |
| Search | o | | | | |
| Subscriptions | o | o | | o | |
| Thumbnails | | | | | set |
| Video Abuse Report Reasons | o | | | | |
| Video Categories | o | | | | |
| Videos | o | o | o | o | rate, getRating, reportAbuse |
| Watermarks | | | | | set, unset |
## Development
```sh
gleam build # compile
gleam test # run all tests
gleam format # format code
gleam docs build # generate docs
```
## License
[BlueOak-1.0.0](LICENCE)