My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Squashed 'ocaml-peertube/' content from commit 662db4f

git-subtree-dir: ocaml-peertube
git-subtree-split: 662db4f870464f031b04ec1a7773423940c85083

+4951
+38
.github/workflows/build.yml
··· 1 + name: Build 2 + 3 + on: 4 + push: 5 + branches: [ main, master ] 6 + pull_request: 7 + branches: [ main, master ] 8 + 9 + jobs: 10 + build: 11 + strategy: 12 + fail-fast: false 13 + matrix: 14 + os: 15 + - ubuntu-latest 16 + - macos-latest 17 + ocaml-compiler: 18 + - "5.2.0" 19 + 20 + runs-on: ${{ matrix.os }} 21 + 22 + steps: 23 + - name: Checkout code 24 + uses: actions/checkout@v4 25 + 26 + - name: Set up OCaml 27 + uses: ocaml/setup-ocaml@v3 28 + with: 29 + ocaml-compiler: ${{ matrix.ocaml-compiler }} 30 + 31 + - name: Install dependencies 32 + run: opam install . --deps-only --with-doc 33 + 34 + - name: Build 35 + run: opam exec -- dune build 36 + 37 + - name: Build documentation 38 + run: opam exec -- dune build @doc
+38
.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.byte 5 + *.native 6 + *.cmo 7 + *.cmi 8 + *.cma 9 + *.cmx 10 + *.cmxa 11 + *.cmxs 12 + *.o 13 + *.a 14 + 15 + # Dune 16 + _opam/ 17 + 18 + # Reference sources 19 + peertube-src/ 20 + 21 + # Editor files 22 + *.swp 23 + *.swo 24 + *~ 25 + .vscode/ 26 + .idea/ 27 + *.sublime-* 28 + 29 + # OS files 30 + .DS_Store 31 + Thumbs.db 32 + 33 + # Merlin (generated by dune) 34 + .merlin 35 + 36 + # Coverage 37 + _coverage/ 38 + *.coverage
+1
.ocamlformat
··· 1 + version = 0.28.1
+15
LICENSE.md
··· 1 + # ISC License 2 + 3 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org> 4 + 5 + Permission to use, copy, modify, and/or distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 + PERFORMANCE OF THIS SOFTWARE.
+310
PLAN.md
··· 1 + # PeerTube OCaml API Coverage Plan 2 + 3 + ## Current Implementation 4 + 5 + **Implemented (5 endpoints):** 6 + - `GET /api/v1/video-channels/{channel}/videos` - List channel videos with pagination 7 + - `GET /api/v1/videos/{uuid}` - Get video details 8 + - Thumbnail download (static files) 9 + 10 + **Types:** 11 + - `video` - Basic video metadata 12 + - `video_response` - Paginated response wrapper 13 + 14 + ## Proposed Expansion 15 + 16 + ### Phase 1: Enhanced Video Discovery (Priority: High) 17 + 18 + Add types and functions for browsing and searching videos. 19 + 20 + #### New Types 21 + ```ocaml 22 + type sort_order = 23 + | Newest | Oldest | Views | Likes | Trending | Hot | Random 24 + 25 + type video_filter = { 26 + category_id : int option; 27 + licence_id : int option; 28 + language : string option; 29 + nsfw : bool option; 30 + is_local : bool option; 31 + has_hls : bool option; 32 + has_webtorrent : bool option; 33 + skip_count : bool option; (* skip total count for performance *) 34 + } 35 + 36 + type search_params = { 37 + search : string; 38 + start : int; 39 + count : int; 40 + sort : sort_order; 41 + search_target : [`Local | `Everywhere] option; 42 + duration_min : int option; 43 + duration_max : int option; 44 + published_after : Ptime.t option; 45 + published_before : Ptime.t option; 46 + } 47 + ``` 48 + 49 + #### New Endpoints 50 + | Function | Endpoint | Description | 51 + |----------|----------|-------------| 52 + | `list_videos` | `GET /api/v1/videos` | Browse all videos with filters | 53 + | `search_videos` | `GET /api/v1/search/videos` | Full-text search | 54 + | `search_channels` | `GET /api/v1/search/video-channels` | Search channels | 55 + | `get_categories` | `GET /api/v1/videos/categories` | Video category list | 56 + | `get_languages` | `GET /api/v1/videos/languages` | Available languages | 57 + | `get_licences` | `GET /api/v1/videos/licences` | License types | 58 + 59 + ### Phase 2: Channels & Accounts (Priority: High) 60 + 61 + #### New Types 62 + ```ocaml 63 + type channel = { 64 + id : int; 65 + name : string; 66 + display_name : string; 67 + description : string option; 68 + url : string; 69 + host : string; 70 + followers_count : int; 71 + following_count : int; 72 + created_at : Ptime.t; 73 + banner_path : string option; 74 + avatar_path : string option; 75 + owner_account : account_summary option; 76 + } 77 + 78 + type account_summary = { 79 + id : int; 80 + name : string; 81 + display_name : string; 82 + url : string; 83 + host : string; 84 + } 85 + 86 + type account = { 87 + id : int; 88 + name : string; 89 + display_name : string; 90 + description : string option; 91 + url : string; 92 + host : string; 93 + followers_count : int; 94 + following_count : int; 95 + created_at : Ptime.t; 96 + avatar_path : string option; 97 + } 98 + ``` 99 + 100 + #### New Endpoints 101 + | Function | Endpoint | Description | 102 + |----------|----------|-------------| 103 + | `list_channels` | `GET /api/v1/video-channels` | Browse all channels | 104 + | `get_channel` | `GET /api/v1/video-channels/{handle}` | Channel details | 105 + | `list_accounts` | `GET /api/v1/accounts` | Browse accounts | 106 + | `get_account` | `GET /api/v1/accounts/{handle}` | Account details | 107 + | `get_account_videos` | `GET /api/v1/accounts/{handle}/videos` | Videos by account | 108 + | `get_account_channels` | `GET /api/v1/accounts/{handle}/video-channels` | Account's channels | 109 + 110 + ### Phase 3: Playlists (Priority: Medium) 111 + 112 + #### New Types 113 + ```ocaml 114 + type playlist_privacy = Public | Unlisted | Private 115 + 116 + type playlist = { 117 + id : int; 118 + uuid : string; 119 + display_name : string; 120 + description : string option; 121 + privacy : playlist_privacy; 122 + url : string; 123 + videos_length : int; 124 + thumbnail_path : string option; 125 + created_at : Ptime.t; 126 + updated_at : Ptime.t; 127 + owner_account : account_summary; 128 + video_channel : channel option; 129 + } 130 + 131 + type playlist_element = { 132 + id : int; 133 + position : int; 134 + start_timestamp : int option; 135 + stop_timestamp : int option; 136 + video : video; 137 + } 138 + ``` 139 + 140 + #### New Endpoints 141 + | Function | Endpoint | Description | 142 + |----------|----------|-------------| 143 + | `list_playlists` | `GET /api/v1/video-playlists` | Browse playlists | 144 + | `get_playlist` | `GET /api/v1/video-playlists/{id}` | Playlist details | 145 + | `get_playlist_videos` | `GET /api/v1/video-playlists/{id}/videos` | Videos in playlist | 146 + | `get_account_playlists` | `GET /api/v1/accounts/{handle}/video-playlists` | Account's playlists | 147 + 148 + ### Phase 4: Server Information (Priority: Medium) 149 + 150 + #### New Types 151 + ```ocaml 152 + type server_config = { 153 + instance_name : string; 154 + instance_short_description : string; 155 + instance_description : string; 156 + instance_terms : string; 157 + instance_default_nsfw_policy : string; 158 + signup_allowed : bool; 159 + signup_allowed_for_current_ip : bool; 160 + signup_requires_email_verification : bool; 161 + transcoding_enabled : bool; 162 + contact_form_enabled : bool; 163 + } 164 + 165 + type server_stats = { 166 + total_users : int; 167 + total_daily_active_users : int; 168 + total_weekly_active_users : int; 169 + total_monthly_active_users : int; 170 + total_local_videos : int; 171 + total_local_video_views : int; 172 + total_local_video_comments : int; 173 + total_local_video_files_size : int64; 174 + total_videos : int; 175 + total_video_comments : int; 176 + total_local_video_channels : int; 177 + total_local_playlists : int; 178 + total_instance_followers : int; 179 + total_instance_following : int; 180 + } 181 + ``` 182 + 183 + #### New Endpoints 184 + | Function | Endpoint | Description | 185 + |----------|----------|-------------| 186 + | `get_config` | `GET /api/v1/config` | Server configuration | 187 + | `get_about` | `GET /api/v1/config/about` | Instance about page | 188 + | `get_stats` | `GET /api/v1/server/stats` | Server statistics | 189 + 190 + ### Phase 5: Authentication (Priority: Low for read-only clients) 191 + 192 + #### New Types 193 + ```ocaml 194 + type oauth_client = { 195 + client_id : string; 196 + client_secret : string; 197 + } 198 + 199 + type auth_token = { 200 + access_token : string; 201 + token_type : string; 202 + expires_in : int; 203 + refresh_token : string; 204 + } 205 + 206 + type authenticated_session = { 207 + session : Requests.t; 208 + token : auth_token; 209 + expires_at : Ptime.t; 210 + } 211 + ``` 212 + 213 + #### New Functions 214 + | Function | Endpoint | Description | 215 + |----------|----------|-------------| 216 + | `get_oauth_client` | `GET /api/v1/oauth-clients/local` | Get OAuth credentials | 217 + | `login` | `POST /api/v1/users/token` | Authenticate user | 218 + | `refresh_token` | `POST /api/v1/users/token` | Refresh access token | 219 + | `logout` | `POST /api/v1/users/revoke-token` | Revoke token | 220 + 221 + ### Phase 6: User Interactions (Priority: Low, requires auth) 222 + 223 + #### New Endpoints (require authentication) 224 + | Function | Endpoint | Description | 225 + |----------|----------|-------------| 226 + | `rate_video` | `POST /api/v1/videos/{id}/rate` | Like/dislike video | 227 + | `get_my_rating` | `GET /api/v1/videos/{id}/rating` | Get my rating | 228 + | `get_comments` | `GET /api/v1/videos/{id}/comments` | Video comments | 229 + | `post_comment` | `POST /api/v1/videos/{id}/comment-threads` | Add comment | 230 + | `subscribe` | `POST /api/v1/users/me/subscriptions` | Subscribe to channel | 231 + | `unsubscribe` | `DELETE /api/v1/users/me/subscriptions/{uri}` | Unsubscribe | 232 + | `get_subscriptions` | `GET /api/v1/users/me/subscriptions` | My subscriptions | 233 + | `get_subscription_videos` | `GET /api/v1/users/me/subscriptions/videos` | Subscription feed | 234 + | `get_notifications` | `GET /api/v1/users/me/notifications` | My notifications | 235 + | `get_watch_history` | `GET /api/v1/users/me/history/videos` | Watch history | 236 + 237 + ## Implementation Strategy 238 + 239 + ### Step 1: Extend Video Type 240 + Add missing fields to the existing `video` type that are commonly returned: 241 + - `duration` (int, seconds) 242 + - `views` (int) 243 + - `likes` (int) 244 + - `dislikes` (int) 245 + - `category` (category record) 246 + - `licence` (licence record) 247 + - `language` (language record) 248 + - `privacy` (privacy record) 249 + - `is_local` (bool) 250 + - `channel` (channel_summary) 251 + - `account` (account_summary) 252 + 253 + ### Step 2: Create Common Pagination Module 254 + ```ocaml 255 + module Pagination : sig 256 + type 'a response = { 257 + total : int; 258 + data : 'a list; 259 + } 260 + 261 + val fetch_all : 262 + ?page_size:int -> 263 + ?max_pages:int -> 264 + (start:int -> count:int -> 'a response) -> 265 + 'a list 266 + end 267 + ``` 268 + 269 + ### Step 3: Create Module Structure 270 + ``` 271 + lib/ 272 + peertube.ml - Main entry, re-exports submodules 273 + peertube.mli - Public interface 274 + video.ml - Video types and operations 275 + channel.ml - Channel types and operations 276 + account.ml - Account types and operations 277 + playlist.ml - Playlist types and operations 278 + search.ml - Search functionality 279 + server.ml - Server config/stats 280 + auth.ml - Authentication (optional) 281 + ``` 282 + 283 + ### Step 4: Add CLI Commands 284 + Extend `opeertube` with new subcommands: 285 + - `opeertube search <query>` - Search videos 286 + - `opeertube channels` - List/search channels 287 + - `opeertube channel <name>` - Channel details 288 + - `opeertube playlists` - List playlists 289 + - `opeertube playlist <id>` - Playlist videos 290 + - `opeertube server info` - Server configuration 291 + - `opeertube server stats` - Server statistics 292 + 293 + ## File Changes Summary 294 + 295 + | File | Changes | 296 + |------|---------| 297 + | `lib/dune` | No changes needed | 298 + | `lib/peertube.mli` | Add new types and functions | 299 + | `lib/peertube.ml` | Add implementations | 300 + | `bin/opeertube.ml` | Add new CLI commands | 301 + | `dune-project` | No changes needed | 302 + 303 + ## Testing Plan 304 + 305 + 1. Unit tests for JSON codec round-trips 306 + 2. Integration tests against public PeerTube instances: 307 + - https://framatube.org 308 + - https://peertube.social 309 + - https://video.ploud.fr 310 + 3. CLI smoke tests for each command
+112
README.md
··· 1 + # ocaml-peertube 2 + 3 + An OCaml client library for the PeerTube video platform API, built on Eio for effect-based I/O. 4 + 5 + ## Features 6 + 7 + - Browse, search, and fetch videos from any PeerTube instance 8 + - Access channels, accounts, and playlists 9 + - Retrieve server configuration and statistics 10 + - Automatic pagination support 11 + - Full JSON serialization with Jsont 12 + - Command-line client (`opeertube`) included 13 + 14 + ## Installation 15 + 16 + ### From source 17 + 18 + ```bash 19 + git clone https://git.recoil.org/anil.recoil.org/ocaml-peertube.git 20 + cd ocaml-peertube 21 + opam install . --deps-only 22 + dune build 23 + ``` 24 + 25 + ## Usage 26 + 27 + ### Library 28 + 29 + ```ocaml 30 + open Peertube 31 + 32 + let () = 33 + Eio_main.run @@ fun env -> 34 + Eio.Switch.run @@ fun sw -> 35 + let session = Requests.create ~sw env in 36 + let client = Client.create ~session ~base_url:"https://video.example.com" in 37 + let videos = Client.list_videos client ~count:10 () in 38 + List.iter (fun v -> 39 + Printf.printf "%s: %s\n" (Video.uuid v) (Video.name v) 40 + ) (Paginated.data videos) 41 + ``` 42 + 43 + ### Command-line client 44 + 45 + ```bash 46 + # Browse videos 47 + opeertube browse -u https://video.example.com 48 + 49 + # Search videos 50 + opeertube search -u https://video.example.com "search query" 51 + 52 + # Get video details 53 + opeertube get -u https://video.example.com <uuid> 54 + 55 + # List channels 56 + opeertube channels list -u https://video.example.com 57 + 58 + # Get server info 59 + opeertube server info -u https://video.example.com 60 + 61 + # JSON output 62 + opeertube browse -u https://video.example.com --json 63 + ``` 64 + 65 + ## API Coverage 66 + 67 + ### Videos 68 + - `Client.list_videos` - Browse videos with filtering and sorting 69 + - `Client.search_videos` - Full-text search with date/duration filters 70 + - `Client.fetch_video_details` - Get details for a specific video 71 + - `Client.fetch_channel_videos` - List videos from a channel 72 + - `Client.fetch_all_channel_videos` - Auto-paginate all channel videos 73 + 74 + ### Channels & Accounts 75 + - `Client.list_channels` / `Client.search_channels` / `Client.get_channel` 76 + - `Client.list_accounts` / `Client.get_account` 77 + - `Client.get_account_videos` / `Client.get_account_channels` 78 + 79 + ### Playlists 80 + - `Client.list_playlists` / `Client.search_playlists` / `Client.get_playlist` 81 + - `Client.get_playlist_videos` / `Client.get_account_playlists` 82 + 83 + ### Server 84 + - `Client.get_config` - Server configuration 85 + - `Client.get_stats` - Server statistics 86 + 87 + ## Documentation 88 + 89 + Build locally with: 90 + 91 + ```bash 92 + dune build @doc 93 + open _build/default/_doc/_html/index.html 94 + ``` 95 + 96 + ## Development 97 + 98 + ### Building 99 + 100 + ```bash 101 + dune build 102 + ``` 103 + 104 + ### Formatting 105 + 106 + ```bash 107 + dune fmt 108 + ``` 109 + 110 + ## License 111 + 112 + ISC License. See [LICENSE.md](LICENSE.md) for details.
+201
TODO.md
··· 1 + # PeerTube OCaml Client - Remaining Features 2 + 3 + ## Completed 4 + 5 + - [x] Phase 1: Video Discovery (list, search, categories, languages, licences) 6 + - [x] Phase 2: Channels & Accounts (list, search, get details) 7 + - [x] Phase 3: Playlists (list, search, get details, videos) 8 + - [x] Phase 4: Server Information (config, stats) 9 + - [x] CLI with all read-only operations 10 + 11 + ## Phase 5: Authentication 12 + 13 + The Requests library will have OAuth support, which should simplify this phase. 14 + 15 + ### Types 16 + 17 + ```ocaml 18 + type oauth_client = { 19 + client_id : string; 20 + client_secret : string; 21 + } 22 + 23 + type auth_token = { 24 + access_token : string; 25 + token_type : string; 26 + expires_in : int; 27 + refresh_token : string; 28 + } 29 + ``` 30 + 31 + ### Endpoints 32 + 33 + - [ ] `get_oauth_client` - `GET /api/v1/oauth-clients/local` 34 + - Fetches the instance's OAuth client credentials 35 + - Required before user authentication 36 + 37 + - [ ] `login` - `POST /api/v1/users/token` 38 + - OAuth2 password grant flow 39 + - Parameters: username, password, client_id, client_secret 40 + - Returns access_token and refresh_token 41 + - Use Requests OAuth support for token management 42 + 43 + - [ ] `refresh_token` - `POST /api/v1/users/token` 44 + - OAuth2 refresh token grant 45 + - Automatically refresh expired tokens 46 + - Requests OAuth support should handle this 47 + 48 + - [ ] `logout` - `POST /api/v1/users/revoke-token` 49 + - Revoke the current access token 50 + 51 + ### Implementation Notes 52 + 53 + - Requests OAuth support will handle token storage and automatic refresh 54 + - Consider storing tokens in `~/.config/opeertube/` for CLI persistence 55 + - Add `--login` flag to CLI for interactive authentication 56 + 57 + ## Phase 6: User Interactions (requires auth) 58 + 59 + ### Video Interactions 60 + 61 + - [ ] `rate_video` - `POST /api/v1/videos/{id}/rate` 62 + - Body: `{ "rating": "like" | "dislike" | "none" }` 63 + 64 + - [ ] `get_my_rating` - `GET /api/v1/videos/{id}/rating` 65 + - Returns user's rating for a video 66 + 67 + ### Comments 68 + 69 + - [ ] `get_comments` - `GET /api/v1/videos/{id}/comment-threads` 70 + - Paginated list of top-level comments 71 + - Each comment has nested replies 72 + 73 + - [ ] `get_comment_thread` - `GET /api/v1/videos/{id}/comment-threads/{threadId}` 74 + - Full thread with all replies 75 + 76 + - [ ] `post_comment` - `POST /api/v1/videos/{id}/comment-threads` 77 + - Body: `{ "text": "comment content" }` 78 + 79 + - [ ] `reply_to_comment` - `POST /api/v1/videos/{id}/comments/{commentId}` 80 + - Body: `{ "text": "reply content" }` 81 + 82 + - [ ] `delete_comment` - `DELETE /api/v1/videos/{id}/comments/{commentId}` 83 + 84 + ### Subscriptions 85 + 86 + - [ ] `subscribe` - `POST /api/v1/users/me/subscriptions` 87 + - Body: `{ "uri": "channel@host" }` 88 + 89 + - [ ] `unsubscribe` - `DELETE /api/v1/users/me/subscriptions/{subscriptionHandle}` 90 + 91 + - [ ] `get_subscriptions` - `GET /api/v1/users/me/subscriptions` 92 + - Paginated list of subscribed channels 93 + 94 + - [ ] `get_subscription_videos` - `GET /api/v1/users/me/subscriptions/videos` 95 + - Subscription feed (videos from subscribed channels) 96 + 97 + - [ ] `subscription_exists` - `GET /api/v1/users/me/subscriptions/exist` 98 + - Check if subscribed to specific channels 99 + 100 + ### User Profile 101 + 102 + - [ ] `get_my_info` - `GET /api/v1/users/me` 103 + - Current user's account info 104 + 105 + - [ ] `update_my_info` - `PUT /api/v1/users/me` 106 + - Update display name, description, etc. 107 + 108 + - [ ] `get_my_videos` - `GET /api/v1/users/me/videos` 109 + - Videos uploaded by current user 110 + 111 + ### Watch History & Notifications 112 + 113 + - [ ] `get_watch_history` - `GET /api/v1/users/me/history/videos` 114 + - Paginated watch history 115 + 116 + - [ ] `delete_watch_history` - `POST /api/v1/users/me/history/videos/remove` 117 + - Clear watch history 118 + 119 + - [ ] `get_notifications` - `GET /api/v1/users/me/notifications` 120 + - User notifications 121 + 122 + - [ ] `mark_notifications_read` - `POST /api/v1/users/me/notifications/read` 123 + - Mark notifications as read 124 + 125 + - [ ] `mark_all_notifications_read` - `POST /api/v1/users/me/notifications/read-all` 126 + 127 + ## Phase 7: Content Upload (requires auth) 128 + 129 + ### Video Upload 130 + 131 + - [ ] `upload_video` - `POST /api/v1/videos/upload` 132 + - Multipart form upload 133 + - Required fields: channelId, name, videofile 134 + - Optional: description, tags, privacy, etc. 135 + 136 + - [ ] `upload_video_resumable_init` - `POST /api/v1/videos/upload-resumable` 137 + - Initialize resumable upload for large files 138 + 139 + - [ ] `upload_video_resumable` - `PUT /api/v1/videos/upload-resumable` 140 + - Continue resumable upload 141 + 142 + - [ ] `update_video` - `PUT /api/v1/videos/{id}` 143 + - Update video metadata 144 + 145 + - [ ] `delete_video` - `DELETE /api/v1/videos/{id}` 146 + 147 + ### Playlist Management 148 + 149 + - [ ] `create_playlist` - `POST /api/v1/video-playlists` 150 + - Create a new playlist 151 + 152 + - [ ] `update_playlist` - `PUT /api/v1/video-playlists/{playlistId}` 153 + 154 + - [ ] `delete_playlist` - `DELETE /api/v1/video-playlists/{playlistId}` 155 + 156 + - [ ] `add_video_to_playlist` - `POST /api/v1/video-playlists/{playlistId}/videos` 157 + - Body: `{ "videoId": id }` 158 + 159 + - [ ] `remove_video_from_playlist` - `DELETE /api/v1/video-playlists/{playlistId}/videos/{playlistElementId}` 160 + 161 + - [ ] `reorder_playlist` - `POST /api/v1/video-playlists/{playlistId}/videos/reorder` 162 + 163 + ## CLI Enhancements 164 + 165 + ### Authentication Commands 166 + 167 + - [ ] `opeertube login` - Interactive login, store token 168 + - [ ] `opeertube logout` - Revoke and delete stored token 169 + - [ ] `opeertube whoami` - Show current user info 170 + 171 + ### Authenticated Operations 172 + 173 + - [ ] `opeertube like VIDEO_UUID` - Like a video 174 + - [ ] `opeertube dislike VIDEO_UUID` - Dislike a video 175 + - [ ] `opeertube subscribe CHANNEL` - Subscribe to channel 176 + - [ ] `opeertube unsubscribe CHANNEL` - Unsubscribe 177 + - [ ] `opeertube subscriptions` - List subscriptions 178 + - [ ] `opeertube feed` - Show subscription feed 179 + - [ ] `opeertube history` - Show watch history 180 + - [ ] `opeertube notifications` - Show notifications 181 + 182 + ### Upload Commands 183 + 184 + - [ ] `opeertube upload FILE --channel CHANNEL --name NAME [options]` 185 + - [ ] `opeertube update VIDEO_UUID [options]` 186 + - [ ] `opeertube delete VIDEO_UUID` 187 + 188 + ## Code Quality 189 + 190 + - [ ] Add unit tests for JSON codec round-trips 191 + - [ ] Add integration tests against public instances 192 + - [ ] Add CI configuration (GitHub Actions) 193 + - [ ] Generate opam file for release 194 + - [ ] Add rate limiting / retry logic for API calls 195 + - [ ] Consider caching for frequently accessed data 196 + 197 + ## Documentation 198 + 199 + - [ ] Add examples to mli documentation 200 + - [ ] Create tutorial for common use cases 201 + - [ ] Document error handling patterns
+13
bin/dune
··· 1 + (executable 2 + (name opeertube) 3 + (public_name opeertube) 4 + (package peertube) 5 + (libraries 6 + peertube 7 + eio_main 8 + cmdliner 9 + logs 10 + logs.cli 11 + logs.fmt 12 + fmt.tty 13 + fmt.cli))
+635
bin/opeertube.ml
··· 1 + (** opeertube - PeerTube API command-line client *) 2 + 3 + open Cmdliner 4 + 5 + let setup_log style_renderer level = 6 + Fmt_tty.setup_std_outputs ?style_renderer (); 7 + Logs.set_level level; 8 + Logs.set_reporter (Logs_fmt.reporter ()) 9 + 10 + let setup_log_term = 11 + Term.(const setup_log $ Fmt_cli.style_renderer () $ Logs_cli.level ()) 12 + 13 + let base_url_arg = 14 + let doc = 15 + "Base URL of the PeerTube instance (e.g., https://video.example.com)" 16 + in 17 + Arg.(required & opt (some string) None & info [ "u"; "url" ] ~docv:"URL" ~doc) 18 + 19 + let channel_arg = 20 + let doc = "Channel name" in 21 + Arg.(required & pos 0 (some string) None & info [] ~docv:"CHANNEL" ~doc) 22 + 23 + let handle_arg = 24 + let doc = "Account or channel handle" in 25 + Arg.(required & pos 0 (some string) None & info [] ~docv:"HANDLE" ~doc) 26 + 27 + let id_arg = 28 + let doc = "Playlist ID or UUID" in 29 + Arg.(required & pos 0 (some string) None & info [] ~docv:"ID" ~doc) 30 + 31 + let uuid_arg = 32 + let doc = "Video UUID" in 33 + Arg.(required & pos 0 (some string) None & info [] ~docv:"UUID" ~doc) 34 + 35 + let query_arg = 36 + let doc = "Search query" in 37 + Arg.(required & pos 0 (some string) None & info [] ~docv:"QUERY" ~doc) 38 + 39 + let count_arg = 40 + let doc = "Number of results per page" in 41 + Arg.(value & opt int 20 & info [ "c"; "count" ] ~docv:"N" ~doc) 42 + 43 + let start_arg = 44 + let doc = "Starting offset for pagination" in 45 + Arg.(value & opt int 0 & info [ "s"; "start" ] ~docv:"N" ~doc) 46 + 47 + let max_pages_arg = 48 + let doc = "Maximum number of pages to fetch (unlimited if not specified)" in 49 + Arg.(value & opt (some int) None & info [ "m"; "max-pages" ] ~docv:"N" ~doc) 50 + 51 + let all_flag = 52 + let doc = "Fetch all results using automatic pagination" in 53 + Arg.(value & flag & info [ "a"; "all" ] ~doc) 54 + 55 + let json_flag = 56 + let doc = "Output as JSON" in 57 + Arg.(value & flag & info [ "j"; "json" ] ~doc) 58 + 59 + let output_arg = 60 + let doc = "Output file path for thumbnail" in 61 + Arg.(required & pos 1 (some string) None & info [] ~docv:"OUTPUT" ~doc) 62 + 63 + let handle_api_error f = 64 + try f () 65 + with Peertube.Client.Api_error (status, msg) -> 66 + Fmt.epr "Error: %s (HTTP %d)@." msg status; 67 + exit 1 68 + 69 + let print_json codec value = 70 + match Jsont_bytesrw.encode_string codec value with 71 + | Ok s -> print_endline s 72 + | Error e -> Fmt.epr "JSON encode error: %s@." e 73 + 74 + (* Video printing *) 75 + 76 + let print_video ?(json = false) (v : Peertube.Video.t) = 77 + if json then print_json Peertube.Video.jsont v 78 + else 79 + Fmt.pr 80 + "@[<v>%s@,\ 81 + \ UUID: %s@,\ 82 + \ ID: %d@,\ 83 + \ URL: %s@,\ 84 + \ Duration: %ds@,\ 85 + \ Views: %d@,\ 86 + \ Published: %a@,\ 87 + \ Tags: [%a]@]@." 88 + (Peertube.Video.name v) (Peertube.Video.uuid v) (Peertube.Video.id v) 89 + (Peertube.Video.url v) 90 + (Peertube.Video.duration v) 91 + (Peertube.Video.views v) (Ptime.pp_rfc3339 ()) 92 + (Peertube.Video.published_at v) 93 + Fmt.(list ~sep:(any ", ") string) 94 + (Peertube.Video.tags v) 95 + 96 + let print_videos ?(json = false) videos = 97 + if json then print_json Jsont.(list Peertube.Video.jsont) videos 98 + else List.iter (print_video ~json:false) videos 99 + 100 + (* Channel printing *) 101 + 102 + let print_channel_summary (c : Peertube.Channel_summary.t) = 103 + Fmt.pr "@[<v>%s (@%s)@, ID: %d@, URL: %s@]@." 104 + (Peertube.Channel_summary.display_name c) 105 + (Peertube.Channel_summary.name c) 106 + (Peertube.Channel_summary.id c) 107 + (Peertube.Channel_summary.url c) 108 + 109 + let print_channel ?(json = false) (c : Peertube.Channel.t) = 110 + if json then print_json Peertube.Channel.jsont c 111 + else begin 112 + let summary = Peertube.Channel.summary c in 113 + Fmt.pr 114 + "@[<v>%s (@%s)@, ID: %d@, URL: %s@, Followers: %d@, Created: %a@]@." 115 + (Peertube.Channel_summary.display_name summary) 116 + (Peertube.Channel_summary.name summary) 117 + (Peertube.Channel_summary.id summary) 118 + (Peertube.Channel_summary.url summary) 119 + (Peertube.Channel.followers_count c) 120 + (Ptime.pp_rfc3339 ()) 121 + (Peertube.Channel.created_at c); 122 + Option.iter 123 + (fun d -> Fmt.pr " Description: %s@." d) 124 + (Peertube.Channel.description c) 125 + end 126 + 127 + let print_channels ?(json = false) channels = 128 + if json then print_json Jsont.(list Peertube.Channel.jsont) channels 129 + else 130 + List.iter 131 + (fun c -> print_channel_summary (Peertube.Channel.summary c)) 132 + channels 133 + 134 + (* Account printing *) 135 + 136 + let print_account_summary (a : Peertube.Account_summary.t) = 137 + Fmt.pr "@[<v>%s (@%s)@, ID: %d@, URL: %s@]@." 138 + (Peertube.Account_summary.display_name a) 139 + (Peertube.Account_summary.name a) 140 + (Peertube.Account_summary.id a) 141 + (Peertube.Account_summary.url a) 142 + 143 + let print_account ?(json = false) (a : Peertube.Account.t) = 144 + if json then print_json Peertube.Account.jsont a 145 + else begin 146 + let summary = Peertube.Account.summary a in 147 + Fmt.pr 148 + "@[<v>%s (@%s)@, ID: %d@, URL: %s@, Followers: %d@, Created: %a@]@." 149 + (Peertube.Account_summary.display_name summary) 150 + (Peertube.Account_summary.name summary) 151 + (Peertube.Account_summary.id summary) 152 + (Peertube.Account_summary.url summary) 153 + (Peertube.Account.followers_count a) 154 + (Ptime.pp_rfc3339 ()) 155 + (Peertube.Account.created_at a); 156 + Option.iter 157 + (fun d -> Fmt.pr " Description: %s@." d) 158 + (Peertube.Account.description a) 159 + end 160 + 161 + let print_accounts ?(json = false) accounts = 162 + if json then print_json Jsont.(list Peertube.Account.jsont) accounts 163 + else 164 + List.iter 165 + (fun a -> print_account_summary (Peertube.Account.summary a)) 166 + accounts 167 + 168 + (* Playlist printing *) 169 + 170 + let print_playlist ?(json = false) (p : Peertube.Playlist.t) = 171 + if json then print_json Peertube.Playlist.jsont p 172 + else 173 + Fmt.pr 174 + "@[<v>%s@,\ 175 + \ UUID: %s@,\ 176 + \ ID: %d@,\ 177 + \ Videos: %d@,\ 178 + \ URL: %s@,\ 179 + \ Created: %a@]@." 180 + (Peertube.Playlist.display_name p) 181 + (Peertube.Playlist.uuid p) (Peertube.Playlist.id p) 182 + (Peertube.Playlist.videos_length p) 183 + (Peertube.Playlist.url p) (Ptime.pp_rfc3339 ()) 184 + (Peertube.Playlist.created_at p) 185 + 186 + let print_playlists ?(json = false) playlists = 187 + if json then print_json Jsont.(list Peertube.Playlist.jsont) playlists 188 + else List.iter (print_playlist ~json:false) playlists 189 + 190 + (* Video commands *) 191 + 192 + let list_channel_videos () base_url channel count start all max_pages json = 193 + handle_api_error @@ fun () -> 194 + Eio_main.run @@ fun env -> 195 + Eio.Switch.run @@ fun sw -> 196 + let session = Requests.create ~sw env in 197 + let client = Peertube.Client.create ~session ~base_url in 198 + if all then begin 199 + let videos = 200 + Peertube.Client.fetch_all_channel_videos client ?max_pages 201 + ~page_size:count ~channel () 202 + in 203 + Fmt.pr "Found %d videos@.@." (List.length videos); 204 + print_videos ~json videos 205 + end 206 + else begin 207 + let response = 208 + Peertube.Client.fetch_channel_videos client ~count ~start ~channel () 209 + in 210 + Fmt.pr "Showing %d of %d videos (offset %d)@.@." 211 + (List.length (Peertube.Paginated.data response)) 212 + (Peertube.Paginated.total response) 213 + start; 214 + print_videos ~json (Peertube.Paginated.data response) 215 + end 216 + 217 + let list_channel_videos_cmd = 218 + let doc = "List videos from a PeerTube channel" in 219 + let info = Cmd.info "channel-videos" ~doc in 220 + Cmd.v info 221 + Term.( 222 + const list_channel_videos $ setup_log_term $ base_url_arg $ channel_arg 223 + $ count_arg $ start_arg $ all_flag $ max_pages_arg $ json_flag) 224 + 225 + let browse_videos () base_url count start json = 226 + handle_api_error @@ fun () -> 227 + Eio_main.run @@ fun env -> 228 + Eio.Switch.run @@ fun sw -> 229 + let session = Requests.create ~sw env in 230 + let client = Peertube.Client.create ~session ~base_url in 231 + let response = Peertube.Client.list_videos client ~count ~start () in 232 + Fmt.pr "Showing %d of %d videos (offset %d)@.@." 233 + (List.length (Peertube.Paginated.data response)) 234 + (Peertube.Paginated.total response) 235 + start; 236 + print_videos ~json (Peertube.Paginated.data response) 237 + 238 + let browse_videos_cmd = 239 + let doc = "Browse all videos on the instance" in 240 + let info = Cmd.info "browse" ~doc in 241 + Cmd.v info 242 + Term.( 243 + const browse_videos $ setup_log_term $ base_url_arg $ count_arg 244 + $ start_arg $ json_flag) 245 + 246 + let search_videos () base_url query count start json = 247 + handle_api_error @@ fun () -> 248 + Eio_main.run @@ fun env -> 249 + Eio.Switch.run @@ fun sw -> 250 + let session = Requests.create ~sw env in 251 + let client = Peertube.Client.create ~session ~base_url in 252 + let response = Peertube.Client.search_videos client ~query ~count ~start () in 253 + Fmt.pr "Found %d results for '%s' (showing %d, offset %d)@.@." 254 + (Peertube.Paginated.total response) 255 + query 256 + (List.length (Peertube.Paginated.data response)) 257 + start; 258 + print_videos ~json (Peertube.Paginated.data response) 259 + 260 + let search_videos_cmd = 261 + let doc = "Search for videos" in 262 + let info = Cmd.info "search" ~doc in 263 + Cmd.v info 264 + Term.( 265 + const search_videos $ setup_log_term $ base_url_arg $ query_arg 266 + $ count_arg $ start_arg $ json_flag) 267 + 268 + let get_video () base_url uuid json = 269 + handle_api_error @@ fun () -> 270 + Eio_main.run @@ fun env -> 271 + Eio.Switch.run @@ fun sw -> 272 + let session = Requests.create ~sw env in 273 + let client = Peertube.Client.create ~session ~base_url in 274 + let video = Peertube.Client.fetch_video_details client ~uuid () in 275 + print_video ~json video 276 + 277 + let get_video_cmd = 278 + let doc = "Get details for a specific video by UUID" in 279 + let info = Cmd.info "get" ~doc in 280 + Cmd.v info 281 + Term.( 282 + const get_video $ setup_log_term $ base_url_arg $ uuid_arg $ json_flag) 283 + 284 + let download_thumbnail () base_url uuid output = 285 + handle_api_error @@ fun () -> 286 + Eio_main.run @@ fun env -> 287 + Eio.Switch.run @@ fun sw -> 288 + let session = Requests.create ~sw env in 289 + let client = Peertube.Client.create ~session ~base_url in 290 + let video = Peertube.Client.fetch_video_details client ~uuid () in 291 + match 292 + Peertube.Client.download_thumbnail client ~video ~output_path:output () 293 + with 294 + | Ok () -> Fmt.pr "Thumbnail saved to %s@." output 295 + | Error (`Msg msg) -> 296 + Fmt.epr "Error: %s@." msg; 297 + exit 1 298 + 299 + let download_thumbnail_cmd = 300 + let doc = "Download a video's thumbnail" in 301 + let info = Cmd.info "thumbnail" ~doc in 302 + Cmd.v info 303 + Term.( 304 + const download_thumbnail $ setup_log_term $ base_url_arg $ uuid_arg 305 + $ output_arg) 306 + 307 + (* Channel commands *) 308 + 309 + let list_channels () base_url count start json = 310 + handle_api_error @@ fun () -> 311 + Eio_main.run @@ fun env -> 312 + Eio.Switch.run @@ fun sw -> 313 + let session = Requests.create ~sw env in 314 + let client = Peertube.Client.create ~session ~base_url in 315 + let response = Peertube.Client.list_channels client ~count ~start () in 316 + Fmt.pr "Showing %d of %d channels (offset %d)@.@." 317 + (List.length (Peertube.Paginated.data response)) 318 + (Peertube.Paginated.total response) 319 + start; 320 + print_channels ~json (Peertube.Paginated.data response) 321 + 322 + let list_channels_cmd = 323 + let doc = "List channels on the instance" in 324 + let info = Cmd.info "list" ~doc in 325 + Cmd.v info 326 + Term.( 327 + const list_channels $ setup_log_term $ base_url_arg $ count_arg 328 + $ start_arg $ json_flag) 329 + 330 + let search_channels () base_url query count start json = 331 + handle_api_error @@ fun () -> 332 + Eio_main.run @@ fun env -> 333 + Eio.Switch.run @@ fun sw -> 334 + let session = Requests.create ~sw env in 335 + let client = Peertube.Client.create ~session ~base_url in 336 + let response = 337 + Peertube.Client.search_channels client ~query ~count ~start () 338 + in 339 + Fmt.pr "Found %d channels for '%s' (showing %d, offset %d)@.@." 340 + (Peertube.Paginated.total response) 341 + query 342 + (List.length (Peertube.Paginated.data response)) 343 + start; 344 + print_channels ~json (Peertube.Paginated.data response) 345 + 346 + let search_channels_cmd = 347 + let doc = "Search for channels" in 348 + let info = Cmd.info "search" ~doc in 349 + Cmd.v info 350 + Term.( 351 + const search_channels $ setup_log_term $ base_url_arg $ query_arg 352 + $ count_arg $ start_arg $ json_flag) 353 + 354 + let get_channel () base_url handle json = 355 + handle_api_error @@ fun () -> 356 + Eio_main.run @@ fun env -> 357 + Eio.Switch.run @@ fun sw -> 358 + let session = Requests.create ~sw env in 359 + let client = Peertube.Client.create ~session ~base_url in 360 + let channel = Peertube.Client.get_channel client ~handle () in 361 + print_channel ~json channel 362 + 363 + let get_channel_cmd = 364 + let doc = "Get details for a specific channel" in 365 + let info = Cmd.info "get" ~doc in 366 + Cmd.v info 367 + Term.( 368 + const get_channel $ setup_log_term $ base_url_arg $ handle_arg $ json_flag) 369 + 370 + let channels_cmd = 371 + let doc = "Channel operations" in 372 + let info = Cmd.info "channels" ~doc in 373 + Cmd.group info [ list_channels_cmd; search_channels_cmd; get_channel_cmd ] 374 + 375 + (* Account commands *) 376 + 377 + let list_accounts () base_url count start json = 378 + handle_api_error @@ fun () -> 379 + Eio_main.run @@ fun env -> 380 + Eio.Switch.run @@ fun sw -> 381 + let session = Requests.create ~sw env in 382 + let client = Peertube.Client.create ~session ~base_url in 383 + let response = Peertube.Client.list_accounts client ~count ~start () in 384 + Fmt.pr "Showing %d of %d accounts (offset %d)@.@." 385 + (List.length (Peertube.Paginated.data response)) 386 + (Peertube.Paginated.total response) 387 + start; 388 + print_accounts ~json (Peertube.Paginated.data response) 389 + 390 + let list_accounts_cmd = 391 + let doc = "List accounts on the instance" in 392 + let info = Cmd.info "list" ~doc in 393 + Cmd.v info 394 + Term.( 395 + const list_accounts $ setup_log_term $ base_url_arg $ count_arg 396 + $ start_arg $ json_flag) 397 + 398 + let get_account () base_url handle json = 399 + handle_api_error @@ fun () -> 400 + Eio_main.run @@ fun env -> 401 + Eio.Switch.run @@ fun sw -> 402 + let session = Requests.create ~sw env in 403 + let client = Peertube.Client.create ~session ~base_url in 404 + let account = Peertube.Client.get_account client ~handle () in 405 + print_account ~json account 406 + 407 + let get_account_cmd = 408 + let doc = "Get details for a specific account" in 409 + let info = Cmd.info "get" ~doc in 410 + Cmd.v info 411 + Term.( 412 + const get_account $ setup_log_term $ base_url_arg $ handle_arg $ json_flag) 413 + 414 + let get_account_videos () base_url handle count start json = 415 + handle_api_error @@ fun () -> 416 + Eio_main.run @@ fun env -> 417 + Eio.Switch.run @@ fun sw -> 418 + let session = Requests.create ~sw env in 419 + let client = Peertube.Client.create ~session ~base_url in 420 + let response = 421 + Peertube.Client.get_account_videos client ~count ~start ~handle () 422 + in 423 + Fmt.pr "Showing %d of %d videos (offset %d)@.@." 424 + (List.length (Peertube.Paginated.data response)) 425 + (Peertube.Paginated.total response) 426 + start; 427 + print_videos ~json (Peertube.Paginated.data response) 428 + 429 + let get_account_videos_cmd = 430 + let doc = "Get videos from an account" in 431 + let info = Cmd.info "videos" ~doc in 432 + Cmd.v info 433 + Term.( 434 + const get_account_videos $ setup_log_term $ base_url_arg $ handle_arg 435 + $ count_arg $ start_arg $ json_flag) 436 + 437 + let accounts_cmd = 438 + let doc = "Account operations" in 439 + let info = Cmd.info "accounts" ~doc in 440 + Cmd.group info [ list_accounts_cmd; get_account_cmd; get_account_videos_cmd ] 441 + 442 + (* Playlist commands *) 443 + 444 + let list_playlists () base_url count start json = 445 + handle_api_error @@ fun () -> 446 + Eio_main.run @@ fun env -> 447 + Eio.Switch.run @@ fun sw -> 448 + let session = Requests.create ~sw env in 449 + let client = Peertube.Client.create ~session ~base_url in 450 + let response = Peertube.Client.list_playlists client ~count ~start () in 451 + Fmt.pr "Showing %d of %d playlists (offset %d)@.@." 452 + (List.length (Peertube.Paginated.data response)) 453 + (Peertube.Paginated.total response) 454 + start; 455 + print_playlists ~json (Peertube.Paginated.data response) 456 + 457 + let list_playlists_cmd = 458 + let doc = "List playlists on the instance" in 459 + let info = Cmd.info "list" ~doc in 460 + Cmd.v info 461 + Term.( 462 + const list_playlists $ setup_log_term $ base_url_arg $ count_arg 463 + $ start_arg $ json_flag) 464 + 465 + let search_playlists () base_url query count start json = 466 + handle_api_error @@ fun () -> 467 + Eio_main.run @@ fun env -> 468 + Eio.Switch.run @@ fun sw -> 469 + let session = Requests.create ~sw env in 470 + let client = Peertube.Client.create ~session ~base_url in 471 + let response = 472 + Peertube.Client.search_playlists client ~query ~count ~start () 473 + in 474 + Fmt.pr "Found %d playlists for '%s' (showing %d, offset %d)@.@." 475 + (Peertube.Paginated.total response) 476 + query 477 + (List.length (Peertube.Paginated.data response)) 478 + start; 479 + print_playlists ~json (Peertube.Paginated.data response) 480 + 481 + let search_playlists_cmd = 482 + let doc = "Search for playlists" in 483 + let info = Cmd.info "search" ~doc in 484 + Cmd.v info 485 + Term.( 486 + const search_playlists $ setup_log_term $ base_url_arg $ query_arg 487 + $ count_arg $ start_arg $ json_flag) 488 + 489 + let get_playlist () base_url id json = 490 + handle_api_error @@ fun () -> 491 + Eio_main.run @@ fun env -> 492 + Eio.Switch.run @@ fun sw -> 493 + let session = Requests.create ~sw env in 494 + let client = Peertube.Client.create ~session ~base_url in 495 + let playlist = Peertube.Client.get_playlist client ~id () in 496 + print_playlist ~json playlist 497 + 498 + let get_playlist_cmd = 499 + let doc = "Get details for a specific playlist" in 500 + let info = Cmd.info "get" ~doc in 501 + Cmd.v info 502 + Term.( 503 + const get_playlist $ setup_log_term $ base_url_arg $ id_arg $ json_flag) 504 + 505 + let get_playlist_videos () base_url id count start json = 506 + handle_api_error @@ fun () -> 507 + Eio_main.run @@ fun env -> 508 + Eio.Switch.run @@ fun sw -> 509 + let session = Requests.create ~sw env in 510 + let client = Peertube.Client.create ~session ~base_url in 511 + let response = 512 + Peertube.Client.get_playlist_videos client ~count ~start ~id () 513 + in 514 + Fmt.pr "Showing %d of %d playlist elements (offset %d)@.@." 515 + (List.length (Peertube.Paginated.data response)) 516 + (Peertube.Paginated.total response) 517 + start; 518 + List.iter 519 + (fun (e : Peertube.Playlist.Element.t) -> 520 + match Peertube.Playlist.Element.video e with 521 + | Some v -> print_video ~json v 522 + | None -> 523 + Fmt.pr "[Deleted video at position %d]@." 524 + (Peertube.Playlist.Element.position e)) 525 + (Peertube.Paginated.data response) 526 + 527 + let get_playlist_videos_cmd = 528 + let doc = "Get videos in a playlist" in 529 + let info = Cmd.info "videos" ~doc in 530 + Cmd.v info 531 + Term.( 532 + const get_playlist_videos $ setup_log_term $ base_url_arg $ id_arg 533 + $ count_arg $ start_arg $ json_flag) 534 + 535 + let playlists_cmd = 536 + let doc = "Playlist operations" in 537 + let info = Cmd.info "playlists" ~doc in 538 + Cmd.group info 539 + [ 540 + list_playlists_cmd; 541 + search_playlists_cmd; 542 + get_playlist_cmd; 543 + get_playlist_videos_cmd; 544 + ] 545 + 546 + (* Server commands *) 547 + 548 + let server_info () base_url json = 549 + handle_api_error @@ fun () -> 550 + Eio_main.run @@ fun env -> 551 + Eio.Switch.run @@ fun sw -> 552 + let session = Requests.create ~sw env in 553 + let client = Peertube.Client.create ~session ~base_url in 554 + let config = Peertube.Client.get_config client () in 555 + if json then print_json Peertube.Server_config.jsont config 556 + else begin 557 + let instance = Peertube.Server_config.instance config in 558 + Fmt.pr 559 + "@[<v>Instance: %s@,\ 560 + \ %s@,\ 561 + \ Version: %s@,\ 562 + \ Signup allowed: %b@,\ 563 + \ Transcoding: %b@]@." 564 + (Peertube.Instance_info.name instance) 565 + (Peertube.Instance_info.short_description instance) 566 + (Peertube.Server_config.server_version config) 567 + (Peertube.Server_config.signup_allowed config) 568 + (Peertube.Server_config.transcoding_enabled config) 569 + end 570 + 571 + let server_info_cmd = 572 + let doc = "Get server configuration" in 573 + let info = Cmd.info "info" ~doc in 574 + Cmd.v info 575 + Term.(const server_info $ setup_log_term $ base_url_arg $ json_flag) 576 + 577 + let server_stats () base_url json = 578 + handle_api_error @@ fun () -> 579 + Eio_main.run @@ fun env -> 580 + Eio.Switch.run @@ fun sw -> 581 + let session = Requests.create ~sw env in 582 + let client = Peertube.Client.create ~session ~base_url in 583 + let stats = Peertube.Client.get_stats client () in 584 + if json then print_json Peertube.Server_stats.jsont stats 585 + else begin 586 + Fmt.pr 587 + "@[<v>Users: %d (daily active: %d, weekly: %d, monthly: %d)@,\ 588 + Local videos: %d (views: %d)@,\ 589 + Total videos: %d@,\ 590 + Local channels: %d@,\ 591 + Local playlists: %d@,\ 592 + Instance followers: %d / following: %d@]@." 593 + (Peertube.Server_stats.total_users stats) 594 + (Peertube.Server_stats.total_daily_active_users stats) 595 + (Peertube.Server_stats.total_weekly_active_users stats) 596 + (Peertube.Server_stats.total_monthly_active_users stats) 597 + (Peertube.Server_stats.total_local_videos stats) 598 + (Peertube.Server_stats.total_local_video_views stats) 599 + (Peertube.Server_stats.total_videos stats) 600 + (Peertube.Server_stats.total_local_video_channels stats) 601 + (Peertube.Server_stats.total_local_playlists stats) 602 + (Peertube.Server_stats.total_instance_followers stats) 603 + (Peertube.Server_stats.total_instance_following stats) 604 + end 605 + 606 + let server_stats_cmd = 607 + let doc = "Get server statistics" in 608 + let info = Cmd.info "stats" ~doc in 609 + Cmd.v info 610 + Term.(const server_stats $ setup_log_term $ base_url_arg $ json_flag) 611 + 612 + let server_cmd = 613 + let doc = "Server information" in 614 + let info = Cmd.info "server" ~doc in 615 + Cmd.group info [ server_info_cmd; server_stats_cmd ] 616 + 617 + (* Main command *) 618 + 619 + let main_cmd = 620 + let doc = "PeerTube API command-line client" in 621 + let info = Cmd.info "opeertube" ~version:"0.1.0" ~doc in 622 + Cmd.group info 623 + [ 624 + browse_videos_cmd; 625 + search_videos_cmd; 626 + get_video_cmd; 627 + list_channel_videos_cmd; 628 + download_thumbnail_cmd; 629 + channels_cmd; 630 + accounts_cmd; 631 + playlists_cmd; 632 + server_cmd; 633 + ] 634 + 635 + let () = exit (Cmd.eval main_cmd)
+28
dune-project
··· 1 + (lang dune 3.16) 2 + (name peertube) 3 + 4 + (generate_opam_files true) 5 + 6 + (source (uri "https://git.recoil.org/anil.recoil.org/ocaml-peertube")) 7 + (license ISC) 8 + (authors "Anil Madhavapeddy") 9 + (maintainers "anil@recoil.org") 10 + (homepage "https://git.recoil.org/anil.recoil.org/ocaml-peertube") 11 + (bug_reports "https://git.recoil.org/anil.recoil.org/ocaml-peertube/issues") 12 + 13 + (package 14 + (name peertube) 15 + (synopsis "PeerTube API client for OCaml using Eio") 16 + (description 17 + "An OCaml client library for the PeerTube video platform API, built on Eio for effect-based I/O. Includes a command-line client (opeertube) for browsing videos, channels, accounts, and playlists.") 18 + (depends 19 + (ocaml (>= 5.1.0)) 20 + (eio (>= 1.0)) 21 + (eio_main (>= 1.0)) 22 + (requests (>= 0.1)) 23 + (jsont (>= 0.1)) 24 + (ptime (>= 1.0)) 25 + (fmt (>= 0.9)) 26 + (logs (>= 0.7)) 27 + (cmdliner (>= 1.2)) 28 + (odoc :with-doc)))
+4
lib/dune
··· 1 + (library 2 + (name peertube) 3 + (public_name peertube) 4 + (libraries requests jsont jsont.bytesrw ptime fmt logs))
+23
lib/peertube.ml
··· 1 + (** PeerTube API client for OCaml using Eio. *) 2 + 3 + module Labeled = Peertube_labeled 4 + module Privacy = Peertube_privacy 5 + module Video_sort = Peertube_video_sort 6 + module Paginated = Peertube_paginated 7 + module Account_summary = Peertube_account_summary 8 + module Account = Peertube_account 9 + module Channel_summary = Peertube_channel_summary 10 + module Channel = Peertube_channel 11 + module Video = Peertube_video 12 + 13 + module Playlist = struct 14 + module Privacy = Peertube_playlist_privacy 15 + module Type = Peertube_playlist_type 16 + module Element = Peertube_playlist_element 17 + include Peertube_playlist 18 + end 19 + 20 + module Instance_info = Peertube_instance_info 21 + module Server_config = Peertube_server_config 22 + module Server_stats = Peertube_server_stats 23 + module Client = Peertube_client
+1089
lib/peertube.mli
··· 1 + (** PeerTube API client for OCaml. 2 + 3 + This library provides a typed client for the 4 + {{:https://docs.joinpeertube.org/api-rest-reference.html}PeerTube video 5 + platform API}, using {{:https://github.com/ocaml-multicore/eio}Eio} for 6 + effect-based I/O and {{:https://erratique.ch/software/jsont}jsont} for JSON 7 + serialization. 8 + 9 + {1 Quick Start} 10 + 11 + {[ 12 + open Peertube 13 + 14 + let () = 15 + Eio_main.run @@ fun env -> 16 + Eio.Switch.run @@ fun sw -> 17 + let session = Requests.create ~sw env in 18 + let client = Client.create ~session ~base_url:"https://framatube.org" in 19 + 20 + (* List recent videos *) 21 + let videos = Client.list_videos client () in 22 + List.iter 23 + (fun v -> Printf.printf "%s (%s)\n" (Video.name v) (Video.uuid v)) 24 + (Paginated.data videos) 25 + ]} 26 + 27 + {1 Module Organization} 28 + 29 + The library is organized into focused modules, each with an abstract type 30 + [t], a JSON codec [jsont], accessors, and a pretty printer [pp]: 31 + 32 + {2 Common Types} 33 + - {!Labeled} - ID-label pairs for categories, licences, languages 34 + - {!Privacy} - Video privacy levels 35 + - {!Video_sort} - Video sort options for queries 36 + - {!Paginated} - Paginated API responses 37 + 38 + {2 Accounts and Channels} 39 + - {!Account_summary} - Account summary (embedded in other responses) 40 + - {!Account} - Full account details 41 + - {!Channel_summary} - Channel summary (embedded in other responses) 42 + - {!Channel} - Full channel details 43 + 44 + {2 Videos} 45 + - {!Video} - Video records with metadata 46 + 47 + {2 Playlists} 48 + - {!Playlist} - Video playlists with nested {!Playlist.Privacy}, 49 + {!Playlist.Type}, and {!Playlist.Element} 50 + 51 + {2 Server Information} 52 + - {!Instance_info} - Instance name and description 53 + - {!Server_config} - Server configuration and features 54 + - {!Server_stats} - Server statistics 55 + 56 + {2 API Client} 57 + - {!Client} - Functions for making API requests *) 58 + 59 + (** {1 Common Types} *) 60 + 61 + (** Labeled values with ID and display label. 62 + 63 + Used for categories, licences, languages, and other enumerated values 64 + returned by the PeerTube API. Each labeled value has an identifier and a 65 + human-readable label. *) 66 + module Labeled : sig 67 + type 'a t 68 + (** A labeled value with an identifier and human-readable label. *) 69 + 70 + val make : id:'a -> label:string -> 'a t 71 + (** [make ~id ~label] creates a labeled value. *) 72 + 73 + val id : 'a t -> 'a 74 + (** [id t] returns the identifier. *) 75 + 76 + val label : 'a t -> string 77 + (** [label t] returns the human-readable label. *) 78 + 79 + val int_jsont : int t Jsont.t 80 + (** JSON codec for labeled values with integer IDs. *) 81 + 82 + val string_jsont : string t Jsont.t 83 + (** JSON codec for labeled values with string IDs. *) 84 + 85 + val pp : (Format.formatter -> 'a -> unit) -> Format.formatter -> 'a t -> unit 86 + (** [pp pp_id] is a pretty printer for labeled values, using [pp_id] for the 87 + identifier. *) 88 + end 89 + 90 + (** Video privacy levels. 91 + 92 + PeerTube videos can have different privacy settings that control who can 93 + view them. *) 94 + module Privacy : sig 95 + (** Video privacy level. 96 + - [Public] - Visible to everyone 97 + - [Unlisted] - Only accessible via direct link 98 + - [Private] - Only visible to the owner 99 + - [Internal] - Only visible to instance users *) 100 + type t = Public | Unlisted | Private | Internal 101 + 102 + val to_int : t -> int 103 + (** [to_int t] converts privacy to its API integer representation. *) 104 + 105 + val of_int : int -> t 106 + (** [of_int n] converts an API integer to privacy level. *) 107 + 108 + val jsont : t Jsont.t 109 + (** JSON codec for privacy. *) 110 + 111 + val pp : Format.formatter -> t -> unit 112 + (** Pretty printer for privacy. *) 113 + end 114 + 115 + (** Video sort options for API queries. 116 + 117 + When listing or searching videos, you can specify how results should be 118 + sorted. *) 119 + module Video_sort : sig 120 + (** Video sort order. 121 + - [Newest] - Most recently published first 122 + - [Oldest] - Oldest first 123 + - [Views] - Most viewed first 124 + - [Likes] - Most liked first 125 + - [Trending] - Currently trending 126 + - [Hot] - Hot/popular videos 127 + - [Random] - Random order 128 + - [Best] - Best match (for search) *) 129 + type t = Newest | Oldest | Views | Likes | Trending | Hot | Random | Best 130 + 131 + val to_string : t -> string 132 + (** [to_string t] converts sort option to API query string. *) 133 + 134 + val pp : Format.formatter -> t -> unit 135 + (** Pretty printer for sort options. *) 136 + end 137 + 138 + (** Paginated API responses. 139 + 140 + Most list endpoints return paginated results with a total count and a page 141 + of data. Use [count] and [start] parameters in API calls to navigate through 142 + pages. *) 143 + module Paginated : sig 144 + type 'a t 145 + (** A paginated response containing total count and a page of data. *) 146 + 147 + val make : total:int -> data:'a list -> 'a t 148 + (** [make ~total ~data] creates a paginated response. *) 149 + 150 + val total : 'a t -> int 151 + (** [total t] returns the total number of items available across all pages. *) 152 + 153 + val data : 'a t -> 'a list 154 + (** [data t] returns the items in this page. *) 155 + 156 + val jsont : kind:string -> 'a Jsont.t -> 'a t Jsont.t 157 + (** [jsont ~kind item_jsont] creates a JSON codec for paginated responses of 158 + items encoded with [item_jsont]. *) 159 + 160 + val pp : (Format.formatter -> 'a -> unit) -> Format.formatter -> 'a t -> unit 161 + (** [pp pp_item] is a pretty printer for paginated responses. *) 162 + end 163 + 164 + (** {1 Account Types} *) 165 + 166 + (** Summary of a PeerTube account. 167 + 168 + Account summaries are embedded in other responses (videos, channels, etc.) 169 + and contain basic identifying information. For full account details, use 170 + {!Client.get_account}. *) 171 + module Account_summary : sig 172 + type t 173 + (** Account summary type. *) 174 + 175 + val make : 176 + id:int -> 177 + name:string -> 178 + display_name:string -> 179 + url:string -> 180 + host:string -> 181 + avatar_path:string option -> 182 + t 183 + (** Create an account summary. *) 184 + 185 + val id : t -> int 186 + (** Numeric account ID. *) 187 + 188 + val name : t -> string 189 + (** Account name (handle without host, e.g., "username"). *) 190 + 191 + val display_name : t -> string 192 + (** Human-readable display name. *) 193 + 194 + val url : t -> string 195 + (** Full account URL. *) 196 + 197 + val host : t -> string 198 + (** Instance host (e.g., "framatube.org"). *) 199 + 200 + val avatar_path : t -> string option 201 + (** Avatar image path (relative to instance URL). *) 202 + 203 + val jsont : t Jsont.t 204 + (** JSON codec. *) 205 + 206 + val pp : Format.formatter -> t -> unit 207 + (** Pretty printer. *) 208 + end 209 + 210 + (** Full PeerTube account details. 211 + 212 + Contains complete account information including statistics and the account 213 + summary. *) 214 + module Account : sig 215 + type t 216 + (** Account type. *) 217 + 218 + val make : 219 + summary:Account_summary.t -> 220 + description:string option -> 221 + created_at:Ptime.t -> 222 + followers_count:int -> 223 + following_count:int -> 224 + following_hosts_count:int option -> 225 + t 226 + (** Create an account. *) 227 + 228 + val summary : t -> Account_summary.t 229 + (** Basic account information. *) 230 + 231 + val description : t -> string option 232 + (** Account description/bio. *) 233 + 234 + val created_at : t -> Ptime.t 235 + (** Account creation timestamp. *) 236 + 237 + val followers_count : t -> int 238 + (** Number of followers. *) 239 + 240 + val following_count : t -> int 241 + (** Number of accounts being followed. *) 242 + 243 + val following_hosts_count : t -> int option 244 + (** Number of federated hosts being followed. *) 245 + 246 + val jsont : t Jsont.t 247 + (** JSON codec. *) 248 + 249 + val pp : Format.formatter -> t -> unit 250 + (** Pretty printer. *) 251 + end 252 + 253 + (** {1 Channel Types} *) 254 + 255 + (** Summary of a PeerTube channel. 256 + 257 + Channel summaries are embedded in other responses (videos, playlists, etc.) 258 + and contain basic identifying information. For full channel details, use 259 + {!Client.get_channel}. *) 260 + module Channel_summary : sig 261 + type t 262 + (** Channel summary type. *) 263 + 264 + val make : 265 + id:int -> 266 + name:string -> 267 + display_name:string -> 268 + url:string -> 269 + host:string -> 270 + avatar_path:string option -> 271 + t 272 + (** Create a channel summary. *) 273 + 274 + val id : t -> int 275 + (** Numeric channel ID. *) 276 + 277 + val name : t -> string 278 + (** Channel name (handle without host). *) 279 + 280 + val display_name : t -> string 281 + (** Human-readable display name. *) 282 + 283 + val url : t -> string 284 + (** Full channel URL. *) 285 + 286 + val host : t -> string 287 + (** Instance host. *) 288 + 289 + val avatar_path : t -> string option 290 + (** Avatar image path (relative to instance URL). *) 291 + 292 + val jsont : t Jsont.t 293 + (** JSON codec. *) 294 + 295 + val pp : Format.formatter -> t -> unit 296 + (** Pretty printer. *) 297 + end 298 + 299 + (** Full PeerTube channel details. 300 + 301 + Contains complete channel information including statistics, the owner 302 + account, and the channel summary. *) 303 + module Channel : sig 304 + type t 305 + (** Channel type. *) 306 + 307 + val make : 308 + summary:Channel_summary.t -> 309 + description:string option -> 310 + support:string option -> 311 + created_at:Ptime.t -> 312 + followers_count:int -> 313 + following_count:int -> 314 + banner_path:string option -> 315 + owner_account:Account_summary.t option -> 316 + t 317 + (** Create a channel. *) 318 + 319 + val summary : t -> Channel_summary.t 320 + (** Basic channel information. *) 321 + 322 + val description : t -> string option 323 + (** Channel description. *) 324 + 325 + val support : t -> string option 326 + (** Support text or links (e.g., donation info). *) 327 + 328 + val created_at : t -> Ptime.t 329 + (** Channel creation timestamp. *) 330 + 331 + val followers_count : t -> int 332 + (** Number of followers. *) 333 + 334 + val following_count : t -> int 335 + (** Number of accounts being followed. *) 336 + 337 + val banner_path : t -> string option 338 + (** Banner image path (relative to instance URL). *) 339 + 340 + val owner_account : t -> Account_summary.t option 341 + (** Account that owns this channel. *) 342 + 343 + val jsont : t Jsont.t 344 + (** JSON codec. *) 345 + 346 + val pp : Format.formatter -> t -> unit 347 + (** Pretty printer. *) 348 + end 349 + 350 + (** {1 Video Types} *) 351 + 352 + (** PeerTube video record. 353 + 354 + Contains all metadata for a video including identifiers, timestamps, 355 + statistics, and references to the channel and account that published it. *) 356 + module Video : sig 357 + type t 358 + (** Video type. *) 359 + 360 + val make : 361 + id:int -> 362 + uuid:string -> 363 + short_uuid:string option -> 364 + name:string -> 365 + description:string option -> 366 + url:string -> 367 + embed_path:string -> 368 + published_at:Ptime.t -> 369 + originally_published_at:Ptime.t option -> 370 + updated_at:Ptime.t option -> 371 + thumbnail_path:string option -> 372 + preview_path:string option -> 373 + tags:string list -> 374 + duration:int -> 375 + views:int -> 376 + likes:int -> 377 + dislikes:int -> 378 + is_local:bool -> 379 + is_live:bool -> 380 + privacy:Privacy.t -> 381 + category:int Labeled.t option -> 382 + licence:int Labeled.t option -> 383 + language:string Labeled.t option -> 384 + channel:Channel_summary.t option -> 385 + account:Account_summary.t option -> 386 + t 387 + (** Create a video. *) 388 + 389 + val id : t -> int 390 + (** Numeric video ID. *) 391 + 392 + val uuid : t -> string 393 + (** Video UUID (standard format). *) 394 + 395 + val short_uuid : t -> string option 396 + (** Short UUID for compact URLs. *) 397 + 398 + val name : t -> string 399 + (** Video title. *) 400 + 401 + val description : t -> string option 402 + (** Video description (may contain markdown). *) 403 + 404 + val url : t -> string 405 + (** Full video URL. *) 406 + 407 + val embed_path : t -> string 408 + (** Embed iframe path. *) 409 + 410 + val published_at : t -> Ptime.t 411 + (** Publication timestamp. *) 412 + 413 + val originally_published_at : t -> Ptime.t option 414 + (** Original publication timestamp (for re-uploads). *) 415 + 416 + val updated_at : t -> Ptime.t option 417 + (** Last update timestamp. *) 418 + 419 + val thumbnail_path : t -> string option 420 + (** Thumbnail image path (relative to instance URL). *) 421 + 422 + val preview_path : t -> string option 423 + (** Preview image path (relative to instance URL). *) 424 + 425 + val tags : t -> string list 426 + (** Video tags. *) 427 + 428 + val duration : t -> int 429 + (** Duration in seconds. *) 430 + 431 + val views : t -> int 432 + (** View count. *) 433 + 434 + val likes : t -> int 435 + (** Like count. *) 436 + 437 + val dislikes : t -> int 438 + (** Dislike count. *) 439 + 440 + val is_local : t -> bool 441 + (** Whether the video is local to this instance (vs federated). *) 442 + 443 + val is_live : t -> bool 444 + (** Whether this is a live stream. *) 445 + 446 + val privacy : t -> Privacy.t 447 + (** Privacy level. *) 448 + 449 + val category : t -> int Labeled.t option 450 + (** Video category (e.g., Music, Education). *) 451 + 452 + val licence : t -> int Labeled.t option 453 + (** Video licence (e.g., Creative Commons). *) 454 + 455 + val language : t -> string Labeled.t option 456 + (** Video language. *) 457 + 458 + val channel : t -> Channel_summary.t option 459 + (** Channel that published the video. *) 460 + 461 + val account : t -> Account_summary.t option 462 + (** Account that published the video. *) 463 + 464 + val jsont : t Jsont.t 465 + (** JSON codec. *) 466 + 467 + val pp : Format.formatter -> t -> unit 468 + (** Pretty printer. *) 469 + end 470 + 471 + (** {1 Playlist Types} *) 472 + 473 + (** PeerTube video playlists. 474 + 475 + Playlists contain ordered lists of videos with optional start/stop 476 + timestamps for each entry. This module includes submodules for 477 + {!Playlist.Privacy}, {!Playlist.Type}, and {!Playlist.Element}. *) 478 + module Playlist : sig 479 + (** {2 Playlist Privacy} *) 480 + 481 + (** Playlist privacy levels. *) 482 + module Privacy : sig 483 + (** Playlist privacy level. 484 + - [Public] - Visible to everyone 485 + - [Unlisted] - Only accessible via direct link 486 + - [Private] - Only visible to the owner *) 487 + type t = Public | Unlisted | Private 488 + 489 + val to_int : t -> int 490 + (** [to_int t] converts privacy to its API integer representation. *) 491 + 492 + val of_int : int -> t 493 + (** [of_int n] converts an API integer to privacy level. *) 494 + 495 + val jsont : t Jsont.t 496 + (** JSON codec. *) 497 + 498 + val pp : Format.formatter -> t -> unit 499 + (** Pretty printer. *) 500 + end 501 + 502 + (** {2 Playlist Type} *) 503 + 504 + (** Playlist types. *) 505 + module Type : sig 506 + (** Playlist type. 507 + - [Regular] - User-created playlist 508 + - [WatchLater] - Special "Watch Later" playlist *) 509 + type t = Regular | WatchLater 510 + 511 + val to_int : t -> int 512 + (** [to_int t] converts type to its API integer representation. *) 513 + 514 + val of_int : int -> t 515 + (** [of_int n] converts an API integer to playlist type. *) 516 + 517 + val jsont : t Jsont.t 518 + (** JSON codec. *) 519 + 520 + val pp : Format.formatter -> t -> unit 521 + (** Pretty printer. *) 522 + end 523 + 524 + (** {2 Playlist Element} *) 525 + 526 + (** An element in a PeerTube playlist. 527 + 528 + Playlist elements wrap videos with positioning and optional timestamp 529 + information. *) 530 + module Element : sig 531 + type t 532 + (** Playlist element type. *) 533 + 534 + val make : 535 + id:int -> 536 + position:int -> 537 + start_timestamp:int option -> 538 + stop_timestamp:int option -> 539 + video:Video.t option -> 540 + t 541 + (** Create a playlist element. *) 542 + 543 + val id : t -> int 544 + (** Element ID. *) 545 + 546 + val position : t -> int 547 + (** Position in playlist (1-indexed). *) 548 + 549 + val start_timestamp : t -> int option 550 + (** Start timestamp in seconds (for partial playback). *) 551 + 552 + val stop_timestamp : t -> int option 553 + (** Stop timestamp in seconds (for partial playback). *) 554 + 555 + val video : t -> Video.t option 556 + (** The video (may be [None] if video was deleted). *) 557 + 558 + val jsont : t Jsont.t 559 + (** JSON codec. *) 560 + 561 + val pp : Format.formatter -> t -> unit 562 + (** Pretty printer. *) 563 + end 564 + 565 + (** {2 Playlist} *) 566 + 567 + type t 568 + (** Playlist type. *) 569 + 570 + val make : 571 + id:int -> 572 + uuid:string -> 573 + short_uuid:string option -> 574 + display_name:string -> 575 + description:string option -> 576 + privacy:Privacy.t -> 577 + url:string -> 578 + thumbnail_path:string option -> 579 + videos_length:int -> 580 + playlist_type:Type.t -> 581 + created_at:Ptime.t -> 582 + updated_at:Ptime.t -> 583 + owner_account:Account_summary.t option -> 584 + video_channel:Channel_summary.t option -> 585 + t 586 + (** Create a playlist. *) 587 + 588 + val id : t -> int 589 + (** Numeric playlist ID. *) 590 + 591 + val uuid : t -> string 592 + (** Playlist UUID. *) 593 + 594 + val short_uuid : t -> string option 595 + (** Short UUID for compact URLs. *) 596 + 597 + val display_name : t -> string 598 + (** Display name. *) 599 + 600 + val description : t -> string option 601 + (** Playlist description. *) 602 + 603 + val privacy : t -> Privacy.t 604 + (** Privacy level. *) 605 + 606 + val url : t -> string 607 + (** Full playlist URL. *) 608 + 609 + val thumbnail_path : t -> string option 610 + (** Thumbnail image path (relative to instance URL). *) 611 + 612 + val videos_length : t -> int 613 + (** Number of videos in playlist. *) 614 + 615 + val playlist_type : t -> Type.t 616 + (** Playlist type (regular or watch later). *) 617 + 618 + val created_at : t -> Ptime.t 619 + (** Creation timestamp. *) 620 + 621 + val updated_at : t -> Ptime.t 622 + (** Last update timestamp. *) 623 + 624 + val owner_account : t -> Account_summary.t option 625 + (** Account that owns this playlist. *) 626 + 627 + val video_channel : t -> Channel_summary.t option 628 + (** Associated video channel (if any). *) 629 + 630 + val jsont : t Jsont.t 631 + (** JSON codec. *) 632 + 633 + val pp : Format.formatter -> t -> unit 634 + (** Pretty printer. *) 635 + end 636 + 637 + (** {1 Server Types} *) 638 + 639 + (** Basic PeerTube instance information. 640 + 641 + Contains the instance name, description, and basic configuration. *) 642 + module Instance_info : sig 643 + type t 644 + (** Instance info type. *) 645 + 646 + val make : 647 + name:string -> 648 + short_description:string -> 649 + description:string option -> 650 + terms:string option -> 651 + is_nsfw:bool -> 652 + default_nsfw_policy:string -> 653 + default_client_route:string -> 654 + t 655 + (** Create instance info. *) 656 + 657 + val name : t -> string 658 + (** Instance name. *) 659 + 660 + val short_description : t -> string 661 + (** Short description (one line). *) 662 + 663 + val description : t -> string option 664 + (** Full description (may contain markdown). *) 665 + 666 + val terms : t -> string option 667 + (** Terms of service. *) 668 + 669 + val is_nsfw : t -> bool 670 + (** Whether the instance is NSFW-focused. *) 671 + 672 + val default_nsfw_policy : t -> string 673 + (** Default NSFW display policy ("display", "blur", "do_not_list"). *) 674 + 675 + val default_client_route : t -> string 676 + (** Default client route (e.g., "/videos/trending"). *) 677 + 678 + val jsont : t Jsont.t 679 + (** JSON codec. *) 680 + 681 + val pp : Format.formatter -> t -> unit 682 + (** Pretty printer. *) 683 + end 684 + 685 + (** PeerTube server configuration. 686 + 687 + Contains server version, enabled features, and instance information. *) 688 + module Server_config : sig 689 + type t 690 + (** Server config type. *) 691 + 692 + val make : 693 + instance:Instance_info.t -> 694 + server_version:string -> 695 + server_commit:string option -> 696 + signup_allowed:bool -> 697 + signup_allowed_for_current_ip:bool -> 698 + signup_requires_email_verification:bool -> 699 + transcoding_enabled:bool -> 700 + contact_form_enabled:bool -> 701 + t 702 + (** Create server config. *) 703 + 704 + val instance : t -> Instance_info.t 705 + (** Instance information. *) 706 + 707 + val server_version : t -> string 708 + (** PeerTube server version (e.g., "5.2.0"). *) 709 + 710 + val server_commit : t -> string option 711 + (** Git commit hash (if available). *) 712 + 713 + val signup_allowed : t -> bool 714 + (** Whether new user signup is allowed. *) 715 + 716 + val signup_allowed_for_current_ip : t -> bool 717 + (** Whether signup is allowed for the current IP address. *) 718 + 719 + val signup_requires_email_verification : t -> bool 720 + (** Whether signup requires email verification. *) 721 + 722 + val transcoding_enabled : t -> bool 723 + (** Whether video transcoding is enabled. *) 724 + 725 + val contact_form_enabled : t -> bool 726 + (** Whether the contact form is enabled. *) 727 + 728 + val jsont : t Jsont.t 729 + (** JSON codec. *) 730 + 731 + val pp : Format.formatter -> t -> unit 732 + (** Pretty printer. *) 733 + end 734 + 735 + (** PeerTube server statistics. 736 + 737 + Contains usage statistics for the instance including user counts, video 738 + counts, and federation information. *) 739 + module Server_stats : sig 740 + type t 741 + (** Server stats type. *) 742 + 743 + val make : 744 + total_users:int -> 745 + total_daily_active_users:int -> 746 + total_weekly_active_users:int -> 747 + total_monthly_active_users:int -> 748 + total_local_videos:int -> 749 + total_local_video_views:int -> 750 + total_local_video_comments:int -> 751 + total_local_video_files_size:int64 -> 752 + total_videos:int -> 753 + total_video_comments:int -> 754 + total_local_video_channels:int -> 755 + total_local_playlists:int -> 756 + total_instance_followers:int -> 757 + total_instance_following:int -> 758 + t 759 + (** Create server stats. *) 760 + 761 + val total_users : t -> int 762 + (** Total registered users. *) 763 + 764 + val total_daily_active_users : t -> int 765 + (** Users active in the last day. *) 766 + 767 + val total_weekly_active_users : t -> int 768 + (** Users active in the last week. *) 769 + 770 + val total_monthly_active_users : t -> int 771 + (** Users active in the last month. *) 772 + 773 + val total_local_videos : t -> int 774 + (** Videos hosted on this instance. *) 775 + 776 + val total_local_video_views : t -> int 777 + (** Total views on local videos. *) 778 + 779 + val total_local_video_comments : t -> int 780 + (** Total comments on local videos. *) 781 + 782 + val total_local_video_files_size : t -> int64 783 + (** Total size of local video files in bytes. *) 784 + 785 + val total_videos : t -> int 786 + (** Total videos including federated content. *) 787 + 788 + val total_video_comments : t -> int 789 + (** Total comments including federated content. *) 790 + 791 + val total_local_video_channels : t -> int 792 + (** Video channels on this instance. *) 793 + 794 + val total_local_playlists : t -> int 795 + (** Playlists on this instance. *) 796 + 797 + val total_instance_followers : t -> int 798 + (** Instances following this instance. *) 799 + 800 + val total_instance_following : t -> int 801 + (** Instances this instance follows. *) 802 + 803 + val jsont : t Jsont.t 804 + (** JSON codec. *) 805 + 806 + val pp : Format.formatter -> t -> unit 807 + (** Pretty printer. *) 808 + end 809 + 810 + (** {1 API Client} *) 811 + 812 + (** PeerTube API client functions. 813 + 814 + Create a session with {!Client.create}, then use it for API operations. 815 + Functions raise {!Client.Api_error} on HTTP errors. 816 + 817 + {[ 818 + let client = Client.create ~session ~base_url:"https://framatube.org" in 819 + let videos = Client.list_videos client () in 820 + ... 821 + ]} *) 822 + module Client : sig 823 + (** {2 Session} *) 824 + 825 + type t 826 + (** A PeerTube API session. 827 + 828 + Encapsulates the HTTP client and base URL for making API requests. In 829 + future, this may also contain authentication credentials. *) 830 + 831 + val create : session:Requests.t -> base_url:string -> t 832 + (** [create ~session ~base_url] creates a new PeerTube API session. 833 + 834 + @param session HTTP client session from the Requests library 835 + @param base_url 836 + Base URL of the PeerTube instance (e.g., "https://framatube.org") *) 837 + 838 + val base_url : t -> string 839 + (** [base_url t] returns the base URL of the PeerTube instance. *) 840 + 841 + val http_session : t -> Requests.t 842 + (** [http_session t] returns the underlying HTTP session. *) 843 + 844 + (** {2 Errors} *) 845 + 846 + val log_src : Logs.src 847 + (** Log source for debug/info messages. *) 848 + 849 + exception Api_error of int * string 850 + (** API error with HTTP status code and message. Raised when the server 851 + returns an error response. *) 852 + 853 + (** {2 Video Operations} *) 854 + 855 + val list_videos : 856 + t -> 857 + ?count:int -> 858 + ?start:int -> 859 + ?sort:Video_sort.t -> 860 + ?nsfw:bool -> 861 + ?is_local:bool -> 862 + ?is_live:bool -> 863 + ?category_id:int -> 864 + ?tags:string -> 865 + unit -> 866 + Video.t Paginated.t 867 + (** List videos with optional filtering and sorting. 868 + 869 + @param count Number of videos per page (default: 20) 870 + @param start Starting offset for pagination (default: 0) 871 + @param sort Sort order (default: {!Video_sort.Newest}) 872 + @param nsfw Include NSFW videos (default: false) 873 + @param is_local Only local videos 874 + @param is_live Only live streams 875 + @param category_id Filter by category ID 876 + @param tags Filter by tags (comma-separated) *) 877 + 878 + val search_videos : 879 + t -> 880 + query:string -> 881 + ?count:int -> 882 + ?start:int -> 883 + ?sort:Video_sort.t -> 884 + ?search_target:[ `Local | `Search_index ] -> 885 + ?duration_min:int -> 886 + ?duration_max:int -> 887 + ?published_after:Ptime.t -> 888 + ?published_before:Ptime.t -> 889 + unit -> 890 + Video.t Paginated.t 891 + (** Search for videos. 892 + 893 + @param query Search query string 894 + @param count Number of results per page (default: 20) 895 + @param start Starting offset (default: 0) 896 + @param sort Sort order (default: {!Video_sort.Best}) 897 + @param search_target 898 + [`Local] for this instance, [`Search_index] for federated search 899 + @param duration_min Minimum duration in seconds 900 + @param duration_max Maximum duration in seconds 901 + @param published_after Only videos published after this date 902 + @param published_before Only videos published before this date *) 903 + 904 + val fetch_channel_videos : 905 + t -> 906 + ?count:int -> 907 + ?start:int -> 908 + channel:string -> 909 + unit -> 910 + Video.t Paginated.t 911 + (** Fetch videos from a channel with pagination. 912 + 913 + @param channel Channel name (e.g., "my_channel") *) 914 + 915 + val fetch_all_channel_videos : 916 + t -> 917 + ?page_size:int -> 918 + ?max_pages:int -> 919 + channel:string -> 920 + unit -> 921 + Video.t list 922 + (** Fetch all videos from a channel using automatic pagination. 923 + 924 + Repeatedly fetches pages until all videos are retrieved or [max_pages] is 925 + reached. 926 + 927 + @param page_size Videos per page (default: 20) 928 + @param max_pages Maximum pages to fetch (default: unlimited) *) 929 + 930 + val fetch_video_details : t -> uuid:string -> unit -> Video.t 931 + (** Fetch detailed information for a single video. 932 + 933 + @param uuid Video UUID *) 934 + 935 + val get_categories : t -> unit -> (int * string) list 936 + (** Get available video categories. 937 + 938 + Returns a list of [(id, label)] pairs. *) 939 + 940 + val get_licences : t -> unit -> (int * string) list 941 + (** Get available video licences. 942 + 943 + Returns a list of [(id, label)] pairs. *) 944 + 945 + val get_languages : t -> unit -> (string * string) list 946 + (** Get available video languages. 947 + 948 + Returns a list of [(code, label)] pairs. *) 949 + 950 + (** {2 Channel Operations} *) 951 + 952 + val list_channels : 953 + t -> 954 + ?count:int -> 955 + ?start:int -> 956 + ?sort:string -> 957 + unit -> 958 + Channel.t Paginated.t 959 + (** List all channels on the instance. 960 + 961 + @param sort Sort order (e.g., "-createdAt" for newest first) *) 962 + 963 + val search_channels : 964 + t -> 965 + query:string -> 966 + ?count:int -> 967 + ?start:int -> 968 + unit -> 969 + Channel.t Paginated.t 970 + (** Search for channels. 971 + 972 + @param query Search query string *) 973 + 974 + val get_channel : t -> handle:string -> unit -> Channel.t 975 + (** Get details for a specific channel. 976 + 977 + @param handle 978 + Channel handle (e.g., "my_channel" or "my_channel@example.com") *) 979 + 980 + (** {2 Account Operations} *) 981 + 982 + val list_accounts : 983 + t -> 984 + ?count:int -> 985 + ?start:int -> 986 + ?sort:string -> 987 + unit -> 988 + Account.t Paginated.t 989 + (** List accounts on the instance. 990 + 991 + @param sort Sort order (e.g., "-createdAt" for newest first) *) 992 + 993 + val get_account : t -> handle:string -> unit -> Account.t 994 + (** Get details for a specific account. 995 + 996 + @param handle Account handle (e.g., "username" or "username@example.com") 997 + *) 998 + 999 + val get_account_videos : 1000 + t -> 1001 + ?count:int -> 1002 + ?start:int -> 1003 + handle:string -> 1004 + unit -> 1005 + Video.t Paginated.t 1006 + (** Get videos from a specific account. *) 1007 + 1008 + val get_account_channels : 1009 + t -> 1010 + ?count:int -> 1011 + ?start:int -> 1012 + handle:string -> 1013 + unit -> 1014 + Channel.t Paginated.t 1015 + (** Get channels owned by an account. *) 1016 + 1017 + (** {2 Playlist Operations} *) 1018 + 1019 + val list_playlists : 1020 + t -> ?count:int -> ?start:int -> unit -> Playlist.t Paginated.t 1021 + (** List playlists on the instance. *) 1022 + 1023 + val search_playlists : 1024 + t -> 1025 + query:string -> 1026 + ?count:int -> 1027 + ?start:int -> 1028 + unit -> 1029 + Playlist.t Paginated.t 1030 + (** Search for playlists. 1031 + 1032 + @param query Search query string *) 1033 + 1034 + val get_playlist : t -> id:string -> unit -> Playlist.t 1035 + (** Get details for a specific playlist. 1036 + 1037 + @param id Playlist ID or UUID *) 1038 + 1039 + val get_playlist_videos : 1040 + t -> 1041 + ?count:int -> 1042 + ?start:int -> 1043 + id:string -> 1044 + unit -> 1045 + Playlist.Element.t Paginated.t 1046 + (** Get videos in a playlist. 1047 + 1048 + @param id Playlist ID or UUID *) 1049 + 1050 + val get_account_playlists : 1051 + t -> 1052 + ?count:int -> 1053 + ?start:int -> 1054 + handle:string -> 1055 + unit -> 1056 + Playlist.t Paginated.t 1057 + (** Get playlists owned by an account. *) 1058 + 1059 + (** {2 Server Operations} *) 1060 + 1061 + val get_config : t -> unit -> Server_config.t 1062 + (** Get server configuration. *) 1063 + 1064 + val get_stats : t -> unit -> Server_stats.t 1065 + (** Get server statistics. *) 1066 + 1067 + (** {2 Utilities} *) 1068 + 1069 + val thumbnail_url : t -> Video.t -> string option 1070 + (** Get the full thumbnail URL for a video. 1071 + 1072 + Combines the session's base URL with the video's thumbnail path. Returns 1073 + [None] if the video has no thumbnail. *) 1074 + 1075 + val download_thumbnail : 1076 + t -> 1077 + video:Video.t -> 1078 + output_path:string -> 1079 + unit -> 1080 + (unit, [ `Msg of string ]) result 1081 + (** Download a video's thumbnail to a file. 1082 + 1083 + @param output_path Path to save the thumbnail image *) 1084 + 1085 + val to_tuple : Video.t -> string * Ptime.t * string * string * string * string 1086 + (** Convert a video to a tuple for external use. 1087 + 1088 + Returns [(description, published_date, title, url, uuid, id_string)]. *) 1089 + end
+80
lib/peertube_account.ml
··· 1 + (** Full PeerTube account details. *) 2 + 3 + type t = { 4 + summary : Peertube_account_summary.t; 5 + description : string option; 6 + created_at : Ptime.t; 7 + followers_count : int; 8 + following_count : int; 9 + following_hosts_count : int option; 10 + } 11 + 12 + let make ~summary ~description ~created_at ~followers_count ~following_count 13 + ~following_hosts_count = 14 + { 15 + summary; 16 + description; 17 + created_at; 18 + followers_count; 19 + following_count; 20 + following_hosts_count; 21 + } 22 + 23 + let summary t = t.summary 24 + let description t = t.description 25 + let created_at t = t.created_at 26 + let followers_count t = t.followers_count 27 + let following_count t = t.following_count 28 + let following_hosts_count t = t.following_hosts_count 29 + 30 + let jsont : t Jsont.t = 31 + let make id name display_name url host avatar_path description created_at 32 + followers_count following_count following_hosts_count = 33 + let summary = 34 + Peertube_account_summary.make ~id ~name ~display_name ~url ~host 35 + ~avatar_path 36 + in 37 + { 38 + summary; 39 + description; 40 + created_at; 41 + followers_count; 42 + following_count; 43 + following_hosts_count; 44 + } 45 + in 46 + Jsont.Object.map ~kind:"account" make 47 + |> Jsont.Object.mem "id" Jsont.int ~enc:(fun a -> 48 + Peertube_account_summary.id a.summary) 49 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun a -> 50 + Peertube_account_summary.name a.summary) 51 + |> Jsont.Object.mem "displayName" Jsont.string ~enc:(fun a -> 52 + Peertube_account_summary.display_name a.summary) 53 + |> Jsont.Object.mem "url" Jsont.string ~enc:(fun a -> 54 + Peertube_account_summary.url a.summary) 55 + |> Jsont.Object.mem "host" Jsont.string ~enc:(fun a -> 56 + Peertube_account_summary.host a.summary) 57 + |> Jsont.Object.mem "avatars" Peertube_avatar.jsont ~dec_absent:None 58 + ~enc_omit:Option.is_none ~enc:(fun a -> 59 + Peertube_account_summary.avatar_path a.summary) 60 + |> Jsont.Object.mem "description" (Jsont.option Jsont.string) 61 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun a -> a.description) 62 + |> Jsont.Object.mem "createdAt" Peertube_ptime.jsont ~enc:(fun a -> 63 + a.created_at) 64 + |> Jsont.Object.mem "followersCount" Jsont.int ~dec_absent:0 ~enc:(fun a -> 65 + a.followers_count) 66 + |> Jsont.Object.mem "followingCount" Jsont.int ~dec_absent:0 ~enc:(fun a -> 67 + a.following_count) 68 + |> Jsont.Object.mem "hostRedundancyAllowed" (Jsont.option Jsont.int) 69 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun a -> 70 + a.following_hosts_count) 71 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 72 + 73 + let pp ppf t = 74 + Fmt.pf ppf 75 + "@[<hov 2>{ summary = %a;@ description = %a;@ created_at = %a;@ followers \ 76 + = %d;@ following = %d }@]" 77 + Peertube_account_summary.pp t.summary 78 + Fmt.(option ~none:(any "None") (fmt "%S")) 79 + t.description Peertube_ptime.pp t.created_at t.followers_count 80 + t.following_count
+38
lib/peertube_account.mli
··· 1 + (** Full PeerTube account details. *) 2 + 3 + type t 4 + (** Account type. *) 5 + 6 + val make : 7 + summary:Peertube_account_summary.t -> 8 + description:string option -> 9 + created_at:Ptime.t -> 10 + followers_count:int -> 11 + following_count:int -> 12 + following_hosts_count:int option -> 13 + t 14 + (** Create an account. *) 15 + 16 + val summary : t -> Peertube_account_summary.t 17 + (** Account summary (basic info). *) 18 + 19 + val description : t -> string option 20 + (** Account description/bio. *) 21 + 22 + val created_at : t -> Ptime.t 23 + (** Account creation timestamp. *) 24 + 25 + val followers_count : t -> int 26 + (** Number of followers. *) 27 + 28 + val following_count : t -> int 29 + (** Number of accounts being followed. *) 30 + 31 + val following_hosts_count : t -> int option 32 + (** Number of hosts being followed (federation). *) 33 + 34 + val jsont : t Jsont.t 35 + (** JSON codec. *) 36 + 37 + val pp : Format.formatter -> t -> unit 38 + (** Pretty printer. *)
+39
lib/peertube_account_summary.ml
··· 1 + (** Summary of a PeerTube account. *) 2 + 3 + type t = { 4 + id : int; 5 + name : string; 6 + display_name : string; 7 + url : string; 8 + host : string; 9 + avatar_path : string option; 10 + } 11 + 12 + let make ~id ~name ~display_name ~url ~host ~avatar_path = 13 + { id; name; display_name; url; host; avatar_path } 14 + 15 + let id t = t.id 16 + let name t = t.name 17 + let display_name t = t.display_name 18 + let url t = t.url 19 + let host t = t.host 20 + let avatar_path t = t.avatar_path 21 + 22 + let jsont : t Jsont.t = 23 + let make id name display_name url host avatar_path = 24 + { id; name; display_name; url; host; avatar_path } 25 + in 26 + Jsont.Object.map ~kind:"account_summary" make 27 + |> Jsont.Object.mem "id" Jsont.int ~enc:(fun a -> a.id) 28 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun a -> a.name) 29 + |> Jsont.Object.mem "displayName" Jsont.string ~enc:(fun a -> a.display_name) 30 + |> Jsont.Object.mem "url" Jsont.string ~enc:(fun a -> a.url) 31 + |> Jsont.Object.mem "host" Jsont.string ~enc:(fun a -> a.host) 32 + |> Jsont.Object.mem "avatars" Peertube_avatar.jsont ~dec_absent:None 33 + ~enc_omit:Option.is_none ~enc:(fun a -> a.avatar_path) 34 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 35 + 36 + let pp ppf t = 37 + Fmt.pf ppf 38 + "@[<hov 2>{ id = %d;@ name = %S;@ display_name = %S;@ host = %S }@]" t.id 39 + t.name t.display_name t.host
+38
lib/peertube_account_summary.mli
··· 1 + (** Summary of a PeerTube account (embedded in other responses). *) 2 + 3 + type t 4 + (** Account summary type. *) 5 + 6 + val make : 7 + id:int -> 8 + name:string -> 9 + display_name:string -> 10 + url:string -> 11 + host:string -> 12 + avatar_path:string option -> 13 + t 14 + (** Create an account summary. *) 15 + 16 + val id : t -> int 17 + (** Account ID. *) 18 + 19 + val name : t -> string 20 + (** Account name (handle without host). *) 21 + 22 + val display_name : t -> string 23 + (** Display name. *) 24 + 25 + val url : t -> string 26 + (** Account URL. *) 27 + 28 + val host : t -> string 29 + (** Instance host. *) 30 + 31 + val avatar_path : t -> string option 32 + (** Avatar image path (relative). *) 33 + 34 + val jsont : t Jsont.t 35 + (** JSON codec. *) 36 + 37 + val pp : Format.formatter -> t -> unit 38 + (** Pretty printer. *)
+14
lib/peertube_avatar.ml
··· 1 + (** Avatar path extraction from PeerTube API responses. *) 2 + 3 + let avatar_jsont = 4 + let make path _width = path in 5 + Jsont.Object.map ~kind:"avatar" make 6 + |> Jsont.Object.mem "path" Jsont.string ~enc:Fun.id 7 + |> Jsont.Object.mem "width" Jsont.int ~dec_absent:0 ~enc:(Fun.const 0) 8 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 9 + 10 + let jsont : string option Jsont.t = 11 + Jsont.map ~kind:"avatar_path" 12 + ~dec:(function [] -> None | x :: _ -> Some x) 13 + ~enc:(function None -> [] | Some p -> [ p ]) 14 + (Jsont.list avatar_jsont)
+7
lib/peertube_avatar.mli
··· 1 + (** Avatar path extraction from PeerTube API responses. 2 + 3 + PeerTube returns avatars as an array of objects with path and width. This 4 + module extracts the first avatar path. *) 5 + 6 + val jsont : string option Jsont.t 7 + (** JSON codec that extracts the first avatar path from an avatars array. *)
+91
lib/peertube_channel.ml
··· 1 + (** Full PeerTube channel details. *) 2 + 3 + type t = { 4 + summary : Peertube_channel_summary.t; 5 + description : string option; 6 + support : string option; 7 + created_at : Ptime.t; 8 + followers_count : int; 9 + following_count : int; 10 + banner_path : string option; 11 + owner_account : Peertube_account_summary.t option; 12 + } 13 + 14 + let make ~summary ~description ~support ~created_at ~followers_count 15 + ~following_count ~banner_path ~owner_account = 16 + { 17 + summary; 18 + description; 19 + support; 20 + created_at; 21 + followers_count; 22 + following_count; 23 + banner_path; 24 + owner_account; 25 + } 26 + 27 + let summary t = t.summary 28 + let description t = t.description 29 + let support t = t.support 30 + let created_at t = t.created_at 31 + let followers_count t = t.followers_count 32 + let following_count t = t.following_count 33 + let banner_path t = t.banner_path 34 + let owner_account t = t.owner_account 35 + 36 + let jsont : t Jsont.t = 37 + let make id name display_name url host avatar_path description support 38 + created_at followers_count following_count banner_path owner_account = 39 + let summary = 40 + Peertube_channel_summary.make ~id ~name ~display_name ~url ~host 41 + ~avatar_path 42 + in 43 + { 44 + summary; 45 + description; 46 + support; 47 + created_at; 48 + followers_count; 49 + following_count; 50 + banner_path; 51 + owner_account; 52 + } 53 + in 54 + Jsont.Object.map ~kind:"channel" make 55 + |> Jsont.Object.mem "id" Jsont.int ~enc:(fun c -> 56 + Peertube_channel_summary.id c.summary) 57 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> 58 + Peertube_channel_summary.name c.summary) 59 + |> Jsont.Object.mem "displayName" Jsont.string ~enc:(fun c -> 60 + Peertube_channel_summary.display_name c.summary) 61 + |> Jsont.Object.mem "url" Jsont.string ~enc:(fun c -> 62 + Peertube_channel_summary.url c.summary) 63 + |> Jsont.Object.mem "host" Jsont.string ~enc:(fun c -> 64 + Peertube_channel_summary.host c.summary) 65 + |> Jsont.Object.mem "avatars" Peertube_avatar.jsont ~dec_absent:None 66 + ~enc_omit:Option.is_none ~enc:(fun c -> 67 + Peertube_channel_summary.avatar_path c.summary) 68 + |> Jsont.Object.mem "description" (Jsont.option Jsont.string) 69 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun c -> c.description) 70 + |> Jsont.Object.mem "support" (Jsont.option Jsont.string) 71 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun c -> c.support) 72 + |> Jsont.Object.mem "createdAt" Peertube_ptime.jsont ~enc:(fun c -> 73 + c.created_at) 74 + |> Jsont.Object.mem "followersCount" Jsont.int ~dec_absent:0 ~enc:(fun c -> 75 + c.followers_count) 76 + |> Jsont.Object.mem "followingCount" Jsont.int ~dec_absent:0 ~enc:(fun c -> 77 + c.following_count) 78 + |> Jsont.Object.mem "banners" Peertube_avatar.jsont ~dec_absent:None 79 + ~enc_omit:Option.is_none ~enc:(fun c -> c.banner_path) 80 + |> Jsont.Object.mem "ownerAccount" 81 + (Jsont.option Peertube_account_summary.jsont) 82 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun c -> c.owner_account) 83 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 84 + 85 + let pp ppf t = 86 + Fmt.pf ppf 87 + "@[<hov 2>{ summary = %a;@ description = %a;@ created_at = %a;@ followers \ 88 + = %d }@]" 89 + Peertube_channel_summary.pp t.summary 90 + Fmt.(option ~none:(any "None") (fmt "%S")) 91 + t.description Peertube_ptime.pp t.created_at t.followers_count
+46
lib/peertube_channel.mli
··· 1 + (** Full PeerTube channel details. *) 2 + 3 + type t 4 + (** Channel type. *) 5 + 6 + val make : 7 + summary:Peertube_channel_summary.t -> 8 + description:string option -> 9 + support:string option -> 10 + created_at:Ptime.t -> 11 + followers_count:int -> 12 + following_count:int -> 13 + banner_path:string option -> 14 + owner_account:Peertube_account_summary.t option -> 15 + t 16 + (** Create a channel. *) 17 + 18 + val summary : t -> Peertube_channel_summary.t 19 + (** Channel summary (basic info). *) 20 + 21 + val description : t -> string option 22 + (** Channel description. *) 23 + 24 + val support : t -> string option 25 + (** Support text/links. *) 26 + 27 + val created_at : t -> Ptime.t 28 + (** Channel creation timestamp. *) 29 + 30 + val followers_count : t -> int 31 + (** Number of followers. *) 32 + 33 + val following_count : t -> int 34 + (** Number of accounts being followed. *) 35 + 36 + val banner_path : t -> string option 37 + (** Banner image path (relative). *) 38 + 39 + val owner_account : t -> Peertube_account_summary.t option 40 + (** Owner account. *) 41 + 42 + val jsont : t Jsont.t 43 + (** JSON codec. *) 44 + 45 + val pp : Format.formatter -> t -> unit 46 + (** Pretty printer. *)
+39
lib/peertube_channel_summary.ml
··· 1 + (** Summary of a PeerTube channel. *) 2 + 3 + type t = { 4 + id : int; 5 + name : string; 6 + display_name : string; 7 + url : string; 8 + host : string; 9 + avatar_path : string option; 10 + } 11 + 12 + let make ~id ~name ~display_name ~url ~host ~avatar_path = 13 + { id; name; display_name; url; host; avatar_path } 14 + 15 + let id t = t.id 16 + let name t = t.name 17 + let display_name t = t.display_name 18 + let url t = t.url 19 + let host t = t.host 20 + let avatar_path t = t.avatar_path 21 + 22 + let jsont : t Jsont.t = 23 + let make id name display_name url host avatar_path = 24 + { id; name; display_name; url; host; avatar_path } 25 + in 26 + Jsont.Object.map ~kind:"channel_summary" make 27 + |> Jsont.Object.mem "id" Jsont.int ~enc:(fun c -> c.id) 28 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun c -> c.name) 29 + |> Jsont.Object.mem "displayName" Jsont.string ~enc:(fun c -> c.display_name) 30 + |> Jsont.Object.mem "url" Jsont.string ~enc:(fun c -> c.url) 31 + |> Jsont.Object.mem "host" Jsont.string ~enc:(fun c -> c.host) 32 + |> Jsont.Object.mem "avatars" Peertube_avatar.jsont ~dec_absent:None 33 + ~enc_omit:Option.is_none ~enc:(fun c -> c.avatar_path) 34 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 35 + 36 + let pp ppf t = 37 + Fmt.pf ppf 38 + "@[<hov 2>{ id = %d;@ name = %S;@ display_name = %S;@ host = %S }@]" t.id 39 + t.name t.display_name t.host
+38
lib/peertube_channel_summary.mli
··· 1 + (** Summary of a PeerTube channel (embedded in other responses). *) 2 + 3 + type t 4 + (** Channel summary type. *) 5 + 6 + val make : 7 + id:int -> 8 + name:string -> 9 + display_name:string -> 10 + url:string -> 11 + host:string -> 12 + avatar_path:string option -> 13 + t 14 + (** Create a channel summary. *) 15 + 16 + val id : t -> int 17 + (** Channel ID. *) 18 + 19 + val name : t -> string 20 + (** Channel name (handle without host). *) 21 + 22 + val display_name : t -> string 23 + (** Display name. *) 24 + 25 + val url : t -> string 26 + (** Channel URL. *) 27 + 28 + val host : t -> string 29 + (** Instance host. *) 30 + 31 + val avatar_path : t -> string option 32 + (** Avatar image path (relative). *) 33 + 34 + val jsont : t Jsont.t 35 + (** JSON codec. *) 36 + 37 + val pp : Format.formatter -> t -> unit 38 + (** Pretty printer. *)
+502
lib/peertube_client.ml
··· 1 + (** PeerTube API client functions. *) 2 + 3 + type t = { session : Requests.t; base_url : string } 4 + (** Session type encapsulating HTTP client and base URL. *) 5 + 6 + let create ~session ~base_url = { session; base_url } 7 + let base_url t = t.base_url 8 + let http_session t = t.session 9 + let log_src = Logs.Src.create "peertube" ~doc:"PeerTube API client" 10 + 11 + module Log = (val Logs.src_log log_src : Logs.LOG) 12 + 13 + exception Api_error of int * string 14 + 15 + (* Error handling *) 16 + 17 + let error_response_jsont : string Jsont.t = 18 + let make _type error _status detail = 19 + match detail with 20 + | Some d -> d 21 + | None -> Option.value ~default:"Unknown error" error 22 + in 23 + Jsont.Object.map ~kind:"error_response" make 24 + |> Jsont.Object.mem "type" Jsont.string ~dec_absent:"" ~enc:(Fun.const "") 25 + |> Jsont.Object.mem "error" 26 + (Jsont.option Jsont.string) 27 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(Fun.const None) 28 + |> Jsont.Object.mem "status" Jsont.int ~dec_absent:0 ~enc:(Fun.const 0) 29 + |> Jsont.Object.mem "detail" 30 + (Jsont.option Jsont.string) 31 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:Option.some 32 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 33 + 34 + let raise_api_error status body = 35 + let msg = 36 + match Jsont_bytesrw.decode_string error_response_jsont body with 37 + | Ok msg -> msg 38 + | Error _ -> Printf.sprintf "HTTP %d" status 39 + in 40 + Log.err (fun m -> m "API error %d: %s" status msg); 41 + raise (Api_error (status, msg)) 42 + 43 + (* HTTP helpers *) 44 + 45 + let get_json ~session ~url codec = 46 + Log.debug (fun m -> m "GET %s" url); 47 + let response = Requests.get session url in 48 + let status = Requests.Response.status_code response in 49 + if Requests.Response.ok response then begin 50 + Log.debug (fun m -> m "Response: %d OK" status); 51 + let body = Requests.Response.text response in 52 + match Jsont_bytesrw.decode_string codec body with 53 + | Ok result -> result 54 + | Error e -> 55 + Log.err (fun m -> m "JSON parse error: %s" e); 56 + failwith (Fmt.str "JSON parse error: %s" e) 57 + end 58 + else begin 59 + let body = Requests.Response.text response in 60 + raise_api_error status body 61 + end 62 + 63 + let build_url base parts params = 64 + let path = String.concat "/" parts in 65 + let params = List.filter_map Fun.id params in 66 + let query = 67 + if params = [] then "" 68 + else "?" ^ String.concat "&" (List.map (fun (k, v) -> k ^ "=" ^ v) params) 69 + in 70 + base ^ "/" ^ path ^ query 71 + 72 + (* Video operations *) 73 + 74 + let video_paginated_jsont = 75 + Peertube_paginated.jsont ~kind:"video_paginated" Peertube_video.jsont 76 + 77 + let list_videos t ?(count = 20) ?(start = 0) 78 + ?(sort = Peertube_video_sort.Newest) ?(nsfw = false) ?is_local ?is_live 79 + ?category_id ?tags () = 80 + let url = 81 + build_url t.base_url [ "api"; "v1"; "videos" ] 82 + [ 83 + Some ("count", string_of_int count); 84 + Some ("start", string_of_int start); 85 + Some ("sort", Peertube_video_sort.to_string sort); 86 + Some ("nsfw", if nsfw then "true" else "false"); 87 + Option.map 88 + (fun b -> ("isLocal", if b then "true" else "false")) 89 + is_local; 90 + Option.map (fun b -> ("isLive", if b then "true" else "false")) is_live; 91 + Option.map (fun i -> ("categoryOneOf", string_of_int i)) category_id; 92 + Option.map (fun t -> ("tagsOneOf", t)) tags; 93 + ] 94 + in 95 + let result = get_json ~session:t.session ~url video_paginated_jsont in 96 + Log.info (fun m -> 97 + m "Listed %d videos (total: %d)" 98 + (List.length (Peertube_paginated.data result)) 99 + (Peertube_paginated.total result)); 100 + result 101 + 102 + let search_videos t ~query ?(count = 20) ?(start = 0) 103 + ?(sort = Peertube_video_sort.Best) ?(search_target = `Local) ?duration_min 104 + ?duration_max ?published_after ?published_before () = 105 + let url = 106 + build_url t.base_url 107 + [ "api"; "v1"; "search"; "videos" ] 108 + [ 109 + Some ("search", query); 110 + Some ("count", string_of_int count); 111 + Some ("start", string_of_int start); 112 + Some ("sort", Peertube_video_sort.to_string sort); 113 + Some 114 + ( "searchTarget", 115 + match search_target with 116 + | `Local -> "local" 117 + | `Search_index -> "search-index" ); 118 + Option.map (fun d -> ("durationMin", string_of_int d)) duration_min; 119 + Option.map (fun d -> ("durationMax", string_of_int d)) duration_max; 120 + Option.map (fun t -> ("startDate", Ptime.to_rfc3339 t)) published_after; 121 + Option.map (fun t -> ("endDate", Ptime.to_rfc3339 t)) published_before; 122 + ] 123 + in 124 + let result = get_json ~session:t.session ~url video_paginated_jsont in 125 + Log.info (fun m -> 126 + m "Search '%s' found %d videos (total: %d)" query 127 + (List.length (Peertube_paginated.data result)) 128 + (Peertube_paginated.total result)); 129 + result 130 + 131 + let fetch_channel_videos t ?(count = 20) ?(start = 0) ~channel () = 132 + let url = 133 + build_url t.base_url 134 + [ "api"; "v1"; "video-channels"; channel; "videos" ] 135 + [ 136 + Some ("count", string_of_int count); Some ("start", string_of_int start); 137 + ] 138 + in 139 + let result = get_json ~session:t.session ~url video_paginated_jsont in 140 + Log.info (fun m -> 141 + m "Fetched %d videos from channel %s (total: %d)" 142 + (List.length (Peertube_paginated.data result)) 143 + channel 144 + (Peertube_paginated.total result)); 145 + result 146 + 147 + let fetch_all_channel_videos t ?(page_size = 20) ?max_pages ~channel () = 148 + Log.info (fun m -> 149 + m "Fetching all videos from channel %s (page_size=%d, max_pages=%s)" 150 + channel page_size 151 + (match max_pages with None -> "unlimited" | Some n -> string_of_int n)); 152 + let rec fetch_pages page start acc = 153 + let response = fetch_channel_videos t ~count:page_size ~start ~channel () in 154 + let all_videos = acc @ Peertube_paginated.data response in 155 + let fetched_count = 156 + start + List.length (Peertube_paginated.data response) 157 + in 158 + let more_available = fetched_count < Peertube_paginated.total response in 159 + let under_max_pages = 160 + match max_pages with None -> true | Some max -> page < max 161 + in 162 + Log.debug (fun m -> 163 + m "Page %d: fetched %d, total so far %d/%d" page 164 + (List.length (Peertube_paginated.data response)) 165 + fetched_count 166 + (Peertube_paginated.total response)); 167 + if more_available && under_max_pages then 168 + fetch_pages (page + 1) fetched_count all_videos 169 + else begin 170 + Log.info (fun m -> 171 + m "Finished fetching %d videos in %d pages" (List.length all_videos) 172 + page); 173 + all_videos 174 + end 175 + in 176 + fetch_pages 1 0 [] 177 + 178 + let fetch_video_details t ~uuid () = 179 + let url = build_url t.base_url [ "api"; "v1"; "videos"; uuid ] [] in 180 + let video = get_json ~session:t.session ~url Peertube_video.jsont in 181 + Log.info (fun m -> 182 + m "Fetched video details: %s (%s)" (Peertube_video.name video) uuid); 183 + video 184 + 185 + (* Categories/languages/licences lookup *) 186 + 187 + module StringMap = Map.Make (String) 188 + 189 + let dict_jsont (key_codec : 'a Jsont.t) : ('a * string) list Jsont.t = 190 + let map_jsont = Jsont.(Object.as_string_map string) in 191 + Jsont.map ~kind:"dict" 192 + ~dec:(fun smap -> 193 + let bindings = StringMap.bindings smap in 194 + List.filter_map 195 + (fun (k, v) -> 196 + match Jsont_bytesrw.decode_string key_codec ("\"" ^ k ^ "\"") with 197 + | Ok key -> Some (key, v) 198 + | Error _ -> None) 199 + bindings) 200 + ~enc:(fun pairs -> 201 + List.fold_left 202 + (fun m (k, v) -> 203 + match Jsont_bytesrw.encode_string key_codec k with 204 + | Ok s -> 205 + let key = String.sub s 1 (String.length s - 2) in 206 + StringMap.add key v m 207 + | Error _ -> m) 208 + StringMap.empty pairs) 209 + map_jsont 210 + 211 + let get_categories t () = 212 + let url = build_url t.base_url [ "api"; "v1"; "videos"; "categories" ] [] in 213 + let result = get_json ~session:t.session ~url (dict_jsont Jsont.int) in 214 + Log.info (fun m -> m "Fetched %d categories" (List.length result)); 215 + result 216 + 217 + let get_licences t () = 218 + let url = build_url t.base_url [ "api"; "v1"; "videos"; "licences" ] [] in 219 + let result = get_json ~session:t.session ~url (dict_jsont Jsont.int) in 220 + Log.info (fun m -> m "Fetched %d licences" (List.length result)); 221 + result 222 + 223 + let get_languages t () = 224 + let url = build_url t.base_url [ "api"; "v1"; "videos"; "languages" ] [] in 225 + let result = get_json ~session:t.session ~url (dict_jsont Jsont.string) in 226 + Log.info (fun m -> m "Fetched %d languages" (List.length result)); 227 + result 228 + 229 + (* Channel operations *) 230 + 231 + let channel_paginated_jsont = 232 + Peertube_paginated.jsont ~kind:"channel_paginated" Peertube_channel.jsont 233 + 234 + let list_channels t ?(count = 20) ?(start = 0) ?(sort = "-createdAt") () = 235 + let url = 236 + build_url t.base_url 237 + [ "api"; "v1"; "video-channels" ] 238 + [ 239 + Some ("count", string_of_int count); 240 + Some ("start", string_of_int start); 241 + Some ("sort", sort); 242 + ] 243 + in 244 + let result = get_json ~session:t.session ~url channel_paginated_jsont in 245 + Log.info (fun m -> 246 + m "Listed %d channels (total: %d)" 247 + (List.length (Peertube_paginated.data result)) 248 + (Peertube_paginated.total result)); 249 + result 250 + 251 + let search_channels t ~query ?(count = 20) ?(start = 0) () = 252 + let url = 253 + build_url t.base_url 254 + [ "api"; "v1"; "search"; "video-channels" ] 255 + [ 256 + Some ("search", query); 257 + Some ("count", string_of_int count); 258 + Some ("start", string_of_int start); 259 + ] 260 + in 261 + let result = get_json ~session:t.session ~url channel_paginated_jsont in 262 + Log.info (fun m -> 263 + m "Search '%s' found %d channels (total: %d)" query 264 + (List.length (Peertube_paginated.data result)) 265 + (Peertube_paginated.total result)); 266 + result 267 + 268 + let get_channel t ~handle () = 269 + let url = build_url t.base_url [ "api"; "v1"; "video-channels"; handle ] [] in 270 + let channel = get_json ~session:t.session ~url Peertube_channel.jsont in 271 + Log.info (fun m -> 272 + m "Fetched channel: %s" 273 + (Peertube_channel_summary.display_name 274 + (Peertube_channel.summary channel))); 275 + channel 276 + 277 + (* Account operations *) 278 + 279 + let account_paginated_jsont = 280 + Peertube_paginated.jsont ~kind:"account_paginated" Peertube_account.jsont 281 + 282 + let list_accounts t ?(count = 20) ?(start = 0) ?(sort = "-createdAt") () = 283 + let url = 284 + build_url t.base_url 285 + [ "api"; "v1"; "accounts" ] 286 + [ 287 + Some ("count", string_of_int count); 288 + Some ("start", string_of_int start); 289 + Some ("sort", sort); 290 + ] 291 + in 292 + let result = get_json ~session:t.session ~url account_paginated_jsont in 293 + Log.info (fun m -> 294 + m "Listed %d accounts (total: %d)" 295 + (List.length (Peertube_paginated.data result)) 296 + (Peertube_paginated.total result)); 297 + result 298 + 299 + let get_account t ~handle () = 300 + let url = build_url t.base_url [ "api"; "v1"; "accounts"; handle ] [] in 301 + let account = get_json ~session:t.session ~url Peertube_account.jsont in 302 + Log.info (fun m -> 303 + m "Fetched account: %s" 304 + (Peertube_account_summary.display_name 305 + (Peertube_account.summary account))); 306 + account 307 + 308 + let get_account_videos t ?(count = 20) ?(start = 0) ~handle () = 309 + let url = 310 + build_url t.base_url 311 + [ "api"; "v1"; "accounts"; handle; "videos" ] 312 + [ 313 + Some ("count", string_of_int count); Some ("start", string_of_int start); 314 + ] 315 + in 316 + let result = get_json ~session:t.session ~url video_paginated_jsont in 317 + Log.info (fun m -> 318 + m "Fetched %d videos from account %s (total: %d)" 319 + (List.length (Peertube_paginated.data result)) 320 + handle 321 + (Peertube_paginated.total result)); 322 + result 323 + 324 + let get_account_channels t ?(count = 20) ?(start = 0) ~handle () = 325 + let url = 326 + build_url t.base_url 327 + [ "api"; "v1"; "accounts"; handle; "video-channels" ] 328 + [ 329 + Some ("count", string_of_int count); Some ("start", string_of_int start); 330 + ] 331 + in 332 + let result = get_json ~session:t.session ~url channel_paginated_jsont in 333 + Log.info (fun m -> 334 + m "Fetched %d channels from account %s (total: %d)" 335 + (List.length (Peertube_paginated.data result)) 336 + handle 337 + (Peertube_paginated.total result)); 338 + result 339 + 340 + (* Playlist operations *) 341 + 342 + let playlist_paginated_jsont = 343 + Peertube_paginated.jsont ~kind:"playlist_paginated" Peertube_playlist.jsont 344 + 345 + let playlist_element_paginated_jsont = 346 + Peertube_paginated.jsont ~kind:"playlist_element_paginated" 347 + Peertube_playlist_element.jsont 348 + 349 + let list_playlists t ?(count = 20) ?(start = 0) () = 350 + let url = 351 + build_url t.base_url 352 + [ "api"; "v1"; "video-playlists" ] 353 + [ 354 + Some ("count", string_of_int count); Some ("start", string_of_int start); 355 + ] 356 + in 357 + let result = get_json ~session:t.session ~url playlist_paginated_jsont in 358 + Log.info (fun m -> 359 + m "Listed %d playlists (total: %d)" 360 + (List.length (Peertube_paginated.data result)) 361 + (Peertube_paginated.total result)); 362 + result 363 + 364 + let search_playlists t ~query ?(count = 20) ?(start = 0) () = 365 + let url = 366 + build_url t.base_url 367 + [ "api"; "v1"; "search"; "video-playlists" ] 368 + [ 369 + Some ("search", query); 370 + Some ("count", string_of_int count); 371 + Some ("start", string_of_int start); 372 + ] 373 + in 374 + let result = get_json ~session:t.session ~url playlist_paginated_jsont in 375 + Log.info (fun m -> 376 + m "Search '%s' found %d playlists (total: %d)" query 377 + (List.length (Peertube_paginated.data result)) 378 + (Peertube_paginated.total result)); 379 + result 380 + 381 + let get_playlist t ~id () = 382 + let url = build_url t.base_url [ "api"; "v1"; "video-playlists"; id ] [] in 383 + let playlist = get_json ~session:t.session ~url Peertube_playlist.jsont in 384 + Log.info (fun m -> 385 + m "Fetched playlist: %s" (Peertube_playlist.display_name playlist)); 386 + playlist 387 + 388 + let get_playlist_videos t ?(count = 20) ?(start = 0) ~id () = 389 + let url = 390 + build_url t.base_url 391 + [ "api"; "v1"; "video-playlists"; id; "videos" ] 392 + [ 393 + Some ("count", string_of_int count); Some ("start", string_of_int start); 394 + ] 395 + in 396 + let result = 397 + get_json ~session:t.session ~url playlist_element_paginated_jsont 398 + in 399 + Log.info (fun m -> 400 + m "Fetched %d videos from playlist (total: %d)" 401 + (List.length (Peertube_paginated.data result)) 402 + (Peertube_paginated.total result)); 403 + result 404 + 405 + let get_account_playlists t ?(count = 20) ?(start = 0) ~handle () = 406 + let url = 407 + build_url t.base_url 408 + [ "api"; "v1"; "accounts"; handle; "video-playlists" ] 409 + [ 410 + Some ("count", string_of_int count); Some ("start", string_of_int start); 411 + ] 412 + in 413 + let result = get_json ~session:t.session ~url playlist_paginated_jsont in 414 + Log.info (fun m -> 415 + m "Fetched %d playlists from account %s (total: %d)" 416 + (List.length (Peertube_paginated.data result)) 417 + handle 418 + (Peertube_paginated.total result)); 419 + result 420 + 421 + (* Server operations *) 422 + 423 + let get_config t () = 424 + let url = build_url t.base_url [ "api"; "v1"; "config" ] [] in 425 + let config = get_json ~session:t.session ~url Peertube_server_config.jsont in 426 + Log.info (fun m -> 427 + m "Fetched config for: %s (v%s)" 428 + (Peertube_instance_info.name (Peertube_server_config.instance config)) 429 + (Peertube_server_config.server_version config)); 430 + config 431 + 432 + let get_stats t () = 433 + let url = build_url t.base_url [ "api"; "v1"; "server"; "stats" ] [] in 434 + let stats = get_json ~session:t.session ~url Peertube_server_stats.jsont in 435 + Log.info (fun m -> 436 + m "Fetched stats: %d users, %d videos" 437 + (Peertube_server_stats.total_users stats) 438 + (Peertube_server_stats.total_videos stats)); 439 + stats 440 + 441 + (* Utilities *) 442 + 443 + let thumbnail_url t video = 444 + Option.map 445 + (fun path -> t.base_url ^ path) 446 + (Peertube_video.thumbnail_path video) 447 + 448 + let download_thumbnail t ~video ~output_path () = 449 + match thumbnail_url t video with 450 + | None -> 451 + Log.warn (fun m -> 452 + m "No thumbnail available for video %s" (Peertube_video.uuid video)); 453 + Error 454 + (`Msg 455 + (Printf.sprintf "No thumbnail available for video %s" 456 + (Peertube_video.uuid video))) 457 + | Some url -> 458 + Log.debug (fun m -> m "Downloading thumbnail from %s" url); 459 + let response = Requests.get t.session url in 460 + let status = Requests.Response.status_code response in 461 + if Requests.Response.ok response then begin 462 + let body = Requests.Response.text response in 463 + try 464 + let oc = open_out_bin output_path in 465 + output_string oc body; 466 + close_out oc; 467 + Log.info (fun m -> 468 + m "Downloaded thumbnail for %s to %s" 469 + (Peertube_video.uuid video) 470 + output_path); 471 + Ok () 472 + with exn -> 473 + Log.err (fun m -> 474 + m "Failed to write thumbnail to %s: %s" output_path 475 + (Printexc.to_string exn)); 476 + Error 477 + (`Msg 478 + (Printf.sprintf "Failed to write thumbnail: %s" 479 + (Printexc.to_string exn))) 480 + end 481 + else begin 482 + Log.err (fun m -> 483 + m "HTTP error %d downloading thumbnail from %s" status url); 484 + Error 485 + (`Msg (Printf.sprintf "HTTP error downloading thumbnail: %d" status)) 486 + end 487 + 488 + let to_tuple video = 489 + let description = 490 + Option.value ~default:"" (Peertube_video.description video) 491 + in 492 + let published_date = 493 + Option.value 494 + ~default:(Peertube_video.published_at video) 495 + (Peertube_video.originally_published_at video) 496 + in 497 + ( description, 498 + published_date, 499 + Peertube_video.name video, 500 + Peertube_video.url video, 501 + Peertube_video.uuid video, 502 + string_of_int (Peertube_video.id video) )
+240
lib/peertube_client.mli
··· 1 + (** PeerTube API client functions. 2 + 3 + This module provides functions for interacting with the PeerTube REST API. 4 + *) 5 + 6 + (** {1 Session} *) 7 + 8 + type t 9 + (** A PeerTube API session. 10 + 11 + Encapsulates the HTTP client and base URL for making API requests. In 12 + future, this may also contain authentication credentials. *) 13 + 14 + val create : session:Requests.t -> base_url:string -> t 15 + (** [create ~session ~base_url] creates a new PeerTube API session. 16 + 17 + @param session HTTP client session from the Requests library 18 + @param base_url 19 + Base URL of the PeerTube instance (e.g., "https://framatube.org") *) 20 + 21 + val base_url : t -> string 22 + (** [base_url t] returns the base URL of the PeerTube instance. *) 23 + 24 + val http_session : t -> Requests.t 25 + (** [http_session t] returns the underlying HTTP session. *) 26 + 27 + (** {1 Logging and Errors} *) 28 + 29 + val log_src : Logs.src 30 + (** Log source for the PeerTube client. *) 31 + 32 + exception Api_error of int * string 33 + (** API error with HTTP status code and message. *) 34 + 35 + (** {1 Video Operations} *) 36 + 37 + val list_videos : 38 + t -> 39 + ?count:int -> 40 + ?start:int -> 41 + ?sort:Peertube_video_sort.t -> 42 + ?nsfw:bool -> 43 + ?is_local:bool -> 44 + ?is_live:bool -> 45 + ?category_id:int -> 46 + ?tags:string -> 47 + unit -> 48 + Peertube_video.t Peertube_paginated.t 49 + (** List videos with optional filtering and sorting. 50 + 51 + @param count Number of videos per page (default: 20) 52 + @param start Starting offset for pagination (default: 0) 53 + @param sort Sort order (default: Newest) 54 + @param nsfw Include NSFW videos (default: false) 55 + @param is_local Only local videos (default: None = both) 56 + @param is_live Only live videos (default: None = both) 57 + @param category_id Filter by category ID 58 + @param tags Filter by tags (comma-separated) *) 59 + 60 + val search_videos : 61 + t -> 62 + query:string -> 63 + ?count:int -> 64 + ?start:int -> 65 + ?sort:Peertube_video_sort.t -> 66 + ?search_target:[ `Local | `Search_index ] -> 67 + ?duration_min:int -> 68 + ?duration_max:int -> 69 + ?published_after:Ptime.t -> 70 + ?published_before:Ptime.t -> 71 + unit -> 72 + Peertube_video.t Peertube_paginated.t 73 + (** Search for videos. 74 + 75 + @param query Search query string 76 + @param count Number of results per page (default: 20) 77 + @param start Starting offset (default: 0) 78 + @param sort Sort order (default: Best for search) 79 + @param search_target Search locally or everywhere (default: local) 80 + @param duration_min Minimum duration in seconds 81 + @param duration_max Maximum duration in seconds 82 + @param published_after Only videos published after this date 83 + @param published_before Only videos published before this date *) 84 + 85 + val fetch_channel_videos : 86 + t -> 87 + ?count:int -> 88 + ?start:int -> 89 + channel:string -> 90 + unit -> 91 + Peertube_video.t Peertube_paginated.t 92 + (** Fetch videos from a channel with pagination. *) 93 + 94 + val fetch_all_channel_videos : 95 + t -> 96 + ?page_size:int -> 97 + ?max_pages:int -> 98 + channel:string -> 99 + unit -> 100 + Peertube_video.t list 101 + (** Fetch all videos from a channel using automatic pagination. *) 102 + 103 + val fetch_video_details : t -> uuid:string -> unit -> Peertube_video.t 104 + (** Fetch detailed information for a single video by UUID. *) 105 + 106 + val get_categories : t -> unit -> (int * string) list 107 + (** Get available video categories. *) 108 + 109 + val get_licences : t -> unit -> (int * string) list 110 + (** Get available video licences. *) 111 + 112 + val get_languages : t -> unit -> (string * string) list 113 + (** Get available video languages. *) 114 + 115 + (** {1 Channel Operations} *) 116 + 117 + val list_channels : 118 + t -> 119 + ?count:int -> 120 + ?start:int -> 121 + ?sort:string -> 122 + unit -> 123 + Peertube_channel.t Peertube_paginated.t 124 + (** List all channels on the instance. 125 + 126 + @param count Number per page (default: 20) 127 + @param start Starting offset (default: 0) 128 + @param sort Sort order: createdAt, -createdAt, etc. *) 129 + 130 + val search_channels : 131 + t -> 132 + query:string -> 133 + ?count:int -> 134 + ?start:int -> 135 + unit -> 136 + Peertube_channel.t Peertube_paginated.t 137 + (** Search for channels. 138 + 139 + @param query Search query string *) 140 + 141 + val get_channel : t -> handle:string -> unit -> Peertube_channel.t 142 + (** Get details for a specific channel. *) 143 + 144 + (** {1 Account Operations} *) 145 + 146 + val list_accounts : 147 + t -> 148 + ?count:int -> 149 + ?start:int -> 150 + ?sort:string -> 151 + unit -> 152 + Peertube_account.t Peertube_paginated.t 153 + (** List accounts on the instance. *) 154 + 155 + val get_account : t -> handle:string -> unit -> Peertube_account.t 156 + (** Get details for a specific account. *) 157 + 158 + val get_account_videos : 159 + t -> 160 + ?count:int -> 161 + ?start:int -> 162 + handle:string -> 163 + unit -> 164 + Peertube_video.t Peertube_paginated.t 165 + (** Get videos from a specific account. *) 166 + 167 + val get_account_channels : 168 + t -> 169 + ?count:int -> 170 + ?start:int -> 171 + handle:string -> 172 + unit -> 173 + Peertube_channel.t Peertube_paginated.t 174 + (** Get channels owned by an account. *) 175 + 176 + (** {1 Playlist Operations} *) 177 + 178 + val list_playlists : 179 + t -> 180 + ?count:int -> 181 + ?start:int -> 182 + unit -> 183 + Peertube_playlist.t Peertube_paginated.t 184 + (** List playlists on the instance. *) 185 + 186 + val search_playlists : 187 + t -> 188 + query:string -> 189 + ?count:int -> 190 + ?start:int -> 191 + unit -> 192 + Peertube_playlist.t Peertube_paginated.t 193 + (** Search for playlists. *) 194 + 195 + val get_playlist : t -> id:string -> unit -> Peertube_playlist.t 196 + (** Get details for a specific playlist. *) 197 + 198 + val get_playlist_videos : 199 + t -> 200 + ?count:int -> 201 + ?start:int -> 202 + id:string -> 203 + unit -> 204 + Peertube_playlist_element.t Peertube_paginated.t 205 + (** Get videos in a playlist. *) 206 + 207 + val get_account_playlists : 208 + t -> 209 + ?count:int -> 210 + ?start:int -> 211 + handle:string -> 212 + unit -> 213 + Peertube_playlist.t Peertube_paginated.t 214 + (** Get playlists owned by an account. *) 215 + 216 + (** {1 Server Operations} *) 217 + 218 + val get_config : t -> unit -> Peertube_server_config.t 219 + (** Get server configuration. *) 220 + 221 + val get_stats : t -> unit -> Peertube_server_stats.t 222 + (** Get server statistics. *) 223 + 224 + (** {1 Utilities} *) 225 + 226 + val thumbnail_url : t -> Peertube_video.t -> string option 227 + (** Get the full thumbnail URL for a video. *) 228 + 229 + val download_thumbnail : 230 + t -> 231 + video:Peertube_video.t -> 232 + output_path:string -> 233 + unit -> 234 + (unit, [ `Msg of string ]) result 235 + (** Download a video's thumbnail to a file. *) 236 + 237 + val to_tuple : 238 + Peertube_video.t -> string * Ptime.t * string * string * string * string 239 + (** Convert a video to a tuple for external use. Returns 240 + [(description, published_date, title, url, uuid, id_string)]. *)
+64
lib/peertube_instance_info.ml
··· 1 + (** Basic PeerTube server/instance information. *) 2 + 3 + type t = { 4 + name : string; 5 + short_description : string; 6 + description : string option; 7 + terms : string option; 8 + is_nsfw : bool; 9 + default_nsfw_policy : string; 10 + default_client_route : string; 11 + } 12 + 13 + let make ~name ~short_description ~description ~terms ~is_nsfw 14 + ~default_nsfw_policy ~default_client_route = 15 + { 16 + name; 17 + short_description; 18 + description; 19 + terms; 20 + is_nsfw; 21 + default_nsfw_policy; 22 + default_client_route; 23 + } 24 + 25 + let name t = t.name 26 + let short_description t = t.short_description 27 + let description t = t.description 28 + let terms t = t.terms 29 + let is_nsfw t = t.is_nsfw 30 + let default_nsfw_policy t = t.default_nsfw_policy 31 + let default_client_route t = t.default_client_route 32 + 33 + let jsont : t Jsont.t = 34 + let make name short_description description terms is_nsfw default_nsfw_policy 35 + default_client_route = 36 + { 37 + name; 38 + short_description; 39 + description; 40 + terms; 41 + is_nsfw; 42 + default_nsfw_policy; 43 + default_client_route; 44 + } 45 + in 46 + Jsont.Object.map ~kind:"instance_info" make 47 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun i -> i.name) 48 + |> Jsont.Object.mem "shortDescription" Jsont.string ~dec_absent:"" 49 + ~enc:(fun i -> i.short_description) 50 + |> Jsont.Object.mem "description" (Jsont.option Jsont.string) 51 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun i -> i.description) 52 + |> Jsont.Object.mem "terms" (Jsont.option Jsont.string) 53 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun i -> i.terms) 54 + |> Jsont.Object.mem "isNSFW" Jsont.bool ~dec_absent:false ~enc:(fun i -> 55 + i.is_nsfw) 56 + |> Jsont.Object.mem "defaultNSFWPolicy" Jsont.string ~dec_absent:"display" 57 + ~enc:(fun i -> i.default_nsfw_policy) 58 + |> Jsont.Object.mem "defaultClientRoute" Jsont.string 59 + ~dec_absent:"/videos/trending" ~enc:(fun i -> i.default_client_route) 60 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 61 + 62 + let pp ppf t = 63 + Fmt.pf ppf "@[<hov 2>{ name = %S;@ short_description = %S;@ is_nsfw = %b }@]" 64 + t.name t.short_description t.is_nsfw
+42
lib/peertube_instance_info.mli
··· 1 + (** Basic PeerTube server/instance information. *) 2 + 3 + type t 4 + (** Instance info type. *) 5 + 6 + val make : 7 + name:string -> 8 + short_description:string -> 9 + description:string option -> 10 + terms:string option -> 11 + is_nsfw:bool -> 12 + default_nsfw_policy:string -> 13 + default_client_route:string -> 14 + t 15 + (** Create instance info. *) 16 + 17 + val name : t -> string 18 + (** Instance name. *) 19 + 20 + val short_description : t -> string 21 + (** Short description. *) 22 + 23 + val description : t -> string option 24 + (** Full description. *) 25 + 26 + val terms : t -> string option 27 + (** Terms of service. *) 28 + 29 + val is_nsfw : t -> bool 30 + (** Whether the instance is NSFW-focused. *) 31 + 32 + val default_nsfw_policy : t -> string 33 + (** Default NSFW display policy. *) 34 + 35 + val default_client_route : t -> string 36 + (** Default client route. *) 37 + 38 + val jsont : t Jsont.t 39 + (** JSON codec. *) 40 + 41 + val pp : Format.formatter -> t -> unit 42 + (** Pretty printer. *)
+24
lib/peertube_labeled.ml
··· 1 + (** Labeled values with an ID and display label. *) 2 + 3 + type 'a t = { id : 'a; label : string } 4 + 5 + let make ~id ~label = { id; label } 6 + let id t = t.id 7 + let label t = t.label 8 + 9 + let int_jsont : int t Jsont.t = 10 + let make id label = { id; label } in 11 + Jsont.Object.map ~kind:"int_labeled" make 12 + |> Jsont.Object.mem "id" Jsont.int ~enc:(fun l -> l.id) 13 + |> Jsont.Object.mem "label" Jsont.string ~enc:(fun l -> l.label) 14 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 15 + 16 + let string_jsont : string t Jsont.t = 17 + let make id label = { id; label } in 18 + Jsont.Object.map ~kind:"string_labeled" make 19 + |> Jsont.Object.mem "id" Jsont.string ~enc:(fun l -> l.id) 20 + |> Jsont.Object.mem "label" Jsont.string ~enc:(fun l -> l.label) 21 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 22 + 23 + let pp pp_id ppf t = 24 + Fmt.pf ppf "@[<hov 2>{ id = %a;@ label = %S }@]" pp_id t.id t.label
+25
lib/peertube_labeled.mli
··· 1 + (** Labeled values with an ID and display label. 2 + 3 + Used for categories, licences, languages, and other enumerated values in the 4 + PeerTube API. *) 5 + 6 + type 'a t 7 + (** A labeled value with an ID and human-readable label. *) 8 + 9 + val make : id:'a -> label:string -> 'a t 10 + (** Create a labeled value. *) 11 + 12 + val id : 'a t -> 'a 13 + (** Get the ID. *) 14 + 15 + val label : 'a t -> string 16 + (** Get the label. *) 17 + 18 + val int_jsont : int t Jsont.t 19 + (** JSON codec for labeled values with int IDs. *) 20 + 21 + val string_jsont : string t Jsont.t 22 + (** JSON codec for labeled values with string IDs. *) 23 + 24 + val pp : (Format.formatter -> 'a -> unit) -> Format.formatter -> 'a t -> unit 25 + (** Pretty printer for labeled values. *)
+19
lib/peertube_paginated.ml
··· 1 + (** Paginated API responses. *) 2 + 3 + type 'a t = { total : int; data : 'a list } 4 + 5 + let make ~total ~data = { total; data } 6 + let total t = t.total 7 + let data t = t.data 8 + 9 + let jsont ~kind item_jsont = 10 + let make total data = { total; data } in 11 + Jsont.Object.map ~kind make 12 + |> Jsont.Object.mem "total" Jsont.int ~enc:(fun r -> r.total) 13 + |> Jsont.Object.mem "data" (Jsont.list item_jsont) ~enc:(fun r -> r.data) 14 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 15 + 16 + let pp pp_item ppf t = 17 + Fmt.pf ppf "@[<hov 2>{ total = %d;@ data = %a }@]" t.total 18 + Fmt.(brackets (list ~sep:semi pp_item)) 19 + t.data
+19
lib/peertube_paginated.mli
··· 1 + (** Paginated API responses. *) 2 + 3 + type 'a t 4 + (** A paginated response containing total count and data list. *) 5 + 6 + val make : total:int -> data:'a list -> 'a t 7 + (** Create a paginated response. *) 8 + 9 + val total : 'a t -> int 10 + (** Total number of items available. *) 11 + 12 + val data : 'a t -> 'a list 13 + (** Items in this page. *) 14 + 15 + val jsont : kind:string -> 'a Jsont.t -> 'a t Jsont.t 16 + (** Create a JSON codec for paginated responses. *) 17 + 18 + val pp : (Format.formatter -> 'a -> unit) -> Format.formatter -> 'a t -> unit 19 + (** Pretty printer for paginated responses. *)
+110
lib/peertube_playlist.ml
··· 1 + (** PeerTube video playlist. *) 2 + 3 + type t = { 4 + id : int; 5 + uuid : string; 6 + short_uuid : string option; 7 + display_name : string; 8 + description : string option; 9 + privacy : Peertube_playlist_privacy.t; 10 + url : string; 11 + thumbnail_path : string option; 12 + videos_length : int; 13 + playlist_type : Peertube_playlist_type.t; 14 + created_at : Ptime.t; 15 + updated_at : Ptime.t; 16 + owner_account : Peertube_account_summary.t option; 17 + video_channel : Peertube_channel_summary.t option; 18 + } 19 + 20 + let make ~id ~uuid ~short_uuid ~display_name ~description ~privacy ~url 21 + ~thumbnail_path ~videos_length ~playlist_type ~created_at ~updated_at 22 + ~owner_account ~video_channel = 23 + { 24 + id; 25 + uuid; 26 + short_uuid; 27 + display_name; 28 + description; 29 + privacy; 30 + url; 31 + thumbnail_path; 32 + videos_length; 33 + playlist_type; 34 + created_at; 35 + updated_at; 36 + owner_account; 37 + video_channel; 38 + } 39 + 40 + let id t = t.id 41 + let uuid t = t.uuid 42 + let short_uuid t = t.short_uuid 43 + let display_name t = t.display_name 44 + let description t = t.description 45 + let privacy t = t.privacy 46 + let url t = t.url 47 + let thumbnail_path t = t.thumbnail_path 48 + let videos_length t = t.videos_length 49 + let playlist_type t = t.playlist_type 50 + let created_at t = t.created_at 51 + let updated_at t = t.updated_at 52 + let owner_account t = t.owner_account 53 + let video_channel t = t.video_channel 54 + 55 + let jsont : t Jsont.t = 56 + let make id uuid short_uuid display_name description privacy url 57 + thumbnail_path videos_length playlist_type created_at updated_at 58 + owner_account video_channel = 59 + { 60 + id; 61 + uuid; 62 + short_uuid; 63 + display_name; 64 + description; 65 + privacy; 66 + url; 67 + thumbnail_path; 68 + videos_length; 69 + playlist_type; 70 + created_at; 71 + updated_at; 72 + owner_account; 73 + video_channel; 74 + } 75 + in 76 + Jsont.Object.map ~kind:"playlist" make 77 + |> Jsont.Object.mem "id" Jsont.int ~enc:(fun p -> p.id) 78 + |> Jsont.Object.mem "uuid" Jsont.string ~enc:(fun p -> p.uuid) 79 + |> Jsont.Object.mem "shortUUID" (Jsont.option Jsont.string) 80 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun p -> p.short_uuid) 81 + |> Jsont.Object.mem "displayName" Jsont.string ~enc:(fun p -> p.display_name) 82 + |> Jsont.Object.mem "description" (Jsont.option Jsont.string) 83 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun p -> p.description) 84 + |> Jsont.Object.mem "privacy" Peertube_playlist_privacy.jsont ~enc:(fun p -> 85 + p.privacy) 86 + |> Jsont.Object.mem "url" Jsont.string ~enc:(fun p -> p.url) 87 + |> Jsont.Object.mem "thumbnailPath" (Jsont.option Jsont.string) 88 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun p -> 89 + p.thumbnail_path) 90 + |> Jsont.Object.mem "videosLength" Jsont.int ~dec_absent:0 ~enc:(fun p -> 91 + p.videos_length) 92 + |> Jsont.Object.mem "type" Peertube_playlist_type.jsont ~enc:(fun p -> 93 + p.playlist_type) 94 + |> Jsont.Object.mem "createdAt" Peertube_ptime.jsont ~enc:(fun p -> 95 + p.created_at) 96 + |> Jsont.Object.mem "updatedAt" Peertube_ptime.jsont ~enc:(fun p -> 97 + p.updated_at) 98 + |> Jsont.Object.mem "ownerAccount" 99 + (Jsont.option Peertube_account_summary.jsont) 100 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun p -> p.owner_account) 101 + |> Jsont.Object.mem "videoChannel" 102 + (Jsont.option Peertube_channel_summary.jsont) 103 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun p -> p.video_channel) 104 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 105 + 106 + let pp ppf t = 107 + Fmt.pf ppf 108 + "@[<hov 2>{ id = %d;@ uuid = %S;@ display_name = %S;@ videos_length = %d \ 109 + }@]" 110 + t.id t.uuid t.display_name t.videos_length
+70
lib/peertube_playlist.mli
··· 1 + (** PeerTube video playlist. *) 2 + 3 + type t 4 + (** Playlist type. *) 5 + 6 + val make : 7 + id:int -> 8 + uuid:string -> 9 + short_uuid:string option -> 10 + display_name:string -> 11 + description:string option -> 12 + privacy:Peertube_playlist_privacy.t -> 13 + url:string -> 14 + thumbnail_path:string option -> 15 + videos_length:int -> 16 + playlist_type:Peertube_playlist_type.t -> 17 + created_at:Ptime.t -> 18 + updated_at:Ptime.t -> 19 + owner_account:Peertube_account_summary.t option -> 20 + video_channel:Peertube_channel_summary.t option -> 21 + t 22 + (** Create a playlist. *) 23 + 24 + val id : t -> int 25 + (** Playlist ID. *) 26 + 27 + val uuid : t -> string 28 + (** Playlist UUID. *) 29 + 30 + val short_uuid : t -> string option 31 + (** Short UUID. *) 32 + 33 + val display_name : t -> string 34 + (** Display name. *) 35 + 36 + val description : t -> string option 37 + (** Description. *) 38 + 39 + val privacy : t -> Peertube_playlist_privacy.t 40 + (** Privacy level. *) 41 + 42 + val url : t -> string 43 + (** Playlist URL. *) 44 + 45 + val thumbnail_path : t -> string option 46 + (** Thumbnail path (relative). *) 47 + 48 + val videos_length : t -> int 49 + (** Number of videos in playlist. *) 50 + 51 + val playlist_type : t -> Peertube_playlist_type.t 52 + (** Playlist type. *) 53 + 54 + val created_at : t -> Ptime.t 55 + (** Creation timestamp. *) 56 + 57 + val updated_at : t -> Ptime.t 58 + (** Last update timestamp. *) 59 + 60 + val owner_account : t -> Peertube_account_summary.t option 61 + (** Owner account. *) 62 + 63 + val video_channel : t -> Peertube_channel_summary.t option 64 + (** Associated video channel. *) 65 + 66 + val jsont : t Jsont.t 67 + (** JSON codec. *) 68 + 69 + val pp : Format.formatter -> t -> unit 70 + (** Pretty printer. *)
+39
lib/peertube_playlist_element.ml
··· 1 + (** An element in a PeerTube playlist. *) 2 + 3 + type t = { 4 + id : int; 5 + position : int; 6 + start_timestamp : int option; 7 + stop_timestamp : int option; 8 + video : Peertube_video.t option; 9 + } 10 + 11 + let make ~id ~position ~start_timestamp ~stop_timestamp ~video = 12 + { id; position; start_timestamp; stop_timestamp; video } 13 + 14 + let id t = t.id 15 + let position t = t.position 16 + let start_timestamp t = t.start_timestamp 17 + let stop_timestamp t = t.stop_timestamp 18 + let video t = t.video 19 + 20 + let jsont : t Jsont.t = 21 + let make id position start_timestamp stop_timestamp video = 22 + { id; position; start_timestamp; stop_timestamp; video } 23 + in 24 + Jsont.Object.map ~kind:"playlist_element" make 25 + |> Jsont.Object.mem "id" Jsont.int ~enc:(fun e -> e.id) 26 + |> Jsont.Object.mem "position" Jsont.int ~enc:(fun e -> e.position) 27 + |> Jsont.Object.mem "startTimestamp" (Jsont.option Jsont.int) ~dec_absent:None 28 + ~enc_omit:Option.is_none ~enc:(fun e -> e.start_timestamp) 29 + |> Jsont.Object.mem "stopTimestamp" (Jsont.option Jsont.int) ~dec_absent:None 30 + ~enc_omit:Option.is_none ~enc:(fun e -> e.stop_timestamp) 31 + |> Jsont.Object.mem "video" (Jsont.option Peertube_video.jsont) 32 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun e -> e.video) 33 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 34 + 35 + let pp ppf t = 36 + Fmt.pf ppf "@[<hov 2>{ id = %d;@ position = %d;@ video = %a }@]" t.id 37 + t.position 38 + Fmt.(option ~none:(any "None") Peertube_video.pp) 39 + t.video
+34
lib/peertube_playlist_element.mli
··· 1 + (** An element in a PeerTube playlist. *) 2 + 3 + type t 4 + (** Playlist element type. *) 5 + 6 + val make : 7 + id:int -> 8 + position:int -> 9 + start_timestamp:int option -> 10 + stop_timestamp:int option -> 11 + video:Peertube_video.t option -> 12 + t 13 + (** Create a playlist element. *) 14 + 15 + val id : t -> int 16 + (** Element ID. *) 17 + 18 + val position : t -> int 19 + (** Position in playlist (0-indexed). *) 20 + 21 + val start_timestamp : t -> int option 22 + (** Start timestamp in seconds. *) 23 + 24 + val stop_timestamp : t -> int option 25 + (** Stop timestamp in seconds. *) 26 + 27 + val video : t -> Peertube_video.t option 28 + (** The video. *) 29 + 30 + val jsont : t Jsont.t 31 + (** JSON codec. *) 32 + 33 + val pp : Format.formatter -> t -> unit 34 + (** Pretty printer. *)
+18
lib/peertube_playlist_privacy.ml
··· 1 + (** Playlist privacy levels in PeerTube. *) 2 + 3 + type t = Public | Unlisted | Private 4 + 5 + let to_int = function Public -> 1 | Unlisted -> 2 | Private -> 3 6 + let of_int = function 1 -> Public | 2 -> Unlisted | 3 -> Private | _ -> Public 7 + 8 + let jsont : t Jsont.t = 9 + let make id _label = of_int id in 10 + Jsont.Object.map ~kind:"playlist_privacy" make 11 + |> Jsont.Object.mem "id" Jsont.int ~enc:to_int 12 + |> Jsont.Object.mem "label" Jsont.string ~dec_absent:"" ~enc:(Fun.const "") 13 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 14 + 15 + let pp ppf = function 16 + | Public -> Fmt.string ppf "Public" 17 + | Unlisted -> Fmt.string ppf "Unlisted" 18 + | Private -> Fmt.string ppf "Private"
+16
lib/peertube_playlist_privacy.mli
··· 1 + (** Playlist privacy levels in PeerTube. *) 2 + 3 + (** Playlist privacy level. *) 4 + type t = Public | Unlisted | Private 5 + 6 + val to_int : t -> int 7 + (** Convert privacy to its integer representation. *) 8 + 9 + val of_int : int -> t 10 + (** Convert integer to privacy level. *) 11 + 12 + val jsont : t Jsont.t 13 + (** JSON codec for playlist privacy. *) 14 + 15 + val pp : Format.formatter -> t -> unit 16 + (** Pretty printer for playlist privacy. *)
+17
lib/peertube_playlist_type.ml
··· 1 + (** Playlist types in PeerTube. *) 2 + 3 + type t = Regular | WatchLater 4 + 5 + let to_int = function Regular -> 1 | WatchLater -> 2 6 + let of_int = function 1 -> Regular | 2 -> WatchLater | _ -> Regular 7 + 8 + let jsont : t Jsont.t = 9 + let make id _label = of_int id in 10 + Jsont.Object.map ~kind:"playlist_type" make 11 + |> Jsont.Object.mem "id" Jsont.int ~enc:to_int 12 + |> Jsont.Object.mem "label" Jsont.string ~dec_absent:"" ~enc:(Fun.const "") 13 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 14 + 15 + let pp ppf = function 16 + | Regular -> Fmt.string ppf "Regular" 17 + | WatchLater -> Fmt.string ppf "WatchLater"
+16
lib/peertube_playlist_type.mli
··· 1 + (** Playlist types in PeerTube. *) 2 + 3 + (** Playlist type. *) 4 + type t = Regular | WatchLater 5 + 6 + val to_int : t -> int 7 + (** Convert type to its integer representation. *) 8 + 9 + val of_int : int -> t 10 + (** Convert integer to playlist type. *) 11 + 12 + val jsont : t Jsont.t 13 + (** JSON codec for playlist type. *) 14 + 15 + val pp : Format.formatter -> t -> unit 16 + (** Pretty printer for playlist type. *)
+29
lib/peertube_privacy.ml
··· 1 + (** Video privacy levels in PeerTube. *) 2 + 3 + type t = Public | Unlisted | Private | Internal 4 + 5 + let to_int = function 6 + | Public -> 1 7 + | Unlisted -> 2 8 + | Private -> 3 9 + | Internal -> 4 10 + 11 + let of_int = function 12 + | 1 -> Public 13 + | 2 -> Unlisted 14 + | 3 -> Private 15 + | 4 -> Internal 16 + | _ -> Public 17 + 18 + let jsont : t Jsont.t = 19 + let make id _label = of_int id in 20 + Jsont.Object.map ~kind:"privacy" make 21 + |> Jsont.Object.mem "id" Jsont.int ~enc:to_int 22 + |> Jsont.Object.mem "label" Jsont.string ~dec_absent:"" ~enc:(Fun.const "") 23 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 24 + 25 + let pp ppf = function 26 + | Public -> Fmt.string ppf "Public" 27 + | Unlisted -> Fmt.string ppf "Unlisted" 28 + | Private -> Fmt.string ppf "Private" 29 + | Internal -> Fmt.string ppf "Internal"
+16
lib/peertube_privacy.mli
··· 1 + (** Video privacy levels in PeerTube. *) 2 + 3 + (** Video privacy level. *) 4 + type t = Public | Unlisted | Private | Internal 5 + 6 + val to_int : t -> int 7 + (** Convert privacy to its integer representation. *) 8 + 9 + val of_int : int -> t 10 + (** Convert integer to privacy level. *) 11 + 12 + val jsont : t Jsont.t 13 + (** JSON codec for privacy. *) 14 + 15 + val pp : Format.formatter -> t -> unit 16 + (** Pretty printer for privacy. *)
+11
lib/peertube_ptime.ml
··· 1 + (** Ptime JSON codec for RFC3339 date strings. *) 2 + 3 + let parse_date str = 4 + match Ptime.of_rfc3339 str with 5 + | Ok (date, _, _) -> Ok date 6 + | Error _ -> Error (Fmt.str "Invalid RFC3339 date: %s" str) 7 + 8 + let jsont : Ptime.t Jsont.t = 9 + Jsont.of_of_string ~kind:"ptime" ~enc:Ptime.to_rfc3339 parse_date 10 + 11 + let pp = Ptime.pp_rfc3339 ()
+7
lib/peertube_ptime.mli
··· 1 + (** Ptime JSON codec for RFC3339 date strings. *) 2 + 3 + val jsont : Ptime.t Jsont.t 4 + (** JSON codec for Ptime.t values encoded as RFC3339 strings. *) 5 + 6 + val pp : Format.formatter -> Ptime.t -> unit 7 + (** Pretty printer for Ptime.t values. *)
+101
lib/peertube_server_config.ml
··· 1 + (** PeerTube server configuration. *) 2 + 3 + type t = { 4 + instance : Peertube_instance_info.t; 5 + server_version : string; 6 + server_commit : string option; 7 + signup_allowed : bool; 8 + signup_allowed_for_current_ip : bool; 9 + signup_requires_email_verification : bool; 10 + transcoding_enabled : bool; 11 + contact_form_enabled : bool; 12 + } 13 + 14 + let make ~instance ~server_version ~server_commit ~signup_allowed 15 + ~signup_allowed_for_current_ip ~signup_requires_email_verification 16 + ~transcoding_enabled ~contact_form_enabled = 17 + { 18 + instance; 19 + server_version; 20 + server_commit; 21 + signup_allowed; 22 + signup_allowed_for_current_ip; 23 + signup_requires_email_verification; 24 + transcoding_enabled; 25 + contact_form_enabled; 26 + } 27 + 28 + let instance t = t.instance 29 + let server_version t = t.server_version 30 + let server_commit t = t.server_commit 31 + let signup_allowed t = t.signup_allowed 32 + let signup_allowed_for_current_ip t = t.signup_allowed_for_current_ip 33 + let signup_requires_email_verification t = t.signup_requires_email_verification 34 + let transcoding_enabled t = t.transcoding_enabled 35 + let contact_form_enabled t = t.contact_form_enabled 36 + 37 + let signup_jsont = 38 + let make allowed allowed_for_current_ip requires_email_verification = 39 + (allowed, allowed_for_current_ip, requires_email_verification) 40 + in 41 + Jsont.Object.map ~kind:"signup" make 42 + |> Jsont.Object.mem "allowed" Jsont.bool ~dec_absent:false 43 + ~enc:(fun (a, _, _) -> a) 44 + |> Jsont.Object.mem "allowedForCurrentIP" Jsont.bool ~dec_absent:false 45 + ~enc:(fun (_, a, _) -> a) 46 + |> Jsont.Object.mem "requiresEmailVerification" Jsont.bool ~dec_absent:false 47 + ~enc:(fun (_, _, r) -> r) 48 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 49 + 50 + let transcoding_jsont = 51 + Jsont.Object.map ~kind:"transcoding" Fun.id 52 + |> Jsont.Object.mem "enabled" Jsont.bool ~dec_absent:false ~enc:Fun.id 53 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 54 + 55 + let contact_form_jsont = 56 + Jsont.Object.map ~kind:"contact_form" Fun.id 57 + |> Jsont.Object.mem "enabled" Jsont.bool ~dec_absent:false ~enc:Fun.id 58 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 59 + 60 + let jsont : t Jsont.t = 61 + let make instance server_version server_commit signup transcoding contact_form 62 + = 63 + let ( signup_allowed, 64 + signup_allowed_for_current_ip, 65 + signup_requires_email_verification ) = 66 + signup 67 + in 68 + { 69 + instance; 70 + server_version; 71 + server_commit; 72 + signup_allowed; 73 + signup_allowed_for_current_ip; 74 + signup_requires_email_verification; 75 + transcoding_enabled = transcoding; 76 + contact_form_enabled = contact_form; 77 + } 78 + in 79 + Jsont.Object.map ~kind:"server_config" make 80 + |> Jsont.Object.mem "instance" Peertube_instance_info.jsont ~enc:(fun c -> 81 + c.instance) 82 + |> Jsont.Object.mem "serverVersion" Jsont.string ~dec_absent:"unknown" 83 + ~enc:(fun c -> c.server_version) 84 + |> Jsont.Object.mem "serverCommit" (Jsont.option Jsont.string) 85 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun c -> c.server_commit) 86 + |> Jsont.Object.mem "signup" signup_jsont ~enc:(fun c -> 87 + ( c.signup_allowed, 88 + c.signup_allowed_for_current_ip, 89 + c.signup_requires_email_verification )) 90 + |> Jsont.Object.mem "transcoding" transcoding_jsont ~enc:(fun c -> 91 + c.transcoding_enabled) 92 + |> Jsont.Object.mem "contactForm" contact_form_jsont ~enc:(fun c -> 93 + c.contact_form_enabled) 94 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 95 + 96 + let pp ppf t = 97 + Fmt.pf ppf 98 + "@[<hov 2>{ instance = %a;@ server_version = %S;@ signup_allowed = %b;@ \ 99 + transcoding_enabled = %b }@]" 100 + Peertube_instance_info.pp t.instance t.server_version t.signup_allowed 101 + t.transcoding_enabled
+46
lib/peertube_server_config.mli
··· 1 + (** PeerTube server configuration. *) 2 + 3 + type t 4 + (** Server config type. *) 5 + 6 + val make : 7 + instance:Peertube_instance_info.t -> 8 + server_version:string -> 9 + server_commit:string option -> 10 + signup_allowed:bool -> 11 + signup_allowed_for_current_ip:bool -> 12 + signup_requires_email_verification:bool -> 13 + transcoding_enabled:bool -> 14 + contact_form_enabled:bool -> 15 + t 16 + (** Create server config. *) 17 + 18 + val instance : t -> Peertube_instance_info.t 19 + (** Instance information. *) 20 + 21 + val server_version : t -> string 22 + (** Server software version. *) 23 + 24 + val server_commit : t -> string option 25 + (** Server commit hash. *) 26 + 27 + val signup_allowed : t -> bool 28 + (** Whether signup is allowed. *) 29 + 30 + val signup_allowed_for_current_ip : t -> bool 31 + (** Whether signup is allowed for current IP. *) 32 + 33 + val signup_requires_email_verification : t -> bool 34 + (** Whether signup requires email verification. *) 35 + 36 + val transcoding_enabled : t -> bool 37 + (** Whether transcoding is enabled. *) 38 + 39 + val contact_form_enabled : t -> bool 40 + (** Whether contact form is enabled. *) 41 + 42 + val jsont : t Jsont.t 43 + (** JSON codec. *) 44 + 45 + val pp : Format.formatter -> t -> unit 46 + (** Pretty printer. *)
+115
lib/peertube_server_stats.ml
··· 1 + (** PeerTube server statistics. *) 2 + 3 + type t = { 4 + total_users : int; 5 + total_daily_active_users : int; 6 + total_weekly_active_users : int; 7 + total_monthly_active_users : int; 8 + total_local_videos : int; 9 + total_local_video_views : int; 10 + total_local_video_comments : int; 11 + total_local_video_files_size : int64; 12 + total_videos : int; 13 + total_video_comments : int; 14 + total_local_video_channels : int; 15 + total_local_playlists : int; 16 + total_instance_followers : int; 17 + total_instance_following : int; 18 + } 19 + 20 + let make ~total_users ~total_daily_active_users ~total_weekly_active_users 21 + ~total_monthly_active_users ~total_local_videos ~total_local_video_views 22 + ~total_local_video_comments ~total_local_video_files_size ~total_videos 23 + ~total_video_comments ~total_local_video_channels ~total_local_playlists 24 + ~total_instance_followers ~total_instance_following = 25 + { 26 + total_users; 27 + total_daily_active_users; 28 + total_weekly_active_users; 29 + total_monthly_active_users; 30 + total_local_videos; 31 + total_local_video_views; 32 + total_local_video_comments; 33 + total_local_video_files_size; 34 + total_videos; 35 + total_video_comments; 36 + total_local_video_channels; 37 + total_local_playlists; 38 + total_instance_followers; 39 + total_instance_following; 40 + } 41 + 42 + let total_users t = t.total_users 43 + let total_daily_active_users t = t.total_daily_active_users 44 + let total_weekly_active_users t = t.total_weekly_active_users 45 + let total_monthly_active_users t = t.total_monthly_active_users 46 + let total_local_videos t = t.total_local_videos 47 + let total_local_video_views t = t.total_local_video_views 48 + let total_local_video_comments t = t.total_local_video_comments 49 + let total_local_video_files_size t = t.total_local_video_files_size 50 + let total_videos t = t.total_videos 51 + let total_video_comments t = t.total_video_comments 52 + let total_local_video_channels t = t.total_local_video_channels 53 + let total_local_playlists t = t.total_local_playlists 54 + let total_instance_followers t = t.total_instance_followers 55 + let total_instance_following t = t.total_instance_following 56 + 57 + let jsont : t Jsont.t = 58 + let make total_users total_daily_active_users total_weekly_active_users 59 + total_monthly_active_users total_local_videos total_local_video_views 60 + total_local_video_comments total_local_video_files_size total_videos 61 + total_video_comments total_local_video_channels total_local_playlists 62 + total_instance_followers total_instance_following = 63 + { 64 + total_users; 65 + total_daily_active_users; 66 + total_weekly_active_users; 67 + total_monthly_active_users; 68 + total_local_videos; 69 + total_local_video_views; 70 + total_local_video_comments; 71 + total_local_video_files_size; 72 + total_videos; 73 + total_video_comments; 74 + total_local_video_channels; 75 + total_local_playlists; 76 + total_instance_followers; 77 + total_instance_following; 78 + } 79 + in 80 + Jsont.Object.map ~kind:"server_stats" make 81 + |> Jsont.Object.mem "totalUsers" Jsont.int ~dec_absent:0 ~enc:(fun s -> 82 + s.total_users) 83 + |> Jsont.Object.mem "totalDailyActiveUsers" Jsont.int ~dec_absent:0 84 + ~enc:(fun s -> s.total_daily_active_users) 85 + |> Jsont.Object.mem "totalWeeklyActiveUsers" Jsont.int ~dec_absent:0 86 + ~enc:(fun s -> s.total_weekly_active_users) 87 + |> Jsont.Object.mem "totalMonthlyActiveUsers" Jsont.int ~dec_absent:0 88 + ~enc:(fun s -> s.total_monthly_active_users) 89 + |> Jsont.Object.mem "totalLocalVideos" Jsont.int ~dec_absent:0 ~enc:(fun s -> 90 + s.total_local_videos) 91 + |> Jsont.Object.mem "totalLocalVideoViews" Jsont.int ~dec_absent:0 92 + ~enc:(fun s -> s.total_local_video_views) 93 + |> Jsont.Object.mem "totalLocalVideoComments" Jsont.int ~dec_absent:0 94 + ~enc:(fun s -> s.total_local_video_comments) 95 + |> Jsont.Object.mem "totalLocalVideoFilesSize" Jsont.int64 ~dec_absent:0L 96 + ~enc:(fun s -> s.total_local_video_files_size) 97 + |> Jsont.Object.mem "totalVideos" Jsont.int ~dec_absent:0 ~enc:(fun s -> 98 + s.total_videos) 99 + |> Jsont.Object.mem "totalVideoComments" Jsont.int ~dec_absent:0 100 + ~enc:(fun s -> s.total_video_comments) 101 + |> Jsont.Object.mem "totalLocalVideoChannels" Jsont.int ~dec_absent:0 102 + ~enc:(fun s -> s.total_local_video_channels) 103 + |> Jsont.Object.mem "totalLocalPlaylists" Jsont.int ~dec_absent:0 104 + ~enc:(fun s -> s.total_local_playlists) 105 + |> Jsont.Object.mem "totalInstanceFollowers" Jsont.int ~dec_absent:0 106 + ~enc:(fun s -> s.total_instance_followers) 107 + |> Jsont.Object.mem "totalInstanceFollowing" Jsont.int ~dec_absent:0 108 + ~enc:(fun s -> s.total_instance_following) 109 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 110 + 111 + let pp ppf t = 112 + Fmt.pf ppf 113 + "@[<hov 2>{ total_users = %d;@ total_videos = %d;@ total_local_videos = %d \ 114 + }@]" 115 + t.total_users t.total_videos t.total_local_videos
+70
lib/peertube_server_stats.mli
··· 1 + (** PeerTube server statistics. *) 2 + 3 + type t 4 + (** Server stats type. *) 5 + 6 + val make : 7 + total_users:int -> 8 + total_daily_active_users:int -> 9 + total_weekly_active_users:int -> 10 + total_monthly_active_users:int -> 11 + total_local_videos:int -> 12 + total_local_video_views:int -> 13 + total_local_video_comments:int -> 14 + total_local_video_files_size:int64 -> 15 + total_videos:int -> 16 + total_video_comments:int -> 17 + total_local_video_channels:int -> 18 + total_local_playlists:int -> 19 + total_instance_followers:int -> 20 + total_instance_following:int -> 21 + t 22 + (** Create server stats. *) 23 + 24 + val total_users : t -> int 25 + (** Total registered users. *) 26 + 27 + val total_daily_active_users : t -> int 28 + (** Daily active users. *) 29 + 30 + val total_weekly_active_users : t -> int 31 + (** Weekly active users. *) 32 + 33 + val total_monthly_active_users : t -> int 34 + (** Monthly active users. *) 35 + 36 + val total_local_videos : t -> int 37 + (** Total local videos. *) 38 + 39 + val total_local_video_views : t -> int 40 + (** Total local video views. *) 41 + 42 + val total_local_video_comments : t -> int 43 + (** Total local video comments. *) 44 + 45 + val total_local_video_files_size : t -> int64 46 + (** Total local video files size in bytes. *) 47 + 48 + val total_videos : t -> int 49 + (** Total videos (including federated). *) 50 + 51 + val total_video_comments : t -> int 52 + (** Total video comments (including federated). *) 53 + 54 + val total_local_video_channels : t -> int 55 + (** Total local video channels. *) 56 + 57 + val total_local_playlists : t -> int 58 + (** Total local playlists. *) 59 + 60 + val total_instance_followers : t -> int 61 + (** Total instance followers. *) 62 + 63 + val total_instance_following : t -> int 64 + (** Total instances being followed. *) 65 + 66 + val jsont : t Jsont.t 67 + (** JSON codec. *) 68 + 69 + val pp : Format.formatter -> t -> unit 70 + (** Pretty printer. *)
+179
lib/peertube_video.ml
··· 1 + (** PeerTube video record. *) 2 + 3 + type t = { 4 + id : int; 5 + uuid : string; 6 + short_uuid : string option; 7 + name : string; 8 + description : string option; 9 + url : string; 10 + embed_path : string; 11 + published_at : Ptime.t; 12 + originally_published_at : Ptime.t option; 13 + updated_at : Ptime.t option; 14 + thumbnail_path : string option; 15 + preview_path : string option; 16 + tags : string list; 17 + duration : int; 18 + views : int; 19 + likes : int; 20 + dislikes : int; 21 + is_local : bool; 22 + is_live : bool; 23 + privacy : Peertube_privacy.t; 24 + category : int Peertube_labeled.t option; 25 + licence : int Peertube_labeled.t option; 26 + language : string Peertube_labeled.t option; 27 + channel : Peertube_channel_summary.t option; 28 + account : Peertube_account_summary.t option; 29 + } 30 + 31 + let make ~id ~uuid ~short_uuid ~name ~description ~url ~embed_path ~published_at 32 + ~originally_published_at ~updated_at ~thumbnail_path ~preview_path ~tags 33 + ~duration ~views ~likes ~dislikes ~is_local ~is_live ~privacy ~category 34 + ~licence ~language ~channel ~account = 35 + { 36 + id; 37 + uuid; 38 + short_uuid; 39 + name; 40 + description; 41 + url; 42 + embed_path; 43 + published_at; 44 + originally_published_at; 45 + updated_at; 46 + thumbnail_path; 47 + preview_path; 48 + tags; 49 + duration; 50 + views; 51 + likes; 52 + dislikes; 53 + is_local; 54 + is_live; 55 + privacy; 56 + category; 57 + licence; 58 + language; 59 + channel; 60 + account; 61 + } 62 + 63 + let id t = t.id 64 + let uuid t = t.uuid 65 + let short_uuid t = t.short_uuid 66 + let name t = t.name 67 + let description t = t.description 68 + let url t = t.url 69 + let embed_path t = t.embed_path 70 + let published_at t = t.published_at 71 + let originally_published_at t = t.originally_published_at 72 + let updated_at t = t.updated_at 73 + let thumbnail_path t = t.thumbnail_path 74 + let preview_path t = t.preview_path 75 + let tags t = t.tags 76 + let duration t = t.duration 77 + let views t = t.views 78 + let likes t = t.likes 79 + let dislikes t = t.dislikes 80 + let is_local t = t.is_local 81 + let is_live t = t.is_live 82 + let privacy t = t.privacy 83 + let category t = t.category 84 + let licence t = t.licence 85 + let language t = t.language 86 + let channel t = t.channel 87 + let account t = t.account 88 + 89 + let jsont : t Jsont.t = 90 + let make id uuid short_uuid name description url embed_path published_at 91 + originally_published_at updated_at thumbnail_path preview_path tags 92 + duration views likes dislikes is_local is_live privacy category licence 93 + language channel account = 94 + { 95 + id; 96 + uuid; 97 + short_uuid; 98 + name; 99 + description; 100 + url; 101 + embed_path; 102 + published_at; 103 + originally_published_at; 104 + updated_at; 105 + thumbnail_path; 106 + preview_path; 107 + tags; 108 + duration; 109 + views; 110 + likes; 111 + dislikes; 112 + is_local; 113 + is_live; 114 + privacy; 115 + category; 116 + licence; 117 + language; 118 + channel; 119 + account; 120 + } 121 + in 122 + Jsont.Object.map ~kind:"video" make 123 + |> Jsont.Object.mem "id" Jsont.int ~enc:(fun v -> v.id) 124 + |> Jsont.Object.mem "uuid" Jsont.string ~enc:(fun v -> v.uuid) 125 + |> Jsont.Object.mem "shortUUID" (Jsont.option Jsont.string) 126 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.short_uuid) 127 + |> Jsont.Object.mem "name" Jsont.string ~enc:(fun v -> v.name) 128 + |> Jsont.Object.mem "description" (Jsont.option Jsont.string) 129 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.description) 130 + |> Jsont.Object.mem "url" Jsont.string ~enc:(fun v -> v.url) 131 + |> Jsont.Object.mem "embedPath" Jsont.string ~enc:(fun v -> v.embed_path) 132 + |> Jsont.Object.mem "publishedAt" Peertube_ptime.jsont ~enc:(fun v -> 133 + v.published_at) 134 + |> Jsont.Object.mem "originallyPublishedAt" 135 + (Jsont.option Peertube_ptime.jsont) 136 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> 137 + v.originally_published_at) 138 + |> Jsont.Object.mem "updatedAt" (Jsont.option Peertube_ptime.jsont) 139 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.updated_at) 140 + |> Jsont.Object.mem "thumbnailPath" (Jsont.option Jsont.string) 141 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> 142 + v.thumbnail_path) 143 + |> Jsont.Object.mem "previewPath" (Jsont.option Jsont.string) 144 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.preview_path) 145 + |> Jsont.Object.mem "tags" 146 + Jsont.(list string) 147 + ~dec_absent:[] 148 + ~enc_omit:(fun l -> l = []) 149 + ~enc:(fun v -> v.tags) 150 + |> Jsont.Object.mem "duration" Jsont.int ~dec_absent:0 ~enc:(fun v -> 151 + v.duration) 152 + |> Jsont.Object.mem "views" Jsont.int ~dec_absent:0 ~enc:(fun v -> v.views) 153 + |> Jsont.Object.mem "likes" Jsont.int ~dec_absent:0 ~enc:(fun v -> v.likes) 154 + |> Jsont.Object.mem "dislikes" Jsont.int ~dec_absent:0 ~enc:(fun v -> 155 + v.dislikes) 156 + |> Jsont.Object.mem "isLocal" Jsont.bool ~dec_absent:true ~enc:(fun v -> 157 + v.is_local) 158 + |> Jsont.Object.mem "isLive" Jsont.bool ~dec_absent:false ~enc:(fun v -> 159 + v.is_live) 160 + |> Jsont.Object.mem "privacy" Peertube_privacy.jsont 161 + ~dec_absent:Peertube_privacy.Public ~enc:(fun v -> v.privacy) 162 + |> Jsont.Object.mem "category" (Jsont.option Peertube_labeled.int_jsont) 163 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.category) 164 + |> Jsont.Object.mem "licence" (Jsont.option Peertube_labeled.int_jsont) 165 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.licence) 166 + |> Jsont.Object.mem "language" (Jsont.option Peertube_labeled.string_jsont) 167 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.language) 168 + |> Jsont.Object.mem "channel" (Jsont.option Peertube_channel_summary.jsont) 169 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.channel) 170 + |> Jsont.Object.mem "account" (Jsont.option Peertube_account_summary.jsont) 171 + ~dec_absent:None ~enc_omit:Option.is_none ~enc:(fun v -> v.account) 172 + |> Jsont.Object.skip_unknown |> Jsont.Object.finish 173 + 174 + let pp ppf t = 175 + Fmt.pf ppf 176 + "@[<hov 2>{ id = %d;@ uuid = %S;@ name = %S;@ published_at = %a;@ duration \ 177 + = %d;@ views = %d;@ likes = %d }@]" 178 + t.id t.uuid t.name Peertube_ptime.pp t.published_at t.duration t.views 179 + t.likes
+114
lib/peertube_video.mli
··· 1 + (** PeerTube video record. *) 2 + 3 + type t 4 + (** Video type. *) 5 + 6 + val make : 7 + id:int -> 8 + uuid:string -> 9 + short_uuid:string option -> 10 + name:string -> 11 + description:string option -> 12 + url:string -> 13 + embed_path:string -> 14 + published_at:Ptime.t -> 15 + originally_published_at:Ptime.t option -> 16 + updated_at:Ptime.t option -> 17 + thumbnail_path:string option -> 18 + preview_path:string option -> 19 + tags:string list -> 20 + duration:int -> 21 + views:int -> 22 + likes:int -> 23 + dislikes:int -> 24 + is_local:bool -> 25 + is_live:bool -> 26 + privacy:Peertube_privacy.t -> 27 + category:int Peertube_labeled.t option -> 28 + licence:int Peertube_labeled.t option -> 29 + language:string Peertube_labeled.t option -> 30 + channel:Peertube_channel_summary.t option -> 31 + account:Peertube_account_summary.t option -> 32 + t 33 + (** Create a video. *) 34 + 35 + val id : t -> int 36 + (** Video ID. *) 37 + 38 + val uuid : t -> string 39 + (** Video UUID. *) 40 + 41 + val short_uuid : t -> string option 42 + (** Short UUID. *) 43 + 44 + val name : t -> string 45 + (** Video title. *) 46 + 47 + val description : t -> string option 48 + (** Video description. *) 49 + 50 + val url : t -> string 51 + (** Video URL. *) 52 + 53 + val embed_path : t -> string 54 + (** Embed path. *) 55 + 56 + val published_at : t -> Ptime.t 57 + (** Publication timestamp. *) 58 + 59 + val originally_published_at : t -> Ptime.t option 60 + (** Original publication timestamp. *) 61 + 62 + val updated_at : t -> Ptime.t option 63 + (** Last update timestamp. *) 64 + 65 + val thumbnail_path : t -> string option 66 + (** Thumbnail path (relative). *) 67 + 68 + val preview_path : t -> string option 69 + (** Preview path (relative). *) 70 + 71 + val tags : t -> string list 72 + (** Video tags. *) 73 + 74 + val duration : t -> int 75 + (** Duration in seconds. *) 76 + 77 + val views : t -> int 78 + (** View count. *) 79 + 80 + val likes : t -> int 81 + (** Like count. *) 82 + 83 + val dislikes : t -> int 84 + (** Dislike count. *) 85 + 86 + val is_local : t -> bool 87 + (** Whether the video is local to this instance. *) 88 + 89 + val is_live : t -> bool 90 + (** Whether this is a live stream. *) 91 + 92 + val privacy : t -> Peertube_privacy.t 93 + (** Privacy level. *) 94 + 95 + val category : t -> int Peertube_labeled.t option 96 + (** Video category. *) 97 + 98 + val licence : t -> int Peertube_labeled.t option 99 + (** Video licence. *) 100 + 101 + val language : t -> string Peertube_labeled.t option 102 + (** Video language. *) 103 + 104 + val channel : t -> Peertube_channel_summary.t option 105 + (** Channel that published the video. *) 106 + 107 + val account : t -> Peertube_account_summary.t option 108 + (** Account that published the video. *) 109 + 110 + val jsont : t Jsont.t 111 + (** JSON codec. *) 112 + 113 + val pp : Format.formatter -> t -> unit 114 + (** Pretty printer. *)
+23
lib/peertube_video_sort.ml
··· 1 + (** Video sort options for PeerTube API queries. *) 2 + 3 + type t = Newest | Oldest | Views | Likes | Trending | Hot | Random | Best 4 + 5 + let to_string = function 6 + | Newest -> "-publishedAt" 7 + | Oldest -> "publishedAt" 8 + | Views -> "-views" 9 + | Likes -> "-likes" 10 + | Trending -> "-trending" 11 + | Hot -> "-hot" 12 + | Random -> "random" 13 + | Best -> "-best" 14 + 15 + let pp ppf = function 16 + | Newest -> Fmt.string ppf "Newest" 17 + | Oldest -> Fmt.string ppf "Oldest" 18 + | Views -> Fmt.string ppf "Views" 19 + | Likes -> Fmt.string ppf "Likes" 20 + | Trending -> Fmt.string ppf "Trending" 21 + | Hot -> Fmt.string ppf "Hot" 22 + | Random -> Fmt.string ppf "Random" 23 + | Best -> Fmt.string ppf "Best"
+10
lib/peertube_video_sort.mli
··· 1 + (** Video sort options for PeerTube API queries. *) 2 + 3 + (** Video sort order. *) 4 + type t = Newest | Oldest | Views | Likes | Trending | Hot | Random | Best 5 + 6 + val to_string : t -> string 7 + (** Convert sort option to API query string. *) 8 + 9 + val pp : Format.formatter -> t -> unit 10 + (** Pretty printer for sort options. *)
+38
peertube.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "PeerTube API client for OCaml using Eio" 4 + description: 5 + "An OCaml client library for the PeerTube video platform API, built on Eio for effect-based I/O. Includes a command-line client (opeertube) for browsing videos, channels, accounts, and playlists." 6 + maintainer: ["anil@recoil.org"] 7 + authors: ["Anil Madhavapeddy"] 8 + license: "ISC" 9 + homepage: "https://git.recoil.org/anil.recoil.org/ocaml-peertube" 10 + bug-reports: "https://git.recoil.org/anil.recoil.org/ocaml-peertube/issues" 11 + depends: [ 12 + "dune" {>= "3.16"} 13 + "ocaml" {>= "5.1.0"} 14 + "eio" {>= "1.0"} 15 + "eio_main" {>= "1.0"} 16 + "requests" {>= "0.1"} 17 + "jsont" {>= "0.1"} 18 + "ptime" {>= "1.0"} 19 + "fmt" {>= "0.9"} 20 + "logs" {>= "0.7"} 21 + "cmdliner" {>= "1.2"} 22 + "odoc" {with-doc} 23 + ] 24 + build: [ 25 + ["dune" "subst"] {dev} 26 + [ 27 + "dune" 28 + "build" 29 + "-p" 30 + name 31 + "-j" 32 + jobs 33 + "@install" 34 + "@runtest" {with-test} 35 + "@doc" {with-doc} 36 + ] 37 + ] 38 + dev-repo: "https://git.recoil.org/anil.recoil.org/ocaml-peertube"