tangled
alpha
login
or
join now
anil.recoil.org
/
ocaml-peertube
0
fork
atom
OCaml bindings to the Peertube ActivityPub video sharing API
0
fork
atom
overview
issues
pulls
pipelines
init
anil.recoil.org
1 month ago
a53d7775
+2739
10 changed files
expand all
collapse all
unified
split
.gitignore
PLAN.md
TODO.md
bin
dune
opeertube.ml
dune-project
lib
dune
peertube.ml
peertube.mli
peertube.opam
+2
.gitignore
···
1
1
+
_build
2
2
+
peertube-src
+310
PLAN.md
···
1
1
+
# PeerTube OCaml API Coverage Plan
2
2
+
3
3
+
## Current Implementation
4
4
+
5
5
+
**Implemented (5 endpoints):**
6
6
+
- `GET /api/v1/video-channels/{channel}/videos` - List channel videos with pagination
7
7
+
- `GET /api/v1/videos/{uuid}` - Get video details
8
8
+
- Thumbnail download (static files)
9
9
+
10
10
+
**Types:**
11
11
+
- `video` - Basic video metadata
12
12
+
- `video_response` - Paginated response wrapper
13
13
+
14
14
+
## Proposed Expansion
15
15
+
16
16
+
### Phase 1: Enhanced Video Discovery (Priority: High)
17
17
+
18
18
+
Add types and functions for browsing and searching videos.
19
19
+
20
20
+
#### New Types
21
21
+
```ocaml
22
22
+
type sort_order =
23
23
+
| Newest | Oldest | Views | Likes | Trending | Hot | Random
24
24
+
25
25
+
type video_filter = {
26
26
+
category_id : int option;
27
27
+
licence_id : int option;
28
28
+
language : string option;
29
29
+
nsfw : bool option;
30
30
+
is_local : bool option;
31
31
+
has_hls : bool option;
32
32
+
has_webtorrent : bool option;
33
33
+
skip_count : bool option; (* skip total count for performance *)
34
34
+
}
35
35
+
36
36
+
type search_params = {
37
37
+
search : string;
38
38
+
start : int;
39
39
+
count : int;
40
40
+
sort : sort_order;
41
41
+
search_target : [`Local | `Everywhere] option;
42
42
+
duration_min : int option;
43
43
+
duration_max : int option;
44
44
+
published_after : Ptime.t option;
45
45
+
published_before : Ptime.t option;
46
46
+
}
47
47
+
```
48
48
+
49
49
+
#### New Endpoints
50
50
+
| Function | Endpoint | Description |
51
51
+
|----------|----------|-------------|
52
52
+
| `list_videos` | `GET /api/v1/videos` | Browse all videos with filters |
53
53
+
| `search_videos` | `GET /api/v1/search/videos` | Full-text search |
54
54
+
| `search_channels` | `GET /api/v1/search/video-channels` | Search channels |
55
55
+
| `get_categories` | `GET /api/v1/videos/categories` | Video category list |
56
56
+
| `get_languages` | `GET /api/v1/videos/languages` | Available languages |
57
57
+
| `get_licences` | `GET /api/v1/videos/licences` | License types |
58
58
+
59
59
+
### Phase 2: Channels & Accounts (Priority: High)
60
60
+
61
61
+
#### New Types
62
62
+
```ocaml
63
63
+
type channel = {
64
64
+
id : int;
65
65
+
name : string;
66
66
+
display_name : string;
67
67
+
description : string option;
68
68
+
url : string;
69
69
+
host : string;
70
70
+
followers_count : int;
71
71
+
following_count : int;
72
72
+
created_at : Ptime.t;
73
73
+
banner_path : string option;
74
74
+
avatar_path : string option;
75
75
+
owner_account : account_summary option;
76
76
+
}
77
77
+
78
78
+
type account_summary = {
79
79
+
id : int;
80
80
+
name : string;
81
81
+
display_name : string;
82
82
+
url : string;
83
83
+
host : string;
84
84
+
}
85
85
+
86
86
+
type account = {
87
87
+
id : int;
88
88
+
name : string;
89
89
+
display_name : string;
90
90
+
description : string option;
91
91
+
url : string;
92
92
+
host : string;
93
93
+
followers_count : int;
94
94
+
following_count : int;
95
95
+
created_at : Ptime.t;
96
96
+
avatar_path : string option;
97
97
+
}
98
98
+
```
99
99
+
100
100
+
#### New Endpoints
101
101
+
| Function | Endpoint | Description |
102
102
+
|----------|----------|-------------|
103
103
+
| `list_channels` | `GET /api/v1/video-channels` | Browse all channels |
104
104
+
| `get_channel` | `GET /api/v1/video-channels/{handle}` | Channel details |
105
105
+
| `list_accounts` | `GET /api/v1/accounts` | Browse accounts |
106
106
+
| `get_account` | `GET /api/v1/accounts/{handle}` | Account details |
107
107
+
| `get_account_videos` | `GET /api/v1/accounts/{handle}/videos` | Videos by account |
108
108
+
| `get_account_channels` | `GET /api/v1/accounts/{handle}/video-channels` | Account's channels |
109
109
+
110
110
+
### Phase 3: Playlists (Priority: Medium)
111
111
+
112
112
+
#### New Types
113
113
+
```ocaml
114
114
+
type playlist_privacy = Public | Unlisted | Private
115
115
+
116
116
+
type playlist = {
117
117
+
id : int;
118
118
+
uuid : string;
119
119
+
display_name : string;
120
120
+
description : string option;
121
121
+
privacy : playlist_privacy;
122
122
+
url : string;
123
123
+
videos_length : int;
124
124
+
thumbnail_path : string option;
125
125
+
created_at : Ptime.t;
126
126
+
updated_at : Ptime.t;
127
127
+
owner_account : account_summary;
128
128
+
video_channel : channel option;
129
129
+
}
130
130
+
131
131
+
type playlist_element = {
132
132
+
id : int;
133
133
+
position : int;
134
134
+
start_timestamp : int option;
135
135
+
stop_timestamp : int option;
136
136
+
video : video;
137
137
+
}
138
138
+
```
139
139
+
140
140
+
#### New Endpoints
141
141
+
| Function | Endpoint | Description |
142
142
+
|----------|----------|-------------|
143
143
+
| `list_playlists` | `GET /api/v1/video-playlists` | Browse playlists |
144
144
+
| `get_playlist` | `GET /api/v1/video-playlists/{id}` | Playlist details |
145
145
+
| `get_playlist_videos` | `GET /api/v1/video-playlists/{id}/videos` | Videos in playlist |
146
146
+
| `get_account_playlists` | `GET /api/v1/accounts/{handle}/video-playlists` | Account's playlists |
147
147
+
148
148
+
### Phase 4: Server Information (Priority: Medium)
149
149
+
150
150
+
#### New Types
151
151
+
```ocaml
152
152
+
type server_config = {
153
153
+
instance_name : string;
154
154
+
instance_short_description : string;
155
155
+
instance_description : string;
156
156
+
instance_terms : string;
157
157
+
instance_default_nsfw_policy : string;
158
158
+
signup_allowed : bool;
159
159
+
signup_allowed_for_current_ip : bool;
160
160
+
signup_requires_email_verification : bool;
161
161
+
transcoding_enabled : bool;
162
162
+
contact_form_enabled : bool;
163
163
+
}
164
164
+
165
165
+
type server_stats = {
166
166
+
total_users : int;
167
167
+
total_daily_active_users : int;
168
168
+
total_weekly_active_users : int;
169
169
+
total_monthly_active_users : int;
170
170
+
total_local_videos : int;
171
171
+
total_local_video_views : int;
172
172
+
total_local_video_comments : int;
173
173
+
total_local_video_files_size : int64;
174
174
+
total_videos : int;
175
175
+
total_video_comments : int;
176
176
+
total_local_video_channels : int;
177
177
+
total_local_playlists : int;
178
178
+
total_instance_followers : int;
179
179
+
total_instance_following : int;
180
180
+
}
181
181
+
```
182
182
+
183
183
+
#### New Endpoints
184
184
+
| Function | Endpoint | Description |
185
185
+
|----------|----------|-------------|
186
186
+
| `get_config` | `GET /api/v1/config` | Server configuration |
187
187
+
| `get_about` | `GET /api/v1/config/about` | Instance about page |
188
188
+
| `get_stats` | `GET /api/v1/server/stats` | Server statistics |
189
189
+
190
190
+
### Phase 5: Authentication (Priority: Low for read-only clients)
191
191
+
192
192
+
#### New Types
193
193
+
```ocaml
194
194
+
type oauth_client = {
195
195
+
client_id : string;
196
196
+
client_secret : string;
197
197
+
}
198
198
+
199
199
+
type auth_token = {
200
200
+
access_token : string;
201
201
+
token_type : string;
202
202
+
expires_in : int;
203
203
+
refresh_token : string;
204
204
+
}
205
205
+
206
206
+
type authenticated_session = {
207
207
+
session : Requests.t;
208
208
+
token : auth_token;
209
209
+
expires_at : Ptime.t;
210
210
+
}
211
211
+
```
212
212
+
213
213
+
#### New Functions
214
214
+
| Function | Endpoint | Description |
215
215
+
|----------|----------|-------------|
216
216
+
| `get_oauth_client` | `GET /api/v1/oauth-clients/local` | Get OAuth credentials |
217
217
+
| `login` | `POST /api/v1/users/token` | Authenticate user |
218
218
+
| `refresh_token` | `POST /api/v1/users/token` | Refresh access token |
219
219
+
| `logout` | `POST /api/v1/users/revoke-token` | Revoke token |
220
220
+
221
221
+
### Phase 6: User Interactions (Priority: Low, requires auth)
222
222
+
223
223
+
#### New Endpoints (require authentication)
224
224
+
| Function | Endpoint | Description |
225
225
+
|----------|----------|-------------|
226
226
+
| `rate_video` | `POST /api/v1/videos/{id}/rate` | Like/dislike video |
227
227
+
| `get_my_rating` | `GET /api/v1/videos/{id}/rating` | Get my rating |
228
228
+
| `get_comments` | `GET /api/v1/videos/{id}/comments` | Video comments |
229
229
+
| `post_comment` | `POST /api/v1/videos/{id}/comment-threads` | Add comment |
230
230
+
| `subscribe` | `POST /api/v1/users/me/subscriptions` | Subscribe to channel |
231
231
+
| `unsubscribe` | `DELETE /api/v1/users/me/subscriptions/{uri}` | Unsubscribe |
232
232
+
| `get_subscriptions` | `GET /api/v1/users/me/subscriptions` | My subscriptions |
233
233
+
| `get_subscription_videos` | `GET /api/v1/users/me/subscriptions/videos` | Subscription feed |
234
234
+
| `get_notifications` | `GET /api/v1/users/me/notifications` | My notifications |
235
235
+
| `get_watch_history` | `GET /api/v1/users/me/history/videos` | Watch history |
236
236
+
237
237
+
## Implementation Strategy
238
238
+
239
239
+
### Step 1: Extend Video Type
240
240
+
Add missing fields to the existing `video` type that are commonly returned:
241
241
+
- `duration` (int, seconds)
242
242
+
- `views` (int)
243
243
+
- `likes` (int)
244
244
+
- `dislikes` (int)
245
245
+
- `category` (category record)
246
246
+
- `licence` (licence record)
247
247
+
- `language` (language record)
248
248
+
- `privacy` (privacy record)
249
249
+
- `is_local` (bool)
250
250
+
- `channel` (channel_summary)
251
251
+
- `account` (account_summary)
252
252
+
253
253
+
### Step 2: Create Common Pagination Module
254
254
+
```ocaml
255
255
+
module Pagination : sig
256
256
+
type 'a response = {
257
257
+
total : int;
258
258
+
data : 'a list;
259
259
+
}
260
260
+
261
261
+
val fetch_all :
262
262
+
?page_size:int ->
263
263
+
?max_pages:int ->
264
264
+
(start:int -> count:int -> 'a response) ->
265
265
+
'a list
266
266
+
end
267
267
+
```
268
268
+
269
269
+
### Step 3: Create Module Structure
270
270
+
```
271
271
+
lib/
272
272
+
peertube.ml - Main entry, re-exports submodules
273
273
+
peertube.mli - Public interface
274
274
+
video.ml - Video types and operations
275
275
+
channel.ml - Channel types and operations
276
276
+
account.ml - Account types and operations
277
277
+
playlist.ml - Playlist types and operations
278
278
+
search.ml - Search functionality
279
279
+
server.ml - Server config/stats
280
280
+
auth.ml - Authentication (optional)
281
281
+
```
282
282
+
283
283
+
### Step 4: Add CLI Commands
284
284
+
Extend `opeertube` with new subcommands:
285
285
+
- `opeertube search <query>` - Search videos
286
286
+
- `opeertube channels` - List/search channels
287
287
+
- `opeertube channel <name>` - Channel details
288
288
+
- `opeertube playlists` - List playlists
289
289
+
- `opeertube playlist <id>` - Playlist videos
290
290
+
- `opeertube server info` - Server configuration
291
291
+
- `opeertube server stats` - Server statistics
292
292
+
293
293
+
## File Changes Summary
294
294
+
295
295
+
| File | Changes |
296
296
+
|------|---------|
297
297
+
| `lib/dune` | No changes needed |
298
298
+
| `lib/peertube.mli` | Add new types and functions |
299
299
+
| `lib/peertube.ml` | Add implementations |
300
300
+
| `bin/opeertube.ml` | Add new CLI commands |
301
301
+
| `dune-project` | No changes needed |
302
302
+
303
303
+
## Testing Plan
304
304
+
305
305
+
1. Unit tests for JSON codec round-trips
306
306
+
2. Integration tests against public PeerTube instances:
307
307
+
- https://framatube.org
308
308
+
- https://peertube.social
309
309
+
- https://video.ploud.fr
310
310
+
3. CLI smoke tests for each command
+201
TODO.md
···
1
1
+
# PeerTube OCaml Client - Remaining Features
2
2
+
3
3
+
## Completed
4
4
+
5
5
+
- [x] Phase 1: Video Discovery (list, search, categories, languages, licences)
6
6
+
- [x] Phase 2: Channels & Accounts (list, search, get details)
7
7
+
- [x] Phase 3: Playlists (list, search, get details, videos)
8
8
+
- [x] Phase 4: Server Information (config, stats)
9
9
+
- [x] CLI with all read-only operations
10
10
+
11
11
+
## Phase 5: Authentication
12
12
+
13
13
+
The Requests library will have OAuth support, which should simplify this phase.
14
14
+
15
15
+
### Types
16
16
+
17
17
+
```ocaml
18
18
+
type oauth_client = {
19
19
+
client_id : string;
20
20
+
client_secret : string;
21
21
+
}
22
22
+
23
23
+
type auth_token = {
24
24
+
access_token : string;
25
25
+
token_type : string;
26
26
+
expires_in : int;
27
27
+
refresh_token : string;
28
28
+
}
29
29
+
```
30
30
+
31
31
+
### Endpoints
32
32
+
33
33
+
- [ ] `get_oauth_client` - `GET /api/v1/oauth-clients/local`
34
34
+
- Fetches the instance's OAuth client credentials
35
35
+
- Required before user authentication
36
36
+
37
37
+
- [ ] `login` - `POST /api/v1/users/token`
38
38
+
- OAuth2 password grant flow
39
39
+
- Parameters: username, password, client_id, client_secret
40
40
+
- Returns access_token and refresh_token
41
41
+
- Use Requests OAuth support for token management
42
42
+
43
43
+
- [ ] `refresh_token` - `POST /api/v1/users/token`
44
44
+
- OAuth2 refresh token grant
45
45
+
- Automatically refresh expired tokens
46
46
+
- Requests OAuth support should handle this
47
47
+
48
48
+
- [ ] `logout` - `POST /api/v1/users/revoke-token`
49
49
+
- Revoke the current access token
50
50
+
51
51
+
### Implementation Notes
52
52
+
53
53
+
- Requests OAuth support will handle token storage and automatic refresh
54
54
+
- Consider storing tokens in `~/.config/opeertube/` for CLI persistence
55
55
+
- Add `--login` flag to CLI for interactive authentication
56
56
+
57
57
+
## Phase 6: User Interactions (requires auth)
58
58
+
59
59
+
### Video Interactions
60
60
+
61
61
+
- [ ] `rate_video` - `POST /api/v1/videos/{id}/rate`
62
62
+
- Body: `{ "rating": "like" | "dislike" | "none" }`
63
63
+
64
64
+
- [ ] `get_my_rating` - `GET /api/v1/videos/{id}/rating`
65
65
+
- Returns user's rating for a video
66
66
+
67
67
+
### Comments
68
68
+
69
69
+
- [ ] `get_comments` - `GET /api/v1/videos/{id}/comment-threads`
70
70
+
- Paginated list of top-level comments
71
71
+
- Each comment has nested replies
72
72
+
73
73
+
- [ ] `get_comment_thread` - `GET /api/v1/videos/{id}/comment-threads/{threadId}`
74
74
+
- Full thread with all replies
75
75
+
76
76
+
- [ ] `post_comment` - `POST /api/v1/videos/{id}/comment-threads`
77
77
+
- Body: `{ "text": "comment content" }`
78
78
+
79
79
+
- [ ] `reply_to_comment` - `POST /api/v1/videos/{id}/comments/{commentId}`
80
80
+
- Body: `{ "text": "reply content" }`
81
81
+
82
82
+
- [ ] `delete_comment` - `DELETE /api/v1/videos/{id}/comments/{commentId}`
83
83
+
84
84
+
### Subscriptions
85
85
+
86
86
+
- [ ] `subscribe` - `POST /api/v1/users/me/subscriptions`
87
87
+
- Body: `{ "uri": "channel@host" }`
88
88
+
89
89
+
- [ ] `unsubscribe` - `DELETE /api/v1/users/me/subscriptions/{subscriptionHandle}`
90
90
+
91
91
+
- [ ] `get_subscriptions` - `GET /api/v1/users/me/subscriptions`
92
92
+
- Paginated list of subscribed channels
93
93
+
94
94
+
- [ ] `get_subscription_videos` - `GET /api/v1/users/me/subscriptions/videos`
95
95
+
- Subscription feed (videos from subscribed channels)
96
96
+
97
97
+
- [ ] `subscription_exists` - `GET /api/v1/users/me/subscriptions/exist`
98
98
+
- Check if subscribed to specific channels
99
99
+
100
100
+
### User Profile
101
101
+
102
102
+
- [ ] `get_my_info` - `GET /api/v1/users/me`
103
103
+
- Current user's account info
104
104
+
105
105
+
- [ ] `update_my_info` - `PUT /api/v1/users/me`
106
106
+
- Update display name, description, etc.
107
107
+
108
108
+
- [ ] `get_my_videos` - `GET /api/v1/users/me/videos`
109
109
+
- Videos uploaded by current user
110
110
+
111
111
+
### Watch History & Notifications
112
112
+
113
113
+
- [ ] `get_watch_history` - `GET /api/v1/users/me/history/videos`
114
114
+
- Paginated watch history
115
115
+
116
116
+
- [ ] `delete_watch_history` - `POST /api/v1/users/me/history/videos/remove`
117
117
+
- Clear watch history
118
118
+
119
119
+
- [ ] `get_notifications` - `GET /api/v1/users/me/notifications`
120
120
+
- User notifications
121
121
+
122
122
+
- [ ] `mark_notifications_read` - `POST /api/v1/users/me/notifications/read`
123
123
+
- Mark notifications as read
124
124
+
125
125
+
- [ ] `mark_all_notifications_read` - `POST /api/v1/users/me/notifications/read-all`
126
126
+
127
127
+
## Phase 7: Content Upload (requires auth)
128
128
+
129
129
+
### Video Upload
130
130
+
131
131
+
- [ ] `upload_video` - `POST /api/v1/videos/upload`
132
132
+
- Multipart form upload
133
133
+
- Required fields: channelId, name, videofile
134
134
+
- Optional: description, tags, privacy, etc.
135
135
+
136
136
+
- [ ] `upload_video_resumable_init` - `POST /api/v1/videos/upload-resumable`
137
137
+
- Initialize resumable upload for large files
138
138
+
139
139
+
- [ ] `upload_video_resumable` - `PUT /api/v1/videos/upload-resumable`
140
140
+
- Continue resumable upload
141
141
+
142
142
+
- [ ] `update_video` - `PUT /api/v1/videos/{id}`
143
143
+
- Update video metadata
144
144
+
145
145
+
- [ ] `delete_video` - `DELETE /api/v1/videos/{id}`
146
146
+
147
147
+
### Playlist Management
148
148
+
149
149
+
- [ ] `create_playlist` - `POST /api/v1/video-playlists`
150
150
+
- Create a new playlist
151
151
+
152
152
+
- [ ] `update_playlist` - `PUT /api/v1/video-playlists/{playlistId}`
153
153
+
154
154
+
- [ ] `delete_playlist` - `DELETE /api/v1/video-playlists/{playlistId}`
155
155
+
156
156
+
- [ ] `add_video_to_playlist` - `POST /api/v1/video-playlists/{playlistId}/videos`
157
157
+
- Body: `{ "videoId": id }`
158
158
+
159
159
+
- [ ] `remove_video_from_playlist` - `DELETE /api/v1/video-playlists/{playlistId}/videos/{playlistElementId}`
160
160
+
161
161
+
- [ ] `reorder_playlist` - `POST /api/v1/video-playlists/{playlistId}/videos/reorder`
162
162
+
163
163
+
## CLI Enhancements
164
164
+
165
165
+
### Authentication Commands
166
166
+
167
167
+
- [ ] `opeertube login` - Interactive login, store token
168
168
+
- [ ] `opeertube logout` - Revoke and delete stored token
169
169
+
- [ ] `opeertube whoami` - Show current user info
170
170
+
171
171
+
### Authenticated Operations
172
172
+
173
173
+
- [ ] `opeertube like VIDEO_UUID` - Like a video
174
174
+
- [ ] `opeertube dislike VIDEO_UUID` - Dislike a video
175
175
+
- [ ] `opeertube subscribe CHANNEL` - Subscribe to channel
176
176
+
- [ ] `opeertube unsubscribe CHANNEL` - Unsubscribe
177
177
+
- [ ] `opeertube subscriptions` - List subscriptions
178
178
+
- [ ] `opeertube feed` - Show subscription feed
179
179
+
- [ ] `opeertube history` - Show watch history
180
180
+
- [ ] `opeertube notifications` - Show notifications
181
181
+
182
182
+
### Upload Commands
183
183
+
184
184
+
- [ ] `opeertube upload FILE --channel CHANNEL --name NAME [options]`
185
185
+
- [ ] `opeertube update VIDEO_UUID [options]`
186
186
+
- [ ] `opeertube delete VIDEO_UUID`
187
187
+
188
188
+
## Code Quality
189
189
+
190
190
+
- [ ] Add unit tests for JSON codec round-trips
191
191
+
- [ ] Add integration tests against public instances
192
192
+
- [ ] Add CI configuration (GitHub Actions)
193
193
+
- [ ] Generate opam file for release
194
194
+
- [ ] Add rate limiting / retry logic for API calls
195
195
+
- [ ] Consider caching for frequently accessed data
196
196
+
197
197
+
## Documentation
198
198
+
199
199
+
- [ ] Add examples to mli documentation
200
200
+
- [ ] Create tutorial for common use cases
201
201
+
- [ ] Document error handling patterns
+5
bin/dune
···
1
1
+
(executable
2
2
+
(name opeertube)
3
3
+
(public_name opeertube)
4
4
+
(package peertube)
5
5
+
(libraries peertube eio_main cmdliner logs logs.cli logs.fmt fmt.tty fmt.cli))
+570
bin/opeertube.ml
···
1
1
+
(** opeertube - PeerTube API command-line client *)
2
2
+
3
3
+
open Cmdliner
4
4
+
5
5
+
let setup_log style_renderer level =
6
6
+
Fmt_tty.setup_std_outputs ?style_renderer ();
7
7
+
Logs.set_level level;
8
8
+
Logs.set_reporter (Logs_fmt.reporter ())
9
9
+
10
10
+
let setup_log_term =
11
11
+
Term.(const setup_log $ Fmt_cli.style_renderer () $ Logs_cli.level ())
12
12
+
13
13
+
let base_url_arg =
14
14
+
let doc = "Base URL of the PeerTube instance (e.g., https://video.example.com)" in
15
15
+
Arg.(required & opt (some string) None & info [ "u"; "url" ] ~docv:"URL" ~doc)
16
16
+
17
17
+
let channel_arg =
18
18
+
let doc = "Channel name" in
19
19
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"CHANNEL" ~doc)
20
20
+
21
21
+
let handle_arg =
22
22
+
let doc = "Account or channel handle" in
23
23
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"HANDLE" ~doc)
24
24
+
25
25
+
let id_arg =
26
26
+
let doc = "Playlist ID or UUID" in
27
27
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"ID" ~doc)
28
28
+
29
29
+
let uuid_arg =
30
30
+
let doc = "Video UUID" in
31
31
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"UUID" ~doc)
32
32
+
33
33
+
let query_arg =
34
34
+
let doc = "Search query" in
35
35
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"QUERY" ~doc)
36
36
+
37
37
+
let count_arg =
38
38
+
let doc = "Number of results per page" in
39
39
+
Arg.(value & opt int 20 & info [ "c"; "count" ] ~docv:"N" ~doc)
40
40
+
41
41
+
let start_arg =
42
42
+
let doc = "Starting offset for pagination" in
43
43
+
Arg.(value & opt int 0 & info [ "s"; "start" ] ~docv:"N" ~doc)
44
44
+
45
45
+
let max_pages_arg =
46
46
+
let doc = "Maximum number of pages to fetch (unlimited if not specified)" in
47
47
+
Arg.(value & opt (some int) None & info [ "m"; "max-pages" ] ~docv:"N" ~doc)
48
48
+
49
49
+
let all_flag =
50
50
+
let doc = "Fetch all results using automatic pagination" in
51
51
+
Arg.(value & flag & info [ "a"; "all" ] ~doc)
52
52
+
53
53
+
let json_flag =
54
54
+
let doc = "Output as JSON" in
55
55
+
Arg.(value & flag & info [ "j"; "json" ] ~doc)
56
56
+
57
57
+
let output_arg =
58
58
+
let doc = "Output file path for thumbnail" in
59
59
+
Arg.(required & pos 1 (some string) None & info [] ~docv:"OUTPUT" ~doc)
60
60
+
61
61
+
let handle_api_error f =
62
62
+
try f ()
63
63
+
with Peertube.Api_error (status, msg) ->
64
64
+
Fmt.epr "Error: %s (HTTP %d)@." msg status;
65
65
+
exit 1
66
66
+
67
67
+
(* Video printing *)
68
68
+
69
69
+
let print_video ?(json = false) (v : Peertube.video) =
70
70
+
if json then
71
71
+
let json_str = Jsont_bytesrw.encode_string Peertube.video_jsont v in
72
72
+
match json_str with
73
73
+
| Ok s -> print_endline s
74
74
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
75
75
+
else
76
76
+
Fmt.pr "@[<v>%s@, UUID: %s@, ID: %d@, URL: %s@, Duration: %ds@, Views: %d@, Published: %a@, Tags: [%a]@]@."
77
77
+
v.name v.uuid v.id v.url v.duration v.views
78
78
+
(Ptime.pp_rfc3339 ()) v.published_at
79
79
+
Fmt.(list ~sep:(any ", ") string) v.tags
80
80
+
81
81
+
let print_videos ?(json = false) videos =
82
82
+
if json then begin
83
83
+
let codec = Jsont.(list Peertube.video_jsont) in
84
84
+
let json_str = Jsont_bytesrw.encode_string codec videos in
85
85
+
match json_str with
86
86
+
| Ok s -> print_endline s
87
87
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
88
88
+
end
89
89
+
else
90
90
+
List.iter (print_video ~json:false) videos
91
91
+
92
92
+
(* Channel printing *)
93
93
+
94
94
+
let print_channel_summary (c : Peertube.channel_summary) =
95
95
+
Fmt.pr "@[<v>%s (@%s)@, ID: %d@, URL: %s@]@."
96
96
+
c.channel_display_name c.channel_name c.channel_id c.channel_url
97
97
+
98
98
+
let print_channel ?(json = false) (c : Peertube.channel) =
99
99
+
if json then
100
100
+
let json_str = Jsont_bytesrw.encode_string Peertube.channel_jsont c in
101
101
+
match json_str with
102
102
+
| Ok s -> print_endline s
103
103
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
104
104
+
else begin
105
105
+
Fmt.pr "@[<v>%s (@%s)@, ID: %d@, URL: %s@, Followers: %d@, Created: %a@]@."
106
106
+
c.channel.channel_display_name c.channel.channel_name
107
107
+
c.channel.channel_id c.channel.channel_url
108
108
+
c.channel_followers_count
109
109
+
(Ptime.pp_rfc3339 ()) c.channel_created_at;
110
110
+
Option.iter (fun d -> Fmt.pr " Description: %s@." d) c.channel_description
111
111
+
end
112
112
+
113
113
+
let print_channels ?(json = false) channels =
114
114
+
if json then begin
115
115
+
let codec = Jsont.(list Peertube.channel_jsont) in
116
116
+
let json_str = Jsont_bytesrw.encode_string codec channels in
117
117
+
match json_str with
118
118
+
| Ok s -> print_endline s
119
119
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
120
120
+
end
121
121
+
else
122
122
+
List.iter (fun (c : Peertube.channel) -> print_channel_summary c.channel) channels
123
123
+
124
124
+
(* Account printing *)
125
125
+
126
126
+
let print_account_summary (a : Peertube.account_summary) =
127
127
+
Fmt.pr "@[<v>%s (@%s)@, ID: %d@, URL: %s@]@."
128
128
+
a.account_display_name a.account_name a.account_id a.account_url
129
129
+
130
130
+
let print_account ?(json = false) (a : Peertube.account) =
131
131
+
if json then
132
132
+
let json_str = Jsont_bytesrw.encode_string Peertube.account_jsont a in
133
133
+
match json_str with
134
134
+
| Ok s -> print_endline s
135
135
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
136
136
+
else begin
137
137
+
Fmt.pr "@[<v>%s (@%s)@, ID: %d@, URL: %s@, Followers: %d@, Created: %a@]@."
138
138
+
a.account.account_display_name a.account.account_name
139
139
+
a.account.account_id a.account.account_url
140
140
+
a.account_followers_count
141
141
+
(Ptime.pp_rfc3339 ()) a.account_created_at;
142
142
+
Option.iter (fun d -> Fmt.pr " Description: %s@." d) a.account_description
143
143
+
end
144
144
+
145
145
+
let print_accounts ?(json = false) accounts =
146
146
+
if json then begin
147
147
+
let codec = Jsont.(list Peertube.account_jsont) in
148
148
+
let json_str = Jsont_bytesrw.encode_string codec accounts in
149
149
+
match json_str with
150
150
+
| Ok s -> print_endline s
151
151
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
152
152
+
end
153
153
+
else
154
154
+
List.iter (fun (a : Peertube.account) -> print_account_summary a.account) accounts
155
155
+
156
156
+
(* Playlist printing *)
157
157
+
158
158
+
let print_playlist ?(json = false) (p : Peertube.playlist) =
159
159
+
if json then
160
160
+
let json_str = Jsont_bytesrw.encode_string Peertube.playlist_jsont p in
161
161
+
match json_str with
162
162
+
| Ok s -> print_endline s
163
163
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
164
164
+
else
165
165
+
Fmt.pr "@[<v>%s@, UUID: %s@, ID: %d@, Videos: %d@, URL: %s@, Created: %a@]@."
166
166
+
p.playlist_display_name p.playlist_uuid p.playlist_id
167
167
+
p.playlist_videos_length p.playlist_url
168
168
+
(Ptime.pp_rfc3339 ()) p.playlist_created_at
169
169
+
170
170
+
let print_playlists ?(json = false) playlists =
171
171
+
if json then begin
172
172
+
let codec = Jsont.(list Peertube.playlist_jsont) in
173
173
+
let json_str = Jsont_bytesrw.encode_string codec playlists in
174
174
+
match json_str with
175
175
+
| Ok s -> print_endline s
176
176
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
177
177
+
end
178
178
+
else
179
179
+
List.iter (print_playlist ~json:false) playlists
180
180
+
181
181
+
(* Video commands *)
182
182
+
183
183
+
let list_channel_videos () base_url channel count start all max_pages json =
184
184
+
handle_api_error @@ fun () ->
185
185
+
Eio_main.run @@ fun env ->
186
186
+
Eio.Switch.run @@ fun sw ->
187
187
+
let session = Requests.create ~sw env in
188
188
+
if all then begin
189
189
+
let videos =
190
190
+
Peertube.fetch_all_channel_videos ?max_pages ~page_size:count ~session
191
191
+
~base_url ~channel ()
192
192
+
in
193
193
+
Fmt.pr "Found %d videos@.@." (List.length videos);
194
194
+
print_videos ~json videos
195
195
+
end
196
196
+
else begin
197
197
+
let response =
198
198
+
Peertube.fetch_channel_videos ~count ~start ~session ~base_url ~channel ()
199
199
+
in
200
200
+
Fmt.pr "Showing %d of %d videos (offset %d)@.@."
201
201
+
(List.length response.data) response.total start;
202
202
+
print_videos ~json response.data
203
203
+
end
204
204
+
205
205
+
let list_channel_videos_cmd =
206
206
+
let doc = "List videos from a PeerTube channel" in
207
207
+
let info = Cmd.info "channel-videos" ~doc in
208
208
+
Cmd.v info
209
209
+
Term.(
210
210
+
const list_channel_videos $ setup_log_term $ base_url_arg $ channel_arg
211
211
+
$ count_arg $ start_arg $ all_flag $ max_pages_arg $ json_flag)
212
212
+
213
213
+
let browse_videos () base_url count start json =
214
214
+
handle_api_error @@ fun () ->
215
215
+
Eio_main.run @@ fun env ->
216
216
+
Eio.Switch.run @@ fun sw ->
217
217
+
let session = Requests.create ~sw env in
218
218
+
let response =
219
219
+
Peertube.list_videos ~count ~start ~session ~base_url ()
220
220
+
in
221
221
+
Fmt.pr "Showing %d of %d videos (offset %d)@.@."
222
222
+
(List.length response.data) response.total start;
223
223
+
print_videos ~json response.data
224
224
+
225
225
+
let browse_videos_cmd =
226
226
+
let doc = "Browse all videos on the instance" in
227
227
+
let info = Cmd.info "browse" ~doc in
228
228
+
Cmd.v info
229
229
+
Term.(
230
230
+
const browse_videos $ setup_log_term $ base_url_arg
231
231
+
$ count_arg $ start_arg $ json_flag)
232
232
+
233
233
+
let search_videos () base_url query count start json =
234
234
+
handle_api_error @@ fun () ->
235
235
+
Eio_main.run @@ fun env ->
236
236
+
Eio.Switch.run @@ fun sw ->
237
237
+
let session = Requests.create ~sw env in
238
238
+
let response =
239
239
+
Peertube.search_videos ~query ~count ~start ~session ~base_url ()
240
240
+
in
241
241
+
Fmt.pr "Found %d results for '%s' (showing %d, offset %d)@.@."
242
242
+
response.total query (List.length response.data) start;
243
243
+
print_videos ~json response.data
244
244
+
245
245
+
let search_videos_cmd =
246
246
+
let doc = "Search for videos" in
247
247
+
let info = Cmd.info "search" ~doc in
248
248
+
Cmd.v info
249
249
+
Term.(
250
250
+
const search_videos $ setup_log_term $ base_url_arg $ query_arg
251
251
+
$ count_arg $ start_arg $ json_flag)
252
252
+
253
253
+
let get_video () base_url uuid json =
254
254
+
handle_api_error @@ fun () ->
255
255
+
Eio_main.run @@ fun env ->
256
256
+
Eio.Switch.run @@ fun sw ->
257
257
+
let session = Requests.create ~sw env in
258
258
+
let video = Peertube.fetch_video_details ~session ~base_url ~uuid () in
259
259
+
print_video ~json video
260
260
+
261
261
+
let get_video_cmd =
262
262
+
let doc = "Get details for a specific video by UUID" in
263
263
+
let info = Cmd.info "get" ~doc in
264
264
+
Cmd.v info
265
265
+
Term.(const get_video $ setup_log_term $ base_url_arg $ uuid_arg $ json_flag)
266
266
+
267
267
+
let download_thumbnail () base_url uuid output =
268
268
+
handle_api_error @@ fun () ->
269
269
+
Eio_main.run @@ fun env ->
270
270
+
Eio.Switch.run @@ fun sw ->
271
271
+
let session = Requests.create ~sw env in
272
272
+
let video = Peertube.fetch_video_details ~session ~base_url ~uuid () in
273
273
+
match Peertube.download_thumbnail ~session ~base_url ~video ~output_path:output () with
274
274
+
| Ok () -> Fmt.pr "Thumbnail saved to %s@." output
275
275
+
| Error (`Msg msg) -> Fmt.epr "Error: %s@." msg; exit 1
276
276
+
277
277
+
let download_thumbnail_cmd =
278
278
+
let doc = "Download a video's thumbnail" in
279
279
+
let info = Cmd.info "thumbnail" ~doc in
280
280
+
Cmd.v info
281
281
+
Term.(
282
282
+
const download_thumbnail $ setup_log_term $ base_url_arg $ uuid_arg
283
283
+
$ output_arg)
284
284
+
285
285
+
(* Channel commands *)
286
286
+
287
287
+
let list_channels () base_url count start json =
288
288
+
handle_api_error @@ fun () ->
289
289
+
Eio_main.run @@ fun env ->
290
290
+
Eio.Switch.run @@ fun sw ->
291
291
+
let session = Requests.create ~sw env in
292
292
+
let response =
293
293
+
Peertube.list_channels ~count ~start ~session ~base_url ()
294
294
+
in
295
295
+
Fmt.pr "Showing %d of %d channels (offset %d)@.@."
296
296
+
(List.length response.data) response.total start;
297
297
+
print_channels ~json response.data
298
298
+
299
299
+
let list_channels_cmd =
300
300
+
let doc = "List channels on the instance" in
301
301
+
let info = Cmd.info "list" ~doc in
302
302
+
Cmd.v info
303
303
+
Term.(
304
304
+
const list_channels $ setup_log_term $ base_url_arg
305
305
+
$ count_arg $ start_arg $ json_flag)
306
306
+
307
307
+
let search_channels () base_url query count start json =
308
308
+
handle_api_error @@ fun () ->
309
309
+
Eio_main.run @@ fun env ->
310
310
+
Eio.Switch.run @@ fun sw ->
311
311
+
let session = Requests.create ~sw env in
312
312
+
let response =
313
313
+
Peertube.search_channels ~query ~count ~start ~session ~base_url ()
314
314
+
in
315
315
+
Fmt.pr "Found %d channels for '%s' (showing %d, offset %d)@.@."
316
316
+
response.total query (List.length response.data) start;
317
317
+
print_channels ~json response.data
318
318
+
319
319
+
let search_channels_cmd =
320
320
+
let doc = "Search for channels" in
321
321
+
let info = Cmd.info "search" ~doc in
322
322
+
Cmd.v info
323
323
+
Term.(
324
324
+
const search_channels $ setup_log_term $ base_url_arg $ query_arg
325
325
+
$ count_arg $ start_arg $ json_flag)
326
326
+
327
327
+
let get_channel () base_url handle json =
328
328
+
handle_api_error @@ fun () ->
329
329
+
Eio_main.run @@ fun env ->
330
330
+
Eio.Switch.run @@ fun sw ->
331
331
+
let session = Requests.create ~sw env in
332
332
+
let channel = Peertube.get_channel ~session ~base_url ~handle () in
333
333
+
print_channel ~json channel
334
334
+
335
335
+
let get_channel_cmd =
336
336
+
let doc = "Get details for a specific channel" in
337
337
+
let info = Cmd.info "get" ~doc in
338
338
+
Cmd.v info
339
339
+
Term.(const get_channel $ setup_log_term $ base_url_arg $ handle_arg $ json_flag)
340
340
+
341
341
+
let channels_cmd =
342
342
+
let doc = "Channel operations" in
343
343
+
let info = Cmd.info "channels" ~doc in
344
344
+
Cmd.group info [ list_channels_cmd; search_channels_cmd; get_channel_cmd ]
345
345
+
346
346
+
(* Account commands *)
347
347
+
348
348
+
let list_accounts () base_url count start json =
349
349
+
handle_api_error @@ fun () ->
350
350
+
Eio_main.run @@ fun env ->
351
351
+
Eio.Switch.run @@ fun sw ->
352
352
+
let session = Requests.create ~sw env in
353
353
+
let response =
354
354
+
Peertube.list_accounts ~count ~start ~session ~base_url ()
355
355
+
in
356
356
+
Fmt.pr "Showing %d of %d accounts (offset %d)@.@."
357
357
+
(List.length response.data) response.total start;
358
358
+
print_accounts ~json response.data
359
359
+
360
360
+
let list_accounts_cmd =
361
361
+
let doc = "List accounts on the instance" in
362
362
+
let info = Cmd.info "list" ~doc in
363
363
+
Cmd.v info
364
364
+
Term.(
365
365
+
const list_accounts $ setup_log_term $ base_url_arg
366
366
+
$ count_arg $ start_arg $ json_flag)
367
367
+
368
368
+
let get_account () base_url handle json =
369
369
+
handle_api_error @@ fun () ->
370
370
+
Eio_main.run @@ fun env ->
371
371
+
Eio.Switch.run @@ fun sw ->
372
372
+
let session = Requests.create ~sw env in
373
373
+
let account = Peertube.get_account ~session ~base_url ~handle () in
374
374
+
print_account ~json account
375
375
+
376
376
+
let get_account_cmd =
377
377
+
let doc = "Get details for a specific account" in
378
378
+
let info = Cmd.info "get" ~doc in
379
379
+
Cmd.v info
380
380
+
Term.(const get_account $ setup_log_term $ base_url_arg $ handle_arg $ json_flag)
381
381
+
382
382
+
let get_account_videos () base_url handle count start json =
383
383
+
handle_api_error @@ fun () ->
384
384
+
Eio_main.run @@ fun env ->
385
385
+
Eio.Switch.run @@ fun sw ->
386
386
+
let session = Requests.create ~sw env in
387
387
+
let response =
388
388
+
Peertube.get_account_videos ~count ~start ~session ~base_url ~handle ()
389
389
+
in
390
390
+
Fmt.pr "Showing %d of %d videos (offset %d)@.@."
391
391
+
(List.length response.data) response.total start;
392
392
+
print_videos ~json response.data
393
393
+
394
394
+
let get_account_videos_cmd =
395
395
+
let doc = "Get videos from an account" in
396
396
+
let info = Cmd.info "videos" ~doc in
397
397
+
Cmd.v info
398
398
+
Term.(
399
399
+
const get_account_videos $ setup_log_term $ base_url_arg $ handle_arg
400
400
+
$ count_arg $ start_arg $ json_flag)
401
401
+
402
402
+
let accounts_cmd =
403
403
+
let doc = "Account operations" in
404
404
+
let info = Cmd.info "accounts" ~doc in
405
405
+
Cmd.group info [ list_accounts_cmd; get_account_cmd; get_account_videos_cmd ]
406
406
+
407
407
+
(* Playlist commands *)
408
408
+
409
409
+
let list_playlists () base_url count start json =
410
410
+
handle_api_error @@ fun () ->
411
411
+
Eio_main.run @@ fun env ->
412
412
+
Eio.Switch.run @@ fun sw ->
413
413
+
let session = Requests.create ~sw env in
414
414
+
let response =
415
415
+
Peertube.list_playlists ~count ~start ~session ~base_url ()
416
416
+
in
417
417
+
Fmt.pr "Showing %d of %d playlists (offset %d)@.@."
418
418
+
(List.length response.data) response.total start;
419
419
+
print_playlists ~json response.data
420
420
+
421
421
+
let list_playlists_cmd =
422
422
+
let doc = "List playlists on the instance" in
423
423
+
let info = Cmd.info "list" ~doc in
424
424
+
Cmd.v info
425
425
+
Term.(
426
426
+
const list_playlists $ setup_log_term $ base_url_arg
427
427
+
$ count_arg $ start_arg $ json_flag)
428
428
+
429
429
+
let search_playlists () base_url query count start json =
430
430
+
handle_api_error @@ fun () ->
431
431
+
Eio_main.run @@ fun env ->
432
432
+
Eio.Switch.run @@ fun sw ->
433
433
+
let session = Requests.create ~sw env in
434
434
+
let response =
435
435
+
Peertube.search_playlists ~query ~count ~start ~session ~base_url ()
436
436
+
in
437
437
+
Fmt.pr "Found %d playlists for '%s' (showing %d, offset %d)@.@."
438
438
+
response.total query (List.length response.data) start;
439
439
+
print_playlists ~json response.data
440
440
+
441
441
+
let search_playlists_cmd =
442
442
+
let doc = "Search for playlists" in
443
443
+
let info = Cmd.info "search" ~doc in
444
444
+
Cmd.v info
445
445
+
Term.(
446
446
+
const search_playlists $ setup_log_term $ base_url_arg $ query_arg
447
447
+
$ count_arg $ start_arg $ json_flag)
448
448
+
449
449
+
let get_playlist () base_url id json =
450
450
+
handle_api_error @@ fun () ->
451
451
+
Eio_main.run @@ fun env ->
452
452
+
Eio.Switch.run @@ fun sw ->
453
453
+
let session = Requests.create ~sw env in
454
454
+
let playlist = Peertube.get_playlist ~session ~base_url ~id () in
455
455
+
print_playlist ~json playlist
456
456
+
457
457
+
let get_playlist_cmd =
458
458
+
let doc = "Get details for a specific playlist" in
459
459
+
let info = Cmd.info "get" ~doc in
460
460
+
Cmd.v info
461
461
+
Term.(const get_playlist $ setup_log_term $ base_url_arg $ id_arg $ json_flag)
462
462
+
463
463
+
let get_playlist_videos () base_url id count start json =
464
464
+
handle_api_error @@ fun () ->
465
465
+
Eio_main.run @@ fun env ->
466
466
+
Eio.Switch.run @@ fun sw ->
467
467
+
let session = Requests.create ~sw env in
468
468
+
let response =
469
469
+
Peertube.get_playlist_videos ~count ~start ~session ~base_url ~id ()
470
470
+
in
471
471
+
Fmt.pr "Showing %d of %d playlist elements (offset %d)@.@."
472
472
+
(List.length response.data) response.total start;
473
473
+
List.iter
474
474
+
(fun (e : Peertube.playlist_element) ->
475
475
+
match e.element_video with
476
476
+
| Some v -> print_video ~json v
477
477
+
| None -> Fmt.pr "[Deleted video at position %d]@." e.element_position)
478
478
+
response.data
479
479
+
480
480
+
let get_playlist_videos_cmd =
481
481
+
let doc = "Get videos in a playlist" in
482
482
+
let info = Cmd.info "videos" ~doc in
483
483
+
Cmd.v info
484
484
+
Term.(
485
485
+
const get_playlist_videos $ setup_log_term $ base_url_arg $ id_arg
486
486
+
$ count_arg $ start_arg $ json_flag)
487
487
+
488
488
+
let playlists_cmd =
489
489
+
let doc = "Playlist operations" in
490
490
+
let info = Cmd.info "playlists" ~doc in
491
491
+
Cmd.group info
492
492
+
[ list_playlists_cmd; search_playlists_cmd; get_playlist_cmd;
493
493
+
get_playlist_videos_cmd ]
494
494
+
495
495
+
(* Server commands *)
496
496
+
497
497
+
let server_info () base_url json =
498
498
+
handle_api_error @@ fun () ->
499
499
+
Eio_main.run @@ fun env ->
500
500
+
Eio.Switch.run @@ fun sw ->
501
501
+
let session = Requests.create ~sw env in
502
502
+
let config = Peertube.get_config ~session ~base_url () in
503
503
+
if json then
504
504
+
let json_str = Jsont_bytesrw.encode_string Peertube.server_config_jsont config in
505
505
+
match json_str with
506
506
+
| Ok s -> print_endline s
507
507
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
508
508
+
else begin
509
509
+
Fmt.pr "@[<v>Instance: %s@, %s@, Version: %s@, Signup allowed: %b@, Transcoding: %b@]@."
510
510
+
config.config_instance.instance_name
511
511
+
config.config_instance.instance_short_description
512
512
+
config.config_server_version
513
513
+
config.config_signup_allowed
514
514
+
config.config_transcoding_enabled
515
515
+
end
516
516
+
517
517
+
let server_info_cmd =
518
518
+
let doc = "Get server configuration" in
519
519
+
let info = Cmd.info "info" ~doc in
520
520
+
Cmd.v info
521
521
+
Term.(const server_info $ setup_log_term $ base_url_arg $ json_flag)
522
522
+
523
523
+
let server_stats () base_url json =
524
524
+
handle_api_error @@ fun () ->
525
525
+
Eio_main.run @@ fun env ->
526
526
+
Eio.Switch.run @@ fun sw ->
527
527
+
let session = Requests.create ~sw env in
528
528
+
let stats = Peertube.get_stats ~session ~base_url () in
529
529
+
if json then
530
530
+
let json_str = Jsont_bytesrw.encode_string Peertube.server_stats_jsont stats in
531
531
+
match json_str with
532
532
+
| Ok s -> print_endline s
533
533
+
| Error e -> Fmt.epr "JSON encode error: %s@." e
534
534
+
else begin
535
535
+
Fmt.pr "@[<v>Users: %d (daily active: %d, weekly: %d, monthly: %d)@,Local videos: %d (views: %d)@,Total videos: %d@,Local channels: %d@,Local playlists: %d@,Instance followers: %d / following: %d@]@."
536
536
+
stats.stats_total_users
537
537
+
stats.stats_total_daily_active_users
538
538
+
stats.stats_total_weekly_active_users
539
539
+
stats.stats_total_monthly_active_users
540
540
+
stats.stats_total_local_videos
541
541
+
stats.stats_total_local_video_views
542
542
+
stats.stats_total_videos
543
543
+
stats.stats_total_local_video_channels
544
544
+
stats.stats_total_local_playlists
545
545
+
stats.stats_total_instance_followers
546
546
+
stats.stats_total_instance_following
547
547
+
end
548
548
+
549
549
+
let server_stats_cmd =
550
550
+
let doc = "Get server statistics" in
551
551
+
let info = Cmd.info "stats" ~doc in
552
552
+
Cmd.v info
553
553
+
Term.(const server_stats $ setup_log_term $ base_url_arg $ json_flag)
554
554
+
555
555
+
let server_cmd =
556
556
+
let doc = "Server information" in
557
557
+
let info = Cmd.info "server" ~doc in
558
558
+
Cmd.group info [ server_info_cmd; server_stats_cmd ]
559
559
+
560
560
+
(* Main command *)
561
561
+
562
562
+
let main_cmd =
563
563
+
let doc = "PeerTube API command-line client" in
564
564
+
let info = Cmd.info "opeertube" ~version:"0.1.0" ~doc in
565
565
+
Cmd.group info
566
566
+
[ browse_videos_cmd; search_videos_cmd; get_video_cmd;
567
567
+
list_channel_videos_cmd; download_thumbnail_cmd;
568
568
+
channels_cmd; accounts_cmd; playlists_cmd; server_cmd ]
569
569
+
570
570
+
let () = exit (Cmd.eval main_cmd)
+24
dune-project
···
1
1
+
(lang dune 3.16)
2
2
+
(name peertube)
3
3
+
4
4
+
(generate_opam_files true)
5
5
+
6
6
+
(source (github avsm/ocaml-peertube))
7
7
+
(license ISC)
8
8
+
(authors "Anil Madhavapeddy")
9
9
+
(maintainers "anil@recoil.org")
10
10
+
11
11
+
(package
12
12
+
(name peertube)
13
13
+
(synopsis "PeerTube API client for OCaml using Eio")
14
14
+
(description "An OCaml client library for the PeerTube video platform API, built on Eio for effect-based I/O")
15
15
+
(depends
16
16
+
(ocaml (>= 5.1.0))
17
17
+
(eio (>= 1.0))
18
18
+
(eio_main (>= 1.0))
19
19
+
(requests (>= 0.1))
20
20
+
(jsont (>= 0.1))
21
21
+
(ptime (>= 1.0))
22
22
+
(fmt (>= 0.9))
23
23
+
(logs (>= 0.7))
24
24
+
(cmdliner (>= 1.2))))
+4
lib/dune
···
1
1
+
(library
2
2
+
(name peertube)
3
3
+
(public_name peertube)
4
4
+
(libraries requests jsont jsont.bytesrw ptime fmt logs))
+1108
lib/peertube.ml
···
1
1
+
(** PeerTube API client implementation using Eio, Requests, and jsont. *)
2
2
+
3
3
+
let log_src = Logs.Src.create "peertube" ~doc:"PeerTube API client"
4
4
+
5
5
+
module Log = (val Logs.src_log log_src : Logs.LOG)
6
6
+
7
7
+
exception Api_error of int * string
8
8
+
9
9
+
(* Error handling *)
10
10
+
11
11
+
let error_response_jsont : string Jsont.t =
12
12
+
let make _type error _status detail =
13
13
+
match detail with
14
14
+
| Some d -> d
15
15
+
| None -> Option.value ~default:"Unknown error" error
16
16
+
in
17
17
+
Jsont.Object.map ~kind:"error_response" make
18
18
+
|> Jsont.Object.mem "type" Jsont.string ~dec_absent:"" ~enc:(Fun.const "")
19
19
+
|> Jsont.Object.mem "error" (Jsont.option Jsont.string)
20
20
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(Fun.const None)
21
21
+
|> Jsont.Object.mem "status" Jsont.int ~dec_absent:0 ~enc:(Fun.const 0)
22
22
+
|> Jsont.Object.mem "detail" (Jsont.option Jsont.string)
23
23
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:Option.some
24
24
+
|> Jsont.Object.skip_unknown
25
25
+
|> Jsont.Object.finish
26
26
+
27
27
+
let raise_api_error status body =
28
28
+
let msg =
29
29
+
match Jsont_bytesrw.decode_string error_response_jsont body with
30
30
+
| Ok msg -> msg
31
31
+
| Error _ -> Printf.sprintf "HTTP %d" status
32
32
+
in
33
33
+
Log.err (fun m -> m "API error %d: %s" status msg);
34
34
+
raise (Api_error (status, msg))
35
35
+
36
36
+
(* Common types *)
37
37
+
38
38
+
type 'a labeled = { id : 'a; label : string }
39
39
+
40
40
+
type privacy = Public | Unlisted | Private | Internal
41
41
+
42
42
+
type video_sort = Newest | Oldest | Views | Likes | Trending | Hot | Random | Best
43
43
+
44
44
+
let string_of_video_sort = function
45
45
+
| Newest -> "-publishedAt"
46
46
+
| Oldest -> "publishedAt"
47
47
+
| Views -> "-views"
48
48
+
| Likes -> "-likes"
49
49
+
| Trending -> "-trending"
50
50
+
| Hot -> "-hot"
51
51
+
| Random -> "random"
52
52
+
| Best -> "-best"
53
53
+
54
54
+
(* Account types *)
55
55
+
56
56
+
type account_summary = {
57
57
+
account_id : int;
58
58
+
account_name : string;
59
59
+
account_display_name : string;
60
60
+
account_url : string;
61
61
+
account_host : string;
62
62
+
account_avatar_path : string option;
63
63
+
}
64
64
+
65
65
+
type account = {
66
66
+
account : account_summary;
67
67
+
account_description : string option;
68
68
+
account_created_at : Ptime.t;
69
69
+
account_followers_count : int;
70
70
+
account_following_count : int;
71
71
+
account_following_hosts_count : int option;
72
72
+
}
73
73
+
74
74
+
(* Channel types *)
75
75
+
76
76
+
type channel_summary = {
77
77
+
channel_id : int;
78
78
+
channel_name : string;
79
79
+
channel_display_name : string;
80
80
+
channel_url : string;
81
81
+
channel_host : string;
82
82
+
channel_avatar_path : string option;
83
83
+
}
84
84
+
85
85
+
type channel = {
86
86
+
channel : channel_summary;
87
87
+
channel_description : string option;
88
88
+
channel_support : string option;
89
89
+
channel_created_at : Ptime.t;
90
90
+
channel_followers_count : int;
91
91
+
channel_following_count : int;
92
92
+
channel_banner_path : string option;
93
93
+
channel_owner_account : account_summary option;
94
94
+
}
95
95
+
96
96
+
(* Video types *)
97
97
+
98
98
+
type video = {
99
99
+
id : int;
100
100
+
uuid : string;
101
101
+
short_uuid : string option;
102
102
+
name : string;
103
103
+
description : string option;
104
104
+
url : string;
105
105
+
embed_path : string;
106
106
+
published_at : Ptime.t;
107
107
+
originally_published_at : Ptime.t option;
108
108
+
updated_at : Ptime.t option;
109
109
+
thumbnail_path : string option;
110
110
+
preview_path : string option;
111
111
+
tags : string list;
112
112
+
duration : int;
113
113
+
views : int;
114
114
+
likes : int;
115
115
+
dislikes : int;
116
116
+
is_local : bool;
117
117
+
is_live : bool;
118
118
+
privacy : privacy;
119
119
+
category : int labeled option;
120
120
+
licence : int labeled option;
121
121
+
language : string labeled option;
122
122
+
channel : channel_summary option;
123
123
+
account : account_summary option;
124
124
+
}
125
125
+
126
126
+
type 'a paginated = { total : int; data : 'a list }
127
127
+
128
128
+
(* Playlist types *)
129
129
+
130
130
+
type playlist_privacy = PlaylistPublic | PlaylistUnlisted | PlaylistPrivate
131
131
+
132
132
+
type playlist_type = Regular | WatchLater
133
133
+
134
134
+
type playlist = {
135
135
+
playlist_id : int;
136
136
+
playlist_uuid : string;
137
137
+
playlist_short_uuid : string option;
138
138
+
playlist_display_name : string;
139
139
+
playlist_description : string option;
140
140
+
playlist_privacy : playlist_privacy;
141
141
+
playlist_url : string;
142
142
+
playlist_thumbnail_path : string option;
143
143
+
playlist_videos_length : int;
144
144
+
playlist_type : playlist_type;
145
145
+
playlist_created_at : Ptime.t;
146
146
+
playlist_updated_at : Ptime.t;
147
147
+
playlist_owner_account : account_summary option;
148
148
+
playlist_video_channel : channel_summary option;
149
149
+
}
150
150
+
151
151
+
type playlist_element = {
152
152
+
element_id : int;
153
153
+
element_position : int;
154
154
+
element_start_timestamp : int option;
155
155
+
element_stop_timestamp : int option;
156
156
+
element_video : video option;
157
157
+
}
158
158
+
159
159
+
(* Server types *)
160
160
+
161
161
+
type instance_info = {
162
162
+
instance_name : string;
163
163
+
instance_short_description : string;
164
164
+
instance_description : string option;
165
165
+
instance_terms : string option;
166
166
+
instance_is_nsfw : bool;
167
167
+
instance_default_nsfw_policy : string;
168
168
+
instance_default_client_route : string;
169
169
+
}
170
170
+
171
171
+
type server_config = {
172
172
+
config_instance : instance_info;
173
173
+
config_server_version : string;
174
174
+
config_server_commit : string option;
175
175
+
config_signup_allowed : bool;
176
176
+
config_signup_allowed_for_current_ip : bool;
177
177
+
config_signup_requires_email_verification : bool;
178
178
+
config_transcoding_enabled : bool;
179
179
+
config_contact_form_enabled : bool;
180
180
+
}
181
181
+
182
182
+
type server_stats = {
183
183
+
stats_total_users : int;
184
184
+
stats_total_daily_active_users : int;
185
185
+
stats_total_weekly_active_users : int;
186
186
+
stats_total_monthly_active_users : int;
187
187
+
stats_total_local_videos : int;
188
188
+
stats_total_local_video_views : int;
189
189
+
stats_total_local_video_comments : int;
190
190
+
stats_total_local_video_files_size : int64;
191
191
+
stats_total_videos : int;
192
192
+
stats_total_video_comments : int;
193
193
+
stats_total_local_video_channels : int;
194
194
+
stats_total_local_playlists : int;
195
195
+
stats_total_instance_followers : int;
196
196
+
stats_total_instance_following : int;
197
197
+
}
198
198
+
199
199
+
(* JSON codecs - helpers *)
200
200
+
201
201
+
let parse_date str =
202
202
+
match Ptime.of_rfc3339 str with
203
203
+
| Ok (date, _, _) -> Ok date
204
204
+
| Error _ ->
205
205
+
Log.warn (fun m -> m "Failed to parse RFC3339 date: %s" str);
206
206
+
Error (Fmt.str "Invalid RFC3339 date: %s" str)
207
207
+
208
208
+
let ptime_jsont : Ptime.t Jsont.t =
209
209
+
Jsont.of_of_string ~kind:"ptime" ~enc:Ptime.to_rfc3339 parse_date
210
210
+
211
211
+
let privacy_jsont : privacy Jsont.t =
212
212
+
let privacy_of_int = function
213
213
+
| 1 -> Public
214
214
+
| 2 -> Unlisted
215
215
+
| 3 -> Private
216
216
+
| 4 -> Internal
217
217
+
| _ -> Public
218
218
+
in
219
219
+
let int_of_privacy = function
220
220
+
| Public -> 1
221
221
+
| Unlisted -> 2
222
222
+
| Private -> 3
223
223
+
| Internal -> 4
224
224
+
in
225
225
+
let make id _label = privacy_of_int id in
226
226
+
Jsont.Object.map ~kind:"privacy" make
227
227
+
|> Jsont.Object.mem "id" Jsont.int ~enc:int_of_privacy
228
228
+
|> Jsont.Object.mem "label" Jsont.string ~dec_absent:"" ~enc:(Fun.const "")
229
229
+
|> Jsont.Object.skip_unknown
230
230
+
|> Jsont.Object.finish
231
231
+
232
232
+
let playlist_privacy_jsont : playlist_privacy Jsont.t =
233
233
+
let privacy_of_int = function
234
234
+
| 1 -> PlaylistPublic
235
235
+
| 2 -> PlaylistUnlisted
236
236
+
| 3 -> PlaylistPrivate
237
237
+
| _ -> PlaylistPublic
238
238
+
in
239
239
+
let int_of_privacy = function
240
240
+
| PlaylistPublic -> 1
241
241
+
| PlaylistUnlisted -> 2
242
242
+
| PlaylistPrivate -> 3
243
243
+
in
244
244
+
let make id _label = privacy_of_int id in
245
245
+
Jsont.Object.map ~kind:"playlist_privacy" make
246
246
+
|> Jsont.Object.mem "id" Jsont.int ~enc:int_of_privacy
247
247
+
|> Jsont.Object.mem "label" Jsont.string ~dec_absent:"" ~enc:(Fun.const "")
248
248
+
|> Jsont.Object.skip_unknown
249
249
+
|> Jsont.Object.finish
250
250
+
251
251
+
let playlist_type_jsont : playlist_type Jsont.t =
252
252
+
let type_of_int = function 1 -> Regular | 2 -> WatchLater | _ -> Regular in
253
253
+
let int_of_type = function Regular -> 1 | WatchLater -> 2 in
254
254
+
let make id _label = type_of_int id in
255
255
+
Jsont.Object.map ~kind:"playlist_type" make
256
256
+
|> Jsont.Object.mem "id" Jsont.int ~enc:int_of_type
257
257
+
|> Jsont.Object.mem "label" Jsont.string ~dec_absent:"" ~enc:(Fun.const "")
258
258
+
|> Jsont.Object.skip_unknown
259
259
+
|> Jsont.Object.finish
260
260
+
261
261
+
let int_labeled_jsont : int labeled Jsont.t =
262
262
+
let make id label : int labeled = { id; label } in
263
263
+
Jsont.Object.map ~kind:"int_labeled" make
264
264
+
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun (l : int labeled) -> l.id)
265
265
+
|> Jsont.Object.mem "label" Jsont.string ~enc:(fun (l : int labeled) -> l.label)
266
266
+
|> Jsont.Object.skip_unknown
267
267
+
|> Jsont.Object.finish
268
268
+
269
269
+
let string_labeled_jsont : string labeled Jsont.t =
270
270
+
let make id label : string labeled = { id; label } in
271
271
+
Jsont.Object.map ~kind:"string_labeled" make
272
272
+
|> Jsont.Object.mem "id" Jsont.string ~enc:(fun (l : string labeled) -> l.id)
273
273
+
|> Jsont.Object.mem "label" Jsont.string ~enc:(fun (l : string labeled) -> l.label)
274
274
+
|> Jsont.Object.skip_unknown
275
275
+
|> Jsont.Object.finish
276
276
+
277
277
+
(* Extract avatar path from nested avatars array *)
278
278
+
let avatar_path_jsont : string option Jsont.t =
279
279
+
let avatar_jsont =
280
280
+
let make path _width = path in
281
281
+
Jsont.Object.map ~kind:"avatar" make
282
282
+
|> Jsont.Object.mem "path" Jsont.string ~enc:Fun.id
283
283
+
|> Jsont.Object.mem "width" Jsont.int ~dec_absent:0 ~enc:(Fun.const 0)
284
284
+
|> Jsont.Object.skip_unknown
285
285
+
|> Jsont.Object.finish
286
286
+
in
287
287
+
Jsont.map ~kind:"avatar_path"
288
288
+
~dec:(function [] -> None | x :: _ -> Some x)
289
289
+
~enc:(function None -> [] | Some p -> [ p ])
290
290
+
(Jsont.list avatar_jsont)
291
291
+
292
292
+
(* Account codecs *)
293
293
+
294
294
+
let account_summary_jsont : account_summary Jsont.t =
295
295
+
let make account_id account_name account_display_name account_url account_host
296
296
+
account_avatar_path =
297
297
+
{ account_id; account_name; account_display_name; account_url; account_host;
298
298
+
account_avatar_path }
299
299
+
in
300
300
+
Jsont.Object.map ~kind:"account_summary" make
301
301
+
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun a -> a.account_id)
302
302
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun a -> a.account_name)
303
303
+
|> Jsont.Object.mem "displayName" Jsont.string
304
304
+
~enc:(fun a -> a.account_display_name)
305
305
+
|> Jsont.Object.mem "url" Jsont.string ~enc:(fun a -> a.account_url)
306
306
+
|> Jsont.Object.mem "host" Jsont.string ~enc:(fun a -> a.account_host)
307
307
+
|> Jsont.Object.mem "avatars" avatar_path_jsont
308
308
+
~dec_absent:None ~enc_omit:Option.is_none
309
309
+
~enc:(fun a -> a.account_avatar_path)
310
310
+
|> Jsont.Object.skip_unknown
311
311
+
|> Jsont.Object.finish
312
312
+
313
313
+
let account_jsont : account Jsont.t =
314
314
+
let make account_id account_name account_display_name account_url account_host
315
315
+
account_avatar_path account_description account_created_at
316
316
+
account_followers_count account_following_count
317
317
+
account_following_hosts_count =
318
318
+
let account =
319
319
+
{ account_id; account_name; account_display_name; account_url;
320
320
+
account_host; account_avatar_path }
321
321
+
in
322
322
+
{ account; account_description; account_created_at; account_followers_count;
323
323
+
account_following_count; account_following_hosts_count }
324
324
+
in
325
325
+
Jsont.Object.map ~kind:"account" make
326
326
+
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun (a : account) -> a.account.account_id)
327
327
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun (a : account) -> a.account.account_name)
328
328
+
|> Jsont.Object.mem "displayName" Jsont.string
329
329
+
~enc:(fun (a : account) -> a.account.account_display_name)
330
330
+
|> Jsont.Object.mem "url" Jsont.string ~enc:(fun (a : account) -> a.account.account_url)
331
331
+
|> Jsont.Object.mem "host" Jsont.string ~enc:(fun (a : account) -> a.account.account_host)
332
332
+
|> Jsont.Object.mem "avatars" avatar_path_jsont
333
333
+
~dec_absent:None ~enc_omit:Option.is_none
334
334
+
~enc:(fun (a : account) -> a.account.account_avatar_path)
335
335
+
|> Jsont.Object.mem "description" (Jsont.option Jsont.string)
336
336
+
~dec_absent:None ~enc_omit:Option.is_none
337
337
+
~enc:(fun a -> a.account_description)
338
338
+
|> Jsont.Object.mem "createdAt" ptime_jsont
339
339
+
~enc:(fun a -> a.account_created_at)
340
340
+
|> Jsont.Object.mem "followersCount" Jsont.int
341
341
+
~dec_absent:0 ~enc:(fun a -> a.account_followers_count)
342
342
+
|> Jsont.Object.mem "followingCount" Jsont.int
343
343
+
~dec_absent:0 ~enc:(fun a -> a.account_following_count)
344
344
+
|> Jsont.Object.mem "hostRedundancyAllowed" (Jsont.option Jsont.int)
345
345
+
~dec_absent:None ~enc_omit:Option.is_none
346
346
+
~enc:(fun a -> a.account_following_hosts_count)
347
347
+
|> Jsont.Object.skip_unknown
348
348
+
|> Jsont.Object.finish
349
349
+
350
350
+
let account_paginated_jsont : account paginated Jsont.t =
351
351
+
let make total data = { total; data } in
352
352
+
Jsont.Object.map ~kind:"account_paginated" make
353
353
+
|> Jsont.Object.mem "total" Jsont.int ~enc:(fun r -> r.total)
354
354
+
|> Jsont.Object.mem "data" (Jsont.list account_jsont) ~enc:(fun r -> r.data)
355
355
+
|> Jsont.Object.skip_unknown
356
356
+
|> Jsont.Object.finish
357
357
+
358
358
+
(* Channel codecs *)
359
359
+
360
360
+
let channel_summary_jsont : channel_summary Jsont.t =
361
361
+
let make channel_id channel_name channel_display_name channel_url channel_host
362
362
+
channel_avatar_path =
363
363
+
{ channel_id; channel_name; channel_display_name; channel_url; channel_host;
364
364
+
channel_avatar_path }
365
365
+
in
366
366
+
Jsont.Object.map ~kind:"channel_summary" make
367
367
+
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun c -> c.channel_id)
368
368
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.channel_name)
369
369
+
|> Jsont.Object.mem "displayName" Jsont.string
370
370
+
~enc:(fun c -> c.channel_display_name)
371
371
+
|> Jsont.Object.mem "url" Jsont.string ~enc:(fun c -> c.channel_url)
372
372
+
|> Jsont.Object.mem "host" Jsont.string ~enc:(fun c -> c.channel_host)
373
373
+
|> Jsont.Object.mem "avatars" avatar_path_jsont
374
374
+
~dec_absent:None ~enc_omit:Option.is_none
375
375
+
~enc:(fun c -> c.channel_avatar_path)
376
376
+
|> Jsont.Object.skip_unknown
377
377
+
|> Jsont.Object.finish
378
378
+
379
379
+
let channel_jsont : channel Jsont.t =
380
380
+
let make channel_id channel_name channel_display_name channel_url channel_host
381
381
+
channel_avatar_path channel_description channel_support channel_created_at
382
382
+
channel_followers_count channel_following_count channel_banner_path
383
383
+
channel_owner_account =
384
384
+
let channel =
385
385
+
{ channel_id; channel_name; channel_display_name; channel_url;
386
386
+
channel_host; channel_avatar_path }
387
387
+
in
388
388
+
{ channel; channel_description; channel_support; channel_created_at;
389
389
+
channel_followers_count; channel_following_count; channel_banner_path;
390
390
+
channel_owner_account }
391
391
+
in
392
392
+
Jsont.Object.map ~kind:"channel" make
393
393
+
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun (c : channel) -> c.channel.channel_id)
394
394
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun (c : channel) -> c.channel.channel_name)
395
395
+
|> Jsont.Object.mem "displayName" Jsont.string
396
396
+
~enc:(fun (c : channel) -> c.channel.channel_display_name)
397
397
+
|> Jsont.Object.mem "url" Jsont.string ~enc:(fun (c : channel) -> c.channel.channel_url)
398
398
+
|> Jsont.Object.mem "host" Jsont.string ~enc:(fun (c : channel) -> c.channel.channel_host)
399
399
+
|> Jsont.Object.mem "avatars" avatar_path_jsont
400
400
+
~dec_absent:None ~enc_omit:Option.is_none
401
401
+
~enc:(fun (c : channel) -> c.channel.channel_avatar_path)
402
402
+
|> Jsont.Object.mem "description" (Jsont.option Jsont.string)
403
403
+
~dec_absent:None ~enc_omit:Option.is_none
404
404
+
~enc:(fun c -> c.channel_description)
405
405
+
|> Jsont.Object.mem "support" (Jsont.option Jsont.string)
406
406
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun c -> c.channel_support)
407
407
+
|> Jsont.Object.mem "createdAt" ptime_jsont
408
408
+
~enc:(fun c -> c.channel_created_at)
409
409
+
|> Jsont.Object.mem "followersCount" Jsont.int
410
410
+
~dec_absent:0 ~enc:(fun c -> c.channel_followers_count)
411
411
+
|> Jsont.Object.mem "followingCount" Jsont.int
412
412
+
~dec_absent:0 ~enc:(fun c -> c.channel_following_count)
413
413
+
|> Jsont.Object.mem "banners" avatar_path_jsont
414
414
+
~dec_absent:None ~enc_omit:Option.is_none
415
415
+
~enc:(fun c -> c.channel_banner_path)
416
416
+
|> Jsont.Object.mem "ownerAccount" (Jsont.option account_summary_jsont)
417
417
+
~dec_absent:None ~enc_omit:Option.is_none
418
418
+
~enc:(fun c -> c.channel_owner_account)
419
419
+
|> Jsont.Object.skip_unknown
420
420
+
|> Jsont.Object.finish
421
421
+
422
422
+
let channel_paginated_jsont : channel paginated Jsont.t =
423
423
+
let make total data = { total; data } in
424
424
+
Jsont.Object.map ~kind:"channel_paginated" make
425
425
+
|> Jsont.Object.mem "total" Jsont.int ~enc:(fun r -> r.total)
426
426
+
|> Jsont.Object.mem "data" (Jsont.list channel_jsont) ~enc:(fun r -> r.data)
427
427
+
|> Jsont.Object.skip_unknown
428
428
+
|> Jsont.Object.finish
429
429
+
430
430
+
(* Video codecs *)
431
431
+
432
432
+
let video_jsont : video Jsont.t =
433
433
+
let make id uuid short_uuid name description url embed_path published_at
434
434
+
originally_published_at updated_at thumbnail_path preview_path tags
435
435
+
duration views likes dislikes is_local is_live privacy category licence
436
436
+
language channel account =
437
437
+
{ id; uuid; short_uuid; name; description; url; embed_path; published_at;
438
438
+
originally_published_at; updated_at; thumbnail_path; preview_path; tags;
439
439
+
duration; views; likes; dislikes; is_local; is_live; privacy; category;
440
440
+
licence; language; channel; account }
441
441
+
in
442
442
+
Jsont.Object.map ~kind:"video" make
443
443
+
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun v -> v.id)
444
444
+
|> Jsont.Object.mem "uuid" Jsont.string ~enc:(fun v -> v.uuid)
445
445
+
|> Jsont.Object.mem "shortUUID" (Jsont.option Jsont.string)
446
446
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.short_uuid)
447
447
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun v -> v.name)
448
448
+
|> Jsont.Object.mem "description" (Jsont.option Jsont.string)
449
449
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.description)
450
450
+
|> Jsont.Object.mem "url" Jsont.string ~enc:(fun v -> v.url)
451
451
+
|> Jsont.Object.mem "embedPath" Jsont.string ~enc:(fun v -> v.embed_path)
452
452
+
|> Jsont.Object.mem "publishedAt" ptime_jsont ~enc:(fun v -> v.published_at)
453
453
+
|> Jsont.Object.mem "originallyPublishedAt" (Jsont.option ptime_jsont)
454
454
+
~dec_absent:None ~enc_omit:Option.is_none
455
455
+
~enc:(fun v -> v.originally_published_at)
456
456
+
|> Jsont.Object.mem "updatedAt" (Jsont.option ptime_jsont)
457
457
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.updated_at)
458
458
+
|> Jsont.Object.mem "thumbnailPath" (Jsont.option Jsont.string)
459
459
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.thumbnail_path)
460
460
+
|> Jsont.Object.mem "previewPath" (Jsont.option Jsont.string)
461
461
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.preview_path)
462
462
+
|> Jsont.Object.mem "tags" Jsont.(list string)
463
463
+
~dec_absent:[] ~enc_omit:(fun l -> l = []) ~enc:(fun v -> v.tags)
464
464
+
|> Jsont.Object.mem "duration" Jsont.int ~dec_absent:0 ~enc:(fun v -> v.duration)
465
465
+
|> Jsont.Object.mem "views" Jsont.int ~dec_absent:0 ~enc:(fun v -> v.views)
466
466
+
|> Jsont.Object.mem "likes" Jsont.int ~dec_absent:0 ~enc:(fun v -> v.likes)
467
467
+
|> Jsont.Object.mem "dislikes" Jsont.int ~dec_absent:0 ~enc:(fun v -> v.dislikes)
468
468
+
|> Jsont.Object.mem "isLocal" Jsont.bool ~dec_absent:true ~enc:(fun v -> v.is_local)
469
469
+
|> Jsont.Object.mem "isLive" Jsont.bool ~dec_absent:false ~enc:(fun v -> v.is_live)
470
470
+
|> Jsont.Object.mem "privacy" privacy_jsont
471
471
+
~dec_absent:Public ~enc:(fun v -> v.privacy)
472
472
+
|> Jsont.Object.mem "category" (Jsont.option int_labeled_jsont)
473
473
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.category)
474
474
+
|> Jsont.Object.mem "licence" (Jsont.option int_labeled_jsont)
475
475
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.licence)
476
476
+
|> Jsont.Object.mem "language" (Jsont.option string_labeled_jsont)
477
477
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.language)
478
478
+
|> Jsont.Object.mem "channel" (Jsont.option channel_summary_jsont)
479
479
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.channel)
480
480
+
|> Jsont.Object.mem "account" (Jsont.option account_summary_jsont)
481
481
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.account)
482
482
+
|> Jsont.Object.skip_unknown
483
483
+
|> Jsont.Object.finish
484
484
+
485
485
+
let video_paginated_jsont : video paginated Jsont.t =
486
486
+
let make total data = { total; data } in
487
487
+
Jsont.Object.map ~kind:"video_paginated" make
488
488
+
|> Jsont.Object.mem "total" Jsont.int ~enc:(fun r -> r.total)
489
489
+
|> Jsont.Object.mem "data" (Jsont.list video_jsont) ~enc:(fun r -> r.data)
490
490
+
|> Jsont.Object.skip_unknown
491
491
+
|> Jsont.Object.finish
492
492
+
493
493
+
(* Playlist codecs *)
494
494
+
495
495
+
let playlist_jsont : playlist Jsont.t =
496
496
+
let make playlist_id playlist_uuid playlist_short_uuid playlist_display_name
497
497
+
playlist_description playlist_privacy playlist_url playlist_thumbnail_path
498
498
+
playlist_videos_length playlist_type playlist_created_at
499
499
+
playlist_updated_at playlist_owner_account playlist_video_channel =
500
500
+
{ playlist_id; playlist_uuid; playlist_short_uuid; playlist_display_name;
501
501
+
playlist_description; playlist_privacy; playlist_url;
502
502
+
playlist_thumbnail_path; playlist_videos_length; playlist_type;
503
503
+
playlist_created_at; playlist_updated_at; playlist_owner_account;
504
504
+
playlist_video_channel }
505
505
+
in
506
506
+
Jsont.Object.map ~kind:"playlist" make
507
507
+
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun p -> p.playlist_id)
508
508
+
|> Jsont.Object.mem "uuid" Jsont.string ~enc:(fun p -> p.playlist_uuid)
509
509
+
|> Jsont.Object.mem "shortUUID" (Jsont.option Jsont.string)
510
510
+
~dec_absent:None ~enc_omit:Option.is_none
511
511
+
~enc:(fun p -> p.playlist_short_uuid)
512
512
+
|> Jsont.Object.mem "displayName" Jsont.string
513
513
+
~enc:(fun p -> p.playlist_display_name)
514
514
+
|> Jsont.Object.mem "description" (Jsont.option Jsont.string)
515
515
+
~dec_absent:None ~enc_omit:Option.is_none
516
516
+
~enc:(fun p -> p.playlist_description)
517
517
+
|> Jsont.Object.mem "privacy" playlist_privacy_jsont
518
518
+
~enc:(fun p -> p.playlist_privacy)
519
519
+
|> Jsont.Object.mem "url" Jsont.string ~enc:(fun p -> p.playlist_url)
520
520
+
|> Jsont.Object.mem "thumbnailPath" (Jsont.option Jsont.string)
521
521
+
~dec_absent:None ~enc_omit:Option.is_none
522
522
+
~enc:(fun p -> p.playlist_thumbnail_path)
523
523
+
|> Jsont.Object.mem "videosLength" Jsont.int
524
524
+
~dec_absent:0 ~enc:(fun p -> p.playlist_videos_length)
525
525
+
|> Jsont.Object.mem "type" playlist_type_jsont ~enc:(fun p -> p.playlist_type)
526
526
+
|> Jsont.Object.mem "createdAt" ptime_jsont
527
527
+
~enc:(fun p -> p.playlist_created_at)
528
528
+
|> Jsont.Object.mem "updatedAt" ptime_jsont
529
529
+
~enc:(fun p -> p.playlist_updated_at)
530
530
+
|> Jsont.Object.mem "ownerAccount" (Jsont.option account_summary_jsont)
531
531
+
~dec_absent:None ~enc_omit:Option.is_none
532
532
+
~enc:(fun p -> p.playlist_owner_account)
533
533
+
|> Jsont.Object.mem "videoChannel" (Jsont.option channel_summary_jsont)
534
534
+
~dec_absent:None ~enc_omit:Option.is_none
535
535
+
~enc:(fun p -> p.playlist_video_channel)
536
536
+
|> Jsont.Object.skip_unknown
537
537
+
|> Jsont.Object.finish
538
538
+
539
539
+
let playlist_paginated_jsont : playlist paginated Jsont.t =
540
540
+
let make total data = { total; data } in
541
541
+
Jsont.Object.map ~kind:"playlist_paginated" make
542
542
+
|> Jsont.Object.mem "total" Jsont.int ~enc:(fun r -> r.total)
543
543
+
|> Jsont.Object.mem "data" (Jsont.list playlist_jsont) ~enc:(fun r -> r.data)
544
544
+
|> Jsont.Object.skip_unknown
545
545
+
|> Jsont.Object.finish
546
546
+
547
547
+
let playlist_element_jsont : playlist_element Jsont.t =
548
548
+
let make element_id element_position element_start_timestamp
549
549
+
element_stop_timestamp element_video =
550
550
+
{ element_id; element_position; element_start_timestamp;
551
551
+
element_stop_timestamp; element_video }
552
552
+
in
553
553
+
Jsont.Object.map ~kind:"playlist_element" make
554
554
+
|> Jsont.Object.mem "id" Jsont.int ~enc:(fun e -> e.element_id)
555
555
+
|> Jsont.Object.mem "position" Jsont.int ~enc:(fun e -> e.element_position)
556
556
+
|> Jsont.Object.mem "startTimestamp" (Jsont.option Jsont.int)
557
557
+
~dec_absent:None ~enc_omit:Option.is_none
558
558
+
~enc:(fun e -> e.element_start_timestamp)
559
559
+
|> Jsont.Object.mem "stopTimestamp" (Jsont.option Jsont.int)
560
560
+
~dec_absent:None ~enc_omit:Option.is_none
561
561
+
~enc:(fun e -> e.element_stop_timestamp)
562
562
+
|> Jsont.Object.mem "video" (Jsont.option video_jsont)
563
563
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun e -> e.element_video)
564
564
+
|> Jsont.Object.skip_unknown
565
565
+
|> Jsont.Object.finish
566
566
+
567
567
+
let playlist_element_paginated_jsont : playlist_element paginated Jsont.t =
568
568
+
let make total data = { total; data } in
569
569
+
Jsont.Object.map ~kind:"playlist_element_paginated" make
570
570
+
|> Jsont.Object.mem "total" Jsont.int ~enc:(fun r -> r.total)
571
571
+
|> Jsont.Object.mem "data" (Jsont.list playlist_element_jsont)
572
572
+
~enc:(fun r -> r.data)
573
573
+
|> Jsont.Object.skip_unknown
574
574
+
|> Jsont.Object.finish
575
575
+
576
576
+
(* Server codecs *)
577
577
+
578
578
+
let instance_info_jsont : instance_info Jsont.t =
579
579
+
let make instance_name instance_short_description instance_description
580
580
+
instance_terms instance_is_nsfw instance_default_nsfw_policy
581
581
+
instance_default_client_route =
582
582
+
{ instance_name; instance_short_description; instance_description;
583
583
+
instance_terms; instance_is_nsfw; instance_default_nsfw_policy;
584
584
+
instance_default_client_route }
585
585
+
in
586
586
+
Jsont.Object.map ~kind:"instance_info" make
587
587
+
|> Jsont.Object.mem "name" Jsont.string ~enc:(fun i -> i.instance_name)
588
588
+
|> Jsont.Object.mem "shortDescription" Jsont.string
589
589
+
~dec_absent:"" ~enc:(fun i -> i.instance_short_description)
590
590
+
|> Jsont.Object.mem "description" (Jsont.option Jsont.string)
591
591
+
~dec_absent:None ~enc_omit:Option.is_none
592
592
+
~enc:(fun i -> i.instance_description)
593
593
+
|> Jsont.Object.mem "terms" (Jsont.option Jsont.string)
594
594
+
~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun i -> i.instance_terms)
595
595
+
|> Jsont.Object.mem "isNSFW" Jsont.bool ~dec_absent:false
596
596
+
~enc:(fun i -> i.instance_is_nsfw)
597
597
+
|> Jsont.Object.mem "defaultNSFWPolicy" Jsont.string
598
598
+
~dec_absent:"display" ~enc:(fun i -> i.instance_default_nsfw_policy)
599
599
+
|> Jsont.Object.mem "defaultClientRoute" Jsont.string
600
600
+
~dec_absent:"/videos/trending" ~enc:(fun i -> i.instance_default_client_route)
601
601
+
|> Jsont.Object.skip_unknown
602
602
+
|> Jsont.Object.finish
603
603
+
604
604
+
let signup_jsont =
605
605
+
let make allowed allowed_for_current_ip requires_email_verification =
606
606
+
(allowed, allowed_for_current_ip, requires_email_verification)
607
607
+
in
608
608
+
Jsont.Object.map ~kind:"signup" make
609
609
+
|> Jsont.Object.mem "allowed" Jsont.bool ~dec_absent:false
610
610
+
~enc:(fun (a, _, _) -> a)
611
611
+
|> Jsont.Object.mem "allowedForCurrentIP" Jsont.bool ~dec_absent:false
612
612
+
~enc:(fun (_, a, _) -> a)
613
613
+
|> Jsont.Object.mem "requiresEmailVerification" Jsont.bool ~dec_absent:false
614
614
+
~enc:(fun (_, _, r) -> r)
615
615
+
|> Jsont.Object.skip_unknown
616
616
+
|> Jsont.Object.finish
617
617
+
618
618
+
let transcoding_jsont =
619
619
+
Jsont.Object.map ~kind:"transcoding" Fun.id
620
620
+
|> Jsont.Object.mem "enabled" Jsont.bool ~dec_absent:false ~enc:Fun.id
621
621
+
|> Jsont.Object.skip_unknown
622
622
+
|> Jsont.Object.finish
623
623
+
624
624
+
let contact_form_jsont =
625
625
+
Jsont.Object.map ~kind:"contact_form" Fun.id
626
626
+
|> Jsont.Object.mem "enabled" Jsont.bool ~dec_absent:false ~enc:Fun.id
627
627
+
|> Jsont.Object.skip_unknown
628
628
+
|> Jsont.Object.finish
629
629
+
630
630
+
let server_config_jsont : server_config Jsont.t =
631
631
+
let make config_instance config_server_version config_server_commit signup
632
632
+
transcoding contact_form =
633
633
+
let config_signup_allowed, config_signup_allowed_for_current_ip,
634
634
+
config_signup_requires_email_verification = signup in
635
635
+
{ config_instance; config_server_version; config_server_commit;
636
636
+
config_signup_allowed; config_signup_allowed_for_current_ip;
637
637
+
config_signup_requires_email_verification;
638
638
+
config_transcoding_enabled = transcoding;
639
639
+
config_contact_form_enabled = contact_form }
640
640
+
in
641
641
+
Jsont.Object.map ~kind:"server_config" make
642
642
+
|> Jsont.Object.mem "instance" instance_info_jsont
643
643
+
~enc:(fun c -> c.config_instance)
644
644
+
|> Jsont.Object.mem "serverVersion" Jsont.string
645
645
+
~dec_absent:"unknown" ~enc:(fun c -> c.config_server_version)
646
646
+
|> Jsont.Object.mem "serverCommit" (Jsont.option Jsont.string)
647
647
+
~dec_absent:None ~enc_omit:Option.is_none
648
648
+
~enc:(fun c -> c.config_server_commit)
649
649
+
|> Jsont.Object.mem "signup" signup_jsont
650
650
+
~enc:(fun c ->
651
651
+
(c.config_signup_allowed, c.config_signup_allowed_for_current_ip,
652
652
+
c.config_signup_requires_email_verification))
653
653
+
|> Jsont.Object.mem "transcoding" transcoding_jsont
654
654
+
~enc:(fun c -> c.config_transcoding_enabled)
655
655
+
|> Jsont.Object.mem "contactForm" contact_form_jsont
656
656
+
~enc:(fun c -> c.config_contact_form_enabled)
657
657
+
|> Jsont.Object.skip_unknown
658
658
+
|> Jsont.Object.finish
659
659
+
660
660
+
let server_stats_jsont : server_stats Jsont.t =
661
661
+
let make stats_total_users stats_total_daily_active_users
662
662
+
stats_total_weekly_active_users stats_total_monthly_active_users
663
663
+
stats_total_local_videos stats_total_local_video_views
664
664
+
stats_total_local_video_comments stats_total_local_video_files_size
665
665
+
stats_total_videos stats_total_video_comments
666
666
+
stats_total_local_video_channels stats_total_local_playlists
667
667
+
stats_total_instance_followers stats_total_instance_following =
668
668
+
{ stats_total_users; stats_total_daily_active_users;
669
669
+
stats_total_weekly_active_users; stats_total_monthly_active_users;
670
670
+
stats_total_local_videos; stats_total_local_video_views;
671
671
+
stats_total_local_video_comments; stats_total_local_video_files_size;
672
672
+
stats_total_videos; stats_total_video_comments;
673
673
+
stats_total_local_video_channels; stats_total_local_playlists;
674
674
+
stats_total_instance_followers; stats_total_instance_following }
675
675
+
in
676
676
+
Jsont.Object.map ~kind:"server_stats" make
677
677
+
|> Jsont.Object.mem "totalUsers" Jsont.int ~dec_absent:0
678
678
+
~enc:(fun s -> s.stats_total_users)
679
679
+
|> Jsont.Object.mem "totalDailyActiveUsers" Jsont.int ~dec_absent:0
680
680
+
~enc:(fun s -> s.stats_total_daily_active_users)
681
681
+
|> Jsont.Object.mem "totalWeeklyActiveUsers" Jsont.int ~dec_absent:0
682
682
+
~enc:(fun s -> s.stats_total_weekly_active_users)
683
683
+
|> Jsont.Object.mem "totalMonthlyActiveUsers" Jsont.int ~dec_absent:0
684
684
+
~enc:(fun s -> s.stats_total_monthly_active_users)
685
685
+
|> Jsont.Object.mem "totalLocalVideos" Jsont.int ~dec_absent:0
686
686
+
~enc:(fun s -> s.stats_total_local_videos)
687
687
+
|> Jsont.Object.mem "totalLocalVideoViews" Jsont.int ~dec_absent:0
688
688
+
~enc:(fun s -> s.stats_total_local_video_views)
689
689
+
|> Jsont.Object.mem "totalLocalVideoComments" Jsont.int ~dec_absent:0
690
690
+
~enc:(fun s -> s.stats_total_local_video_comments)
691
691
+
|> Jsont.Object.mem "totalLocalVideoFilesSize" Jsont.int64 ~dec_absent:0L
692
692
+
~enc:(fun s -> s.stats_total_local_video_files_size)
693
693
+
|> Jsont.Object.mem "totalVideos" Jsont.int ~dec_absent:0
694
694
+
~enc:(fun s -> s.stats_total_videos)
695
695
+
|> Jsont.Object.mem "totalVideoComments" Jsont.int ~dec_absent:0
696
696
+
~enc:(fun s -> s.stats_total_video_comments)
697
697
+
|> Jsont.Object.mem "totalLocalVideoChannels" Jsont.int ~dec_absent:0
698
698
+
~enc:(fun s -> s.stats_total_local_video_channels)
699
699
+
|> Jsont.Object.mem "totalLocalPlaylists" Jsont.int ~dec_absent:0
700
700
+
~enc:(fun s -> s.stats_total_local_playlists)
701
701
+
|> Jsont.Object.mem "totalInstanceFollowers" Jsont.int ~dec_absent:0
702
702
+
~enc:(fun s -> s.stats_total_instance_followers)
703
703
+
|> Jsont.Object.mem "totalInstanceFollowing" Jsont.int ~dec_absent:0
704
704
+
~enc:(fun s -> s.stats_total_instance_following)
705
705
+
|> Jsont.Object.skip_unknown
706
706
+
|> Jsont.Object.finish
707
707
+
708
708
+
(* HTTP helpers *)
709
709
+
710
710
+
let get_json ~session ~url codec =
711
711
+
Log.debug (fun m -> m "GET %s" url);
712
712
+
let response = Requests.get session url in
713
713
+
let status = Requests.Response.status_code response in
714
714
+
if Requests.Response.ok response then begin
715
715
+
Log.debug (fun m -> m "Response: %d OK" status);
716
716
+
let body = Requests.Response.text response in
717
717
+
match Jsont_bytesrw.decode_string codec body with
718
718
+
| Ok result -> result
719
719
+
| Error e ->
720
720
+
Log.err (fun m -> m "JSON parse error: %s" e);
721
721
+
failwith (Fmt.str "JSON parse error: %s" e)
722
722
+
end
723
723
+
else begin
724
724
+
let body = Requests.Response.text response in
725
725
+
raise_api_error status body
726
726
+
end
727
727
+
728
728
+
let build_url base parts params =
729
729
+
let path = String.concat "/" parts in
730
730
+
let params = List.filter_map Fun.id params in
731
731
+
let query =
732
732
+
if params = [] then ""
733
733
+
else "?" ^ String.concat "&" (List.map (fun (k, v) -> k ^ "=" ^ v) params)
734
734
+
in
735
735
+
base ^ "/" ^ path ^ query
736
736
+
737
737
+
(* Video operations *)
738
738
+
739
739
+
let list_videos ?(count = 20) ?(start = 0) ?(sort = Newest) ?(nsfw = false)
740
740
+
?is_local ?is_live ?category_id ?tags ~session ~base_url () =
741
741
+
let url =
742
742
+
build_url base_url [ "api"; "v1"; "videos" ]
743
743
+
[
744
744
+
Some ("count", string_of_int count);
745
745
+
Some ("start", string_of_int start);
746
746
+
Some ("sort", string_of_video_sort sort);
747
747
+
Some ("nsfw", if nsfw then "true" else "false");
748
748
+
Option.map (fun b -> ("isLocal", if b then "true" else "false")) is_local;
749
749
+
Option.map (fun b -> ("isLive", if b then "true" else "false")) is_live;
750
750
+
Option.map (fun i -> ("categoryOneOf", string_of_int i)) category_id;
751
751
+
Option.map (fun t -> ("tagsOneOf", t)) tags;
752
752
+
]
753
753
+
in
754
754
+
let result = get_json ~session ~url video_paginated_jsont in
755
755
+
Log.info (fun m ->
756
756
+
m "Listed %d videos (total: %d)" (List.length result.data) result.total);
757
757
+
result
758
758
+
759
759
+
let search_videos ~query ?(count = 20) ?(start = 0) ?(sort = Best)
760
760
+
?(search_target = `Local) ?duration_min ?duration_max ?published_after
761
761
+
?published_before ~session ~base_url () =
762
762
+
let url =
763
763
+
build_url base_url [ "api"; "v1"; "search"; "videos" ]
764
764
+
[
765
765
+
Some ("search", query);
766
766
+
Some ("count", string_of_int count);
767
767
+
Some ("start", string_of_int start);
768
768
+
Some ("sort", string_of_video_sort sort);
769
769
+
Some
770
770
+
( "searchTarget",
771
771
+
match search_target with `Local -> "local" | `Search_index -> "search-index" );
772
772
+
Option.map (fun d -> ("durationMin", string_of_int d)) duration_min;
773
773
+
Option.map (fun d -> ("durationMax", string_of_int d)) duration_max;
774
774
+
Option.map (fun t -> ("startDate", Ptime.to_rfc3339 t)) published_after;
775
775
+
Option.map (fun t -> ("endDate", Ptime.to_rfc3339 t)) published_before;
776
776
+
]
777
777
+
in
778
778
+
let result = get_json ~session ~url video_paginated_jsont in
779
779
+
Log.info (fun m ->
780
780
+
m "Search '%s' found %d videos (total: %d)" query
781
781
+
(List.length result.data) result.total);
782
782
+
result
783
783
+
784
784
+
let fetch_channel_videos ?(count = 20) ?(start = 0) ~session ~base_url ~channel
785
785
+
() =
786
786
+
let url =
787
787
+
build_url base_url [ "api"; "v1"; "video-channels"; channel; "videos" ]
788
788
+
[
789
789
+
Some ("count", string_of_int count);
790
790
+
Some ("start", string_of_int start);
791
791
+
]
792
792
+
in
793
793
+
let result = get_json ~session ~url video_paginated_jsont in
794
794
+
Log.info (fun m ->
795
795
+
m "Fetched %d videos from channel %s (total: %d)"
796
796
+
(List.length result.data) channel result.total);
797
797
+
result
798
798
+
799
799
+
let fetch_all_channel_videos ?(page_size = 20) ?max_pages ~session ~base_url
800
800
+
~channel () =
801
801
+
Log.info (fun m ->
802
802
+
m "Fetching all videos from channel %s (page_size=%d, max_pages=%s)"
803
803
+
channel page_size
804
804
+
(match max_pages with None -> "unlimited" | Some n -> string_of_int n));
805
805
+
let rec fetch_pages page start acc =
806
806
+
let response =
807
807
+
fetch_channel_videos ~count:page_size ~start ~session ~base_url ~channel
808
808
+
()
809
809
+
in
810
810
+
let all_videos = acc @ response.data in
811
811
+
let fetched_count = start + List.length response.data in
812
812
+
let more_available = fetched_count < response.total in
813
813
+
let under_max_pages =
814
814
+
match max_pages with None -> true | Some max -> page < max
815
815
+
in
816
816
+
Log.debug (fun m ->
817
817
+
m "Page %d: fetched %d, total so far %d/%d" page
818
818
+
(List.length response.data) fetched_count response.total);
819
819
+
if more_available && under_max_pages then
820
820
+
fetch_pages (page + 1) fetched_count all_videos
821
821
+
else begin
822
822
+
Log.info (fun m ->
823
823
+
m "Finished fetching %d videos in %d pages" (List.length all_videos)
824
824
+
page);
825
825
+
all_videos
826
826
+
end
827
827
+
in
828
828
+
fetch_pages 1 0 []
829
829
+
830
830
+
let fetch_video_details ~session ~base_url ~uuid () =
831
831
+
let url = build_url base_url [ "api"; "v1"; "videos"; uuid ] [] in
832
832
+
let video = get_json ~session ~url video_jsont in
833
833
+
Log.info (fun m -> m "Fetched video details: %s (%s)" video.name uuid);
834
834
+
video
835
835
+
836
836
+
(* Categories/languages/licences lookup - returns key-value pairs *)
837
837
+
838
838
+
module StringMap = Map.Make (String)
839
839
+
840
840
+
let dict_jsont (key_codec : 'a Jsont.t) : ('a * string) list Jsont.t =
841
841
+
let map_jsont = Jsont.(Object.as_string_map string) in
842
842
+
Jsont.map ~kind:"dict"
843
843
+
~dec:(fun smap ->
844
844
+
let bindings = StringMap.bindings smap in
845
845
+
List.filter_map
846
846
+
(fun (k, v) ->
847
847
+
match Jsont_bytesrw.decode_string key_codec ("\"" ^ k ^ "\"") with
848
848
+
| Ok key -> Some (key, v)
849
849
+
| Error _ -> None)
850
850
+
bindings)
851
851
+
~enc:(fun pairs ->
852
852
+
List.fold_left
853
853
+
(fun m (k, v) ->
854
854
+
match Jsont_bytesrw.encode_string key_codec k with
855
855
+
| Ok s ->
856
856
+
let key = String.sub s 1 (String.length s - 2) in
857
857
+
StringMap.add key v m
858
858
+
| Error _ -> m)
859
859
+
StringMap.empty pairs)
860
860
+
map_jsont
861
861
+
862
862
+
let get_categories ~session ~base_url () =
863
863
+
let url = build_url base_url [ "api"; "v1"; "videos"; "categories" ] [] in
864
864
+
let result = get_json ~session ~url (dict_jsont Jsont.int) in
865
865
+
Log.info (fun m -> m "Fetched %d categories" (List.length result));
866
866
+
result
867
867
+
868
868
+
let get_licences ~session ~base_url () =
869
869
+
let url = build_url base_url [ "api"; "v1"; "videos"; "licences" ] [] in
870
870
+
let result = get_json ~session ~url (dict_jsont Jsont.int) in
871
871
+
Log.info (fun m -> m "Fetched %d licences" (List.length result));
872
872
+
result
873
873
+
874
874
+
let get_languages ~session ~base_url () =
875
875
+
let url = build_url base_url [ "api"; "v1"; "videos"; "languages" ] [] in
876
876
+
let result = get_json ~session ~url (dict_jsont Jsont.string) in
877
877
+
Log.info (fun m -> m "Fetched %d languages" (List.length result));
878
878
+
result
879
879
+
880
880
+
(* Channel operations *)
881
881
+
882
882
+
let list_channels ?(count = 20) ?(start = 0) ?(sort = "-createdAt") ~session
883
883
+
~base_url () =
884
884
+
let url =
885
885
+
build_url base_url [ "api"; "v1"; "video-channels" ]
886
886
+
[
887
887
+
Some ("count", string_of_int count);
888
888
+
Some ("start", string_of_int start);
889
889
+
Some ("sort", sort);
890
890
+
]
891
891
+
in
892
892
+
let result = get_json ~session ~url channel_paginated_jsont in
893
893
+
Log.info (fun m ->
894
894
+
m "Listed %d channels (total: %d)" (List.length result.data) result.total);
895
895
+
result
896
896
+
897
897
+
let search_channels ~query ?(count = 20) ?(start = 0) ~session ~base_url () =
898
898
+
let url =
899
899
+
build_url base_url [ "api"; "v1"; "search"; "video-channels" ]
900
900
+
[
901
901
+
Some ("search", query);
902
902
+
Some ("count", string_of_int count);
903
903
+
Some ("start", string_of_int start);
904
904
+
]
905
905
+
in
906
906
+
let result = get_json ~session ~url channel_paginated_jsont in
907
907
+
Log.info (fun m ->
908
908
+
m "Search '%s' found %d channels (total: %d)" query
909
909
+
(List.length result.data) result.total);
910
910
+
result
911
911
+
912
912
+
let get_channel ~session ~base_url ~handle () =
913
913
+
let url = build_url base_url [ "api"; "v1"; "video-channels"; handle ] [] in
914
914
+
let channel = get_json ~session ~url channel_jsont in
915
915
+
Log.info (fun m ->
916
916
+
m "Fetched channel: %s" channel.channel.channel_display_name);
917
917
+
channel
918
918
+
919
919
+
(* Account operations *)
920
920
+
921
921
+
let list_accounts ?(count = 20) ?(start = 0) ?(sort = "-createdAt") ~session
922
922
+
~base_url () =
923
923
+
let url =
924
924
+
build_url base_url [ "api"; "v1"; "accounts" ]
925
925
+
[
926
926
+
Some ("count", string_of_int count);
927
927
+
Some ("start", string_of_int start);
928
928
+
Some ("sort", sort);
929
929
+
]
930
930
+
in
931
931
+
let result = get_json ~session ~url account_paginated_jsont in
932
932
+
Log.info (fun m ->
933
933
+
m "Listed %d accounts (total: %d)" (List.length result.data) result.total);
934
934
+
result
935
935
+
936
936
+
let get_account ~session ~base_url ~handle () =
937
937
+
let url = build_url base_url [ "api"; "v1"; "accounts"; handle ] [] in
938
938
+
let account = get_json ~session ~url account_jsont in
939
939
+
Log.info (fun m ->
940
940
+
m "Fetched account: %s" account.account.account_display_name);
941
941
+
account
942
942
+
943
943
+
let get_account_videos ?(count = 20) ?(start = 0) ~session ~base_url ~handle ()
944
944
+
=
945
945
+
let url =
946
946
+
build_url base_url [ "api"; "v1"; "accounts"; handle; "videos" ]
947
947
+
[
948
948
+
Some ("count", string_of_int count);
949
949
+
Some ("start", string_of_int start);
950
950
+
]
951
951
+
in
952
952
+
let result = get_json ~session ~url video_paginated_jsont in
953
953
+
Log.info (fun m ->
954
954
+
m "Fetched %d videos from account %s (total: %d)"
955
955
+
(List.length result.data) handle result.total);
956
956
+
result
957
957
+
958
958
+
let get_account_channels ?(count = 20) ?(start = 0) ~session ~base_url ~handle
959
959
+
() =
960
960
+
let url =
961
961
+
build_url base_url [ "api"; "v1"; "accounts"; handle; "video-channels" ]
962
962
+
[
963
963
+
Some ("count", string_of_int count);
964
964
+
Some ("start", string_of_int start);
965
965
+
]
966
966
+
in
967
967
+
let result = get_json ~session ~url channel_paginated_jsont in
968
968
+
Log.info (fun m ->
969
969
+
m "Fetched %d channels from account %s (total: %d)"
970
970
+
(List.length result.data) handle result.total);
971
971
+
result
972
972
+
973
973
+
(* Playlist operations *)
974
974
+
975
975
+
let list_playlists ?(count = 20) ?(start = 0) ~session ~base_url () =
976
976
+
let url =
977
977
+
build_url base_url [ "api"; "v1"; "video-playlists" ]
978
978
+
[
979
979
+
Some ("count", string_of_int count);
980
980
+
Some ("start", string_of_int start);
981
981
+
]
982
982
+
in
983
983
+
let result = get_json ~session ~url playlist_paginated_jsont in
984
984
+
Log.info (fun m ->
985
985
+
m "Listed %d playlists (total: %d)" (List.length result.data) result.total);
986
986
+
result
987
987
+
988
988
+
let search_playlists ~query ?(count = 20) ?(start = 0) ~session ~base_url () =
989
989
+
let url =
990
990
+
build_url base_url [ "api"; "v1"; "search"; "video-playlists" ]
991
991
+
[
992
992
+
Some ("search", query);
993
993
+
Some ("count", string_of_int count);
994
994
+
Some ("start", string_of_int start);
995
995
+
]
996
996
+
in
997
997
+
let result = get_json ~session ~url playlist_paginated_jsont in
998
998
+
Log.info (fun m ->
999
999
+
m "Search '%s' found %d playlists (total: %d)" query
1000
1000
+
(List.length result.data) result.total);
1001
1001
+
result
1002
1002
+
1003
1003
+
let get_playlist ~session ~base_url ~id () =
1004
1004
+
let url = build_url base_url [ "api"; "v1"; "video-playlists"; id ] [] in
1005
1005
+
let playlist = get_json ~session ~url playlist_jsont in
1006
1006
+
Log.info (fun m -> m "Fetched playlist: %s" playlist.playlist_display_name);
1007
1007
+
playlist
1008
1008
+
1009
1009
+
let get_playlist_videos ?(count = 20) ?(start = 0) ~session ~base_url ~id () =
1010
1010
+
let url =
1011
1011
+
build_url base_url [ "api"; "v1"; "video-playlists"; id; "videos" ]
1012
1012
+
[
1013
1013
+
Some ("count", string_of_int count);
1014
1014
+
Some ("start", string_of_int start);
1015
1015
+
]
1016
1016
+
in
1017
1017
+
let result = get_json ~session ~url playlist_element_paginated_jsont in
1018
1018
+
Log.info (fun m ->
1019
1019
+
m "Fetched %d videos from playlist (total: %d)" (List.length result.data)
1020
1020
+
result.total);
1021
1021
+
result
1022
1022
+
1023
1023
+
let get_account_playlists ?(count = 20) ?(start = 0) ~session ~base_url ~handle
1024
1024
+
() =
1025
1025
+
let url =
1026
1026
+
build_url base_url [ "api"; "v1"; "accounts"; handle; "video-playlists" ]
1027
1027
+
[
1028
1028
+
Some ("count", string_of_int count);
1029
1029
+
Some ("start", string_of_int start);
1030
1030
+
]
1031
1031
+
in
1032
1032
+
let result = get_json ~session ~url playlist_paginated_jsont in
1033
1033
+
Log.info (fun m ->
1034
1034
+
m "Fetched %d playlists from account %s (total: %d)"
1035
1035
+
(List.length result.data) handle result.total);
1036
1036
+
result
1037
1037
+
1038
1038
+
(* Server operations *)
1039
1039
+
1040
1040
+
let get_config ~session ~base_url () =
1041
1041
+
let url = build_url base_url [ "api"; "v1"; "config" ] [] in
1042
1042
+
let config = get_json ~session ~url server_config_jsont in
1043
1043
+
Log.info (fun m ->
1044
1044
+
m "Fetched config for: %s (v%s)" config.config_instance.instance_name
1045
1045
+
config.config_server_version);
1046
1046
+
config
1047
1047
+
1048
1048
+
let get_stats ~session ~base_url () =
1049
1049
+
let url = build_url base_url [ "api"; "v1"; "server"; "stats" ] [] in
1050
1050
+
let stats = get_json ~session ~url server_stats_jsont in
1051
1051
+
Log.info (fun m ->
1052
1052
+
m "Fetched stats: %d users, %d videos" stats.stats_total_users
1053
1053
+
stats.stats_total_videos);
1054
1054
+
stats
1055
1055
+
1056
1056
+
(* Utilities *)
1057
1057
+
1058
1058
+
let thumbnail_url ~base_url video =
1059
1059
+
match video.thumbnail_path with
1060
1060
+
| Some path -> Some (base_url ^ path)
1061
1061
+
| None -> None
1062
1062
+
1063
1063
+
let download_thumbnail ~session ~base_url ~video ~output_path () =
1064
1064
+
match thumbnail_url ~base_url video with
1065
1065
+
| None ->
1066
1066
+
Log.warn (fun m -> m "No thumbnail available for video %s" video.uuid);
1067
1067
+
Error
1068
1068
+
(`Msg (Printf.sprintf "No thumbnail available for video %s" video.uuid))
1069
1069
+
| Some url ->
1070
1070
+
Log.debug (fun m -> m "Downloading thumbnail from %s" url);
1071
1071
+
let response = Requests.get session url in
1072
1072
+
let status = Requests.Response.status_code response in
1073
1073
+
if Requests.Response.ok response then begin
1074
1074
+
let body = Requests.Response.text response in
1075
1075
+
try
1076
1076
+
let oc = open_out_bin output_path in
1077
1077
+
output_string oc body;
1078
1078
+
close_out oc;
1079
1079
+
Log.info (fun m ->
1080
1080
+
m "Downloaded thumbnail for %s to %s" video.uuid output_path);
1081
1081
+
Ok ()
1082
1082
+
with exn ->
1083
1083
+
Log.err (fun m ->
1084
1084
+
m "Failed to write thumbnail to %s: %s" output_path
1085
1085
+
(Printexc.to_string exn));
1086
1086
+
Error
1087
1087
+
(`Msg
1088
1088
+
(Printf.sprintf "Failed to write thumbnail: %s"
1089
1089
+
(Printexc.to_string exn)))
1090
1090
+
end
1091
1091
+
else begin
1092
1092
+
Log.err (fun m ->
1093
1093
+
m "HTTP error %d downloading thumbnail from %s" status url);
1094
1094
+
Error
1095
1095
+
(`Msg (Printf.sprintf "HTTP error downloading thumbnail: %d" status))
1096
1096
+
end
1097
1097
+
1098
1098
+
let to_tuple video =
1099
1099
+
let description = Option.value ~default:"" video.description in
1100
1100
+
let published_date =
1101
1101
+
Option.value ~default:video.published_at video.originally_published_at
1102
1102
+
in
1103
1103
+
( description,
1104
1104
+
published_date,
1105
1105
+
video.name,
1106
1106
+
video.url,
1107
1107
+
video.uuid,
1108
1108
+
string_of_int video.id )
+477
lib/peertube.mli
···
1
1
+
(** PeerTube API client for OCaml using Eio.
2
2
+
3
3
+
This library provides a typed client for the PeerTube video platform API,
4
4
+
using Eio for effect-based I/O and jsont for JSON serialization. *)
5
5
+
6
6
+
(** {1 Logging} *)
7
7
+
8
8
+
(** Log source for the PeerTube client. *)
9
9
+
val log_src : Logs.src
10
10
+
11
11
+
(** {1 Errors} *)
12
12
+
13
13
+
(** API error with HTTP status code and message. *)
14
14
+
exception Api_error of int * string
15
15
+
16
16
+
(** {1 Common Types} *)
17
17
+
18
18
+
(** A labeled value with numeric ID (used for categories, licences, languages). *)
19
19
+
type 'a labeled = {
20
20
+
id : 'a;
21
21
+
label : string;
22
22
+
}
23
23
+
24
24
+
(** Video privacy levels. *)
25
25
+
type privacy = Public | Unlisted | Private | Internal
26
26
+
27
27
+
(** Video sort options. *)
28
28
+
type video_sort =
29
29
+
| Newest
30
30
+
| Oldest
31
31
+
| Views
32
32
+
| Likes
33
33
+
| Trending
34
34
+
| Hot
35
35
+
| Random
36
36
+
| Best
37
37
+
38
38
+
(** {1 Account Types} *)
39
39
+
40
40
+
(** Summary of an account (embedded in other responses). *)
41
41
+
type account_summary = {
42
42
+
account_id : int;
43
43
+
account_name : string;
44
44
+
account_display_name : string;
45
45
+
account_url : string;
46
46
+
account_host : string;
47
47
+
account_avatar_path : string option;
48
48
+
}
49
49
+
50
50
+
(** Full account details. *)
51
51
+
type account = {
52
52
+
account : account_summary;
53
53
+
account_description : string option;
54
54
+
account_created_at : Ptime.t;
55
55
+
account_followers_count : int;
56
56
+
account_following_count : int;
57
57
+
account_following_hosts_count : int option;
58
58
+
}
59
59
+
60
60
+
(** {1 Channel Types} *)
61
61
+
62
62
+
(** Summary of a channel (embedded in other responses). *)
63
63
+
type channel_summary = {
64
64
+
channel_id : int;
65
65
+
channel_name : string;
66
66
+
channel_display_name : string;
67
67
+
channel_url : string;
68
68
+
channel_host : string;
69
69
+
channel_avatar_path : string option;
70
70
+
}
71
71
+
72
72
+
(** Full channel details. *)
73
73
+
type channel = {
74
74
+
channel : channel_summary;
75
75
+
channel_description : string option;
76
76
+
channel_support : string option;
77
77
+
channel_created_at : Ptime.t;
78
78
+
channel_followers_count : int;
79
79
+
channel_following_count : int;
80
80
+
channel_banner_path : string option;
81
81
+
channel_owner_account : account_summary option;
82
82
+
}
83
83
+
84
84
+
(** {1 Video Types} *)
85
85
+
86
86
+
(** A PeerTube video record. *)
87
87
+
type video = {
88
88
+
id : int;
89
89
+
uuid : string;
90
90
+
short_uuid : string option;
91
91
+
name : string;
92
92
+
description : string option;
93
93
+
url : string;
94
94
+
embed_path : string;
95
95
+
published_at : Ptime.t;
96
96
+
originally_published_at : Ptime.t option;
97
97
+
updated_at : Ptime.t option;
98
98
+
thumbnail_path : string option;
99
99
+
preview_path : string option;
100
100
+
tags : string list;
101
101
+
duration : int;
102
102
+
views : int;
103
103
+
likes : int;
104
104
+
dislikes : int;
105
105
+
is_local : bool;
106
106
+
is_live : bool;
107
107
+
privacy : privacy;
108
108
+
category : int labeled option;
109
109
+
licence : int labeled option;
110
110
+
language : string labeled option;
111
111
+
channel : channel_summary option;
112
112
+
account : account_summary option;
113
113
+
}
114
114
+
115
115
+
(** A paginated response. *)
116
116
+
type 'a paginated = {
117
117
+
total : int;
118
118
+
data : 'a list;
119
119
+
}
120
120
+
121
121
+
(** {1 Playlist Types} *)
122
122
+
123
123
+
(** Playlist privacy levels. *)
124
124
+
type playlist_privacy = PlaylistPublic | PlaylistUnlisted | PlaylistPrivate
125
125
+
126
126
+
(** Playlist type. *)
127
127
+
type playlist_type = Regular | WatchLater
128
128
+
129
129
+
(** A video playlist. *)
130
130
+
type playlist = {
131
131
+
playlist_id : int;
132
132
+
playlist_uuid : string;
133
133
+
playlist_short_uuid : string option;
134
134
+
playlist_display_name : string;
135
135
+
playlist_description : string option;
136
136
+
playlist_privacy : playlist_privacy;
137
137
+
playlist_url : string;
138
138
+
playlist_thumbnail_path : string option;
139
139
+
playlist_videos_length : int;
140
140
+
playlist_type : playlist_type;
141
141
+
playlist_created_at : Ptime.t;
142
142
+
playlist_updated_at : Ptime.t;
143
143
+
playlist_owner_account : account_summary option;
144
144
+
playlist_video_channel : channel_summary option;
145
145
+
}
146
146
+
147
147
+
(** An element in a playlist. *)
148
148
+
type playlist_element = {
149
149
+
element_id : int;
150
150
+
element_position : int;
151
151
+
element_start_timestamp : int option;
152
152
+
element_stop_timestamp : int option;
153
153
+
element_video : video option;
154
154
+
}
155
155
+
156
156
+
(** {1 Server Types} *)
157
157
+
158
158
+
(** Basic server/instance information. *)
159
159
+
type instance_info = {
160
160
+
instance_name : string;
161
161
+
instance_short_description : string;
162
162
+
instance_description : string option;
163
163
+
instance_terms : string option;
164
164
+
instance_is_nsfw : bool;
165
165
+
instance_default_nsfw_policy : string;
166
166
+
instance_default_client_route : string;
167
167
+
}
168
168
+
169
169
+
(** Server configuration. *)
170
170
+
type server_config = {
171
171
+
config_instance : instance_info;
172
172
+
config_server_version : string;
173
173
+
config_server_commit : string option;
174
174
+
config_signup_allowed : bool;
175
175
+
config_signup_allowed_for_current_ip : bool;
176
176
+
config_signup_requires_email_verification : bool;
177
177
+
config_transcoding_enabled : bool;
178
178
+
config_contact_form_enabled : bool;
179
179
+
}
180
180
+
181
181
+
(** Server statistics. *)
182
182
+
type server_stats = {
183
183
+
stats_total_users : int;
184
184
+
stats_total_daily_active_users : int;
185
185
+
stats_total_weekly_active_users : int;
186
186
+
stats_total_monthly_active_users : int;
187
187
+
stats_total_local_videos : int;
188
188
+
stats_total_local_video_views : int;
189
189
+
stats_total_local_video_comments : int;
190
190
+
stats_total_local_video_files_size : int64;
191
191
+
stats_total_videos : int;
192
192
+
stats_total_video_comments : int;
193
193
+
stats_total_local_video_channels : int;
194
194
+
stats_total_local_playlists : int;
195
195
+
stats_total_instance_followers : int;
196
196
+
stats_total_instance_following : int;
197
197
+
}
198
198
+
199
199
+
(** {1 JSON Codecs} *)
200
200
+
201
201
+
val video_jsont : video Jsont.t
202
202
+
val video_paginated_jsont : video paginated Jsont.t
203
203
+
val channel_jsont : channel Jsont.t
204
204
+
val channel_summary_jsont : channel_summary Jsont.t
205
205
+
val channel_paginated_jsont : channel paginated Jsont.t
206
206
+
val account_jsont : account Jsont.t
207
207
+
val account_summary_jsont : account_summary Jsont.t
208
208
+
val account_paginated_jsont : account paginated Jsont.t
209
209
+
val playlist_jsont : playlist Jsont.t
210
210
+
val playlist_paginated_jsont : playlist paginated Jsont.t
211
211
+
val playlist_element_jsont : playlist_element Jsont.t
212
212
+
val playlist_element_paginated_jsont : playlist_element paginated Jsont.t
213
213
+
val server_config_jsont : server_config Jsont.t
214
214
+
val server_stats_jsont : server_stats Jsont.t
215
215
+
216
216
+
(** {1 Video Operations} *)
217
217
+
218
218
+
(** List videos with optional filtering and sorting.
219
219
+
220
220
+
@param count Number of videos per page (default: 20)
221
221
+
@param start Starting offset for pagination (default: 0)
222
222
+
@param sort Sort order (default: Newest)
223
223
+
@param nsfw Include NSFW videos (default: false)
224
224
+
@param is_local Only local videos (default: None = both)
225
225
+
@param is_live Only live videos (default: None = both)
226
226
+
@param category_id Filter by category ID
227
227
+
@param tags Filter by tags (comma-separated) *)
228
228
+
val list_videos :
229
229
+
?count:int ->
230
230
+
?start:int ->
231
231
+
?sort:video_sort ->
232
232
+
?nsfw:bool ->
233
233
+
?is_local:bool ->
234
234
+
?is_live:bool ->
235
235
+
?category_id:int ->
236
236
+
?tags:string ->
237
237
+
session:Requests.t ->
238
238
+
base_url:string ->
239
239
+
unit ->
240
240
+
video paginated
241
241
+
242
242
+
(** Search for videos.
243
243
+
244
244
+
@param query Search query string
245
245
+
@param count Number of results per page (default: 20)
246
246
+
@param start Starting offset (default: 0)
247
247
+
@param sort Sort order (default: Best for search)
248
248
+
@param search_target Search locally or everywhere (default: local)
249
249
+
@param duration_min Minimum duration in seconds
250
250
+
@param duration_max Maximum duration in seconds
251
251
+
@param published_after Only videos published after this date
252
252
+
@param published_before Only videos published before this date *)
253
253
+
val search_videos :
254
254
+
query:string ->
255
255
+
?count:int ->
256
256
+
?start:int ->
257
257
+
?sort:video_sort ->
258
258
+
?search_target:[ `Local | `Search_index ] ->
259
259
+
?duration_min:int ->
260
260
+
?duration_max:int ->
261
261
+
?published_after:Ptime.t ->
262
262
+
?published_before:Ptime.t ->
263
263
+
session:Requests.t ->
264
264
+
base_url:string ->
265
265
+
unit ->
266
266
+
video paginated
267
267
+
268
268
+
(** Fetch videos from a channel with pagination. *)
269
269
+
val fetch_channel_videos :
270
270
+
?count:int ->
271
271
+
?start:int ->
272
272
+
session:Requests.t ->
273
273
+
base_url:string ->
274
274
+
channel:string ->
275
275
+
unit ->
276
276
+
video paginated
277
277
+
278
278
+
(** Fetch all videos from a channel using automatic pagination. *)
279
279
+
val fetch_all_channel_videos :
280
280
+
?page_size:int ->
281
281
+
?max_pages:int ->
282
282
+
session:Requests.t ->
283
283
+
base_url:string ->
284
284
+
channel:string ->
285
285
+
unit ->
286
286
+
video list
287
287
+
288
288
+
(** Fetch detailed information for a single video by UUID. *)
289
289
+
val fetch_video_details :
290
290
+
session:Requests.t ->
291
291
+
base_url:string ->
292
292
+
uuid:string ->
293
293
+
unit ->
294
294
+
video
295
295
+
296
296
+
(** Get available video categories. *)
297
297
+
val get_categories :
298
298
+
session:Requests.t ->
299
299
+
base_url:string ->
300
300
+
unit ->
301
301
+
(int * string) list
302
302
+
303
303
+
(** Get available video licences. *)
304
304
+
val get_licences :
305
305
+
session:Requests.t ->
306
306
+
base_url:string ->
307
307
+
unit ->
308
308
+
(int * string) list
309
309
+
310
310
+
(** Get available video languages. *)
311
311
+
val get_languages :
312
312
+
session:Requests.t ->
313
313
+
base_url:string ->
314
314
+
unit ->
315
315
+
(string * string) list
316
316
+
317
317
+
(** {1 Channel Operations} *)
318
318
+
319
319
+
(** List all channels on the instance.
320
320
+
321
321
+
@param count Number per page (default: 20)
322
322
+
@param start Starting offset (default: 0)
323
323
+
@param sort Sort order: createdAt, -createdAt, etc. *)
324
324
+
val list_channels :
325
325
+
?count:int ->
326
326
+
?start:int ->
327
327
+
?sort:string ->
328
328
+
session:Requests.t ->
329
329
+
base_url:string ->
330
330
+
unit ->
331
331
+
channel paginated
332
332
+
333
333
+
(** Search for channels.
334
334
+
335
335
+
@param query Search query string *)
336
336
+
val search_channels :
337
337
+
query:string ->
338
338
+
?count:int ->
339
339
+
?start:int ->
340
340
+
session:Requests.t ->
341
341
+
base_url:string ->
342
342
+
unit ->
343
343
+
channel paginated
344
344
+
345
345
+
(** Get details for a specific channel. *)
346
346
+
val get_channel :
347
347
+
session:Requests.t ->
348
348
+
base_url:string ->
349
349
+
handle:string ->
350
350
+
unit ->
351
351
+
channel
352
352
+
353
353
+
(** {1 Account Operations} *)
354
354
+
355
355
+
(** List accounts on the instance. *)
356
356
+
val list_accounts :
357
357
+
?count:int ->
358
358
+
?start:int ->
359
359
+
?sort:string ->
360
360
+
session:Requests.t ->
361
361
+
base_url:string ->
362
362
+
unit ->
363
363
+
account paginated
364
364
+
365
365
+
(** Get details for a specific account. *)
366
366
+
val get_account :
367
367
+
session:Requests.t ->
368
368
+
base_url:string ->
369
369
+
handle:string ->
370
370
+
unit ->
371
371
+
account
372
372
+
373
373
+
(** Get videos from a specific account. *)
374
374
+
val get_account_videos :
375
375
+
?count:int ->
376
376
+
?start:int ->
377
377
+
session:Requests.t ->
378
378
+
base_url:string ->
379
379
+
handle:string ->
380
380
+
unit ->
381
381
+
video paginated
382
382
+
383
383
+
(** Get channels owned by an account. *)
384
384
+
val get_account_channels :
385
385
+
?count:int ->
386
386
+
?start:int ->
387
387
+
session:Requests.t ->
388
388
+
base_url:string ->
389
389
+
handle:string ->
390
390
+
unit ->
391
391
+
channel paginated
392
392
+
393
393
+
(** {1 Playlist Operations} *)
394
394
+
395
395
+
(** List playlists on the instance. *)
396
396
+
val list_playlists :
397
397
+
?count:int ->
398
398
+
?start:int ->
399
399
+
session:Requests.t ->
400
400
+
base_url:string ->
401
401
+
unit ->
402
402
+
playlist paginated
403
403
+
404
404
+
(** Search for playlists. *)
405
405
+
val search_playlists :
406
406
+
query:string ->
407
407
+
?count:int ->
408
408
+
?start:int ->
409
409
+
session:Requests.t ->
410
410
+
base_url:string ->
411
411
+
unit ->
412
412
+
playlist paginated
413
413
+
414
414
+
(** Get details for a specific playlist. *)
415
415
+
val get_playlist :
416
416
+
session:Requests.t ->
417
417
+
base_url:string ->
418
418
+
id:string ->
419
419
+
unit ->
420
420
+
playlist
421
421
+
422
422
+
(** Get videos in a playlist. *)
423
423
+
val get_playlist_videos :
424
424
+
?count:int ->
425
425
+
?start:int ->
426
426
+
session:Requests.t ->
427
427
+
base_url:string ->
428
428
+
id:string ->
429
429
+
unit ->
430
430
+
playlist_element paginated
431
431
+
432
432
+
(** Get playlists owned by an account. *)
433
433
+
val get_account_playlists :
434
434
+
?count:int ->
435
435
+
?start:int ->
436
436
+
session:Requests.t ->
437
437
+
base_url:string ->
438
438
+
handle:string ->
439
439
+
unit ->
440
440
+
playlist paginated
441
441
+
442
442
+
(** {1 Server Operations} *)
443
443
+
444
444
+
(** Get server configuration. *)
445
445
+
val get_config :
446
446
+
session:Requests.t ->
447
447
+
base_url:string ->
448
448
+
unit ->
449
449
+
server_config
450
450
+
451
451
+
(** Get server statistics. *)
452
452
+
val get_stats :
453
453
+
session:Requests.t ->
454
454
+
base_url:string ->
455
455
+
unit ->
456
456
+
server_stats
457
457
+
458
458
+
(** {1 Utilities} *)
459
459
+
460
460
+
(** Get the full thumbnail URL for a video. *)
461
461
+
val thumbnail_url : base_url:string -> video -> string option
462
462
+
463
463
+
(** Download a video's thumbnail to a file. *)
464
464
+
val download_thumbnail :
465
465
+
session:Requests.t ->
466
466
+
base_url:string ->
467
467
+
video:video ->
468
468
+
output_path:string ->
469
469
+
unit ->
470
470
+
(unit, [ `Msg of string ]) result
471
471
+
472
472
+
(** Convert a video to a tuple for external use.
473
473
+
Returns [(description, published_date, title, url, uuid, id_string)]. *)
474
474
+
val to_tuple : video -> string * Ptime.t * string * string * string * string
475
475
+
476
476
+
(** Convert video_sort to API string. *)
477
477
+
val string_of_video_sort : video_sort -> string
+38
peertube.opam
···
1
1
+
# This file is generated by dune, edit dune-project instead
2
2
+
opam-version: "2.0"
3
3
+
synopsis: "PeerTube API client for OCaml using Eio"
4
4
+
description:
5
5
+
"An OCaml client library for the PeerTube video platform API, built on Eio for effect-based I/O"
6
6
+
maintainer: ["anil@recoil.org"]
7
7
+
authors: ["Anil Madhavapeddy"]
8
8
+
license: "ISC"
9
9
+
homepage: "https://github.com/avsm/ocaml-peertube"
10
10
+
bug-reports: "https://github.com/avsm/ocaml-peertube/issues"
11
11
+
depends: [
12
12
+
"dune" {>= "3.16"}
13
13
+
"ocaml" {>= "5.1.0"}
14
14
+
"eio" {>= "1.0"}
15
15
+
"eio_main" {>= "1.0"}
16
16
+
"requests" {>= "0.1"}
17
17
+
"jsont" {>= "0.1"}
18
18
+
"ptime" {>= "1.0"}
19
19
+
"fmt" {>= "0.9"}
20
20
+
"logs" {>= "0.7"}
21
21
+
"cmdliner" {>= "1.2"}
22
22
+
"odoc" {with-doc}
23
23
+
]
24
24
+
build: [
25
25
+
["dune" "subst"] {dev}
26
26
+
[
27
27
+
"dune"
28
28
+
"build"
29
29
+
"-p"
30
30
+
name
31
31
+
"-j"
32
32
+
jobs
33
33
+
"@install"
34
34
+
"@runtest" {with-test}
35
35
+
"@doc" {with-doc}
36
36
+
]
37
37
+
]
38
38
+
dev-repo: "git+https://github.com/avsm/ocaml-peertube.git"