My agentic slop goes here. Not intended for anyone else!
1(** PeerTube API client implementation (Eio version) *)
2
3(** Type representing a PeerTube client *)
4type 'net t_internal = {
5 base_url: string;
6 requests_session: (float Eio.Time.clock_ty Eio.Resource.t, 'net Eio.Net.ty Eio.Resource.t) Requests.t;
7}
8
9type t = [`Generic | `Unix] t_internal
10
11(** Create a new PeerTube client *)
12let create ~requests_session ~base_url : t =
13 { base_url; requests_session }
14
15(** Type representing a PeerTube video *)
16type video = {
17 id: int;
18 uuid: string;
19 name: string;
20 description: string option;
21 url: string;
22 embed_path: string;
23 published_at: Ptime.t;
24 originally_published_at: Ptime.t option;
25 thumbnail_path: string option;
26 tags: string list option;
27 unknown: Jsont.json;
28}
29
30(** Type for PeerTube API response containing videos *)
31type video_response = {
32 total: int;
33 data: video list;
34 unknown: Jsont.json;
35}
36
37(** Accessor functions for video *)
38let video_id (v : video) = v.id
39let video_uuid (v : video) = v.uuid
40let video_name (v : video) = v.name
41let video_description (v : video) = v.description
42let video_url (v : video) = v.url
43let video_embed_path (v : video) = v.embed_path
44let video_published_at (v : video) = v.published_at
45let video_originally_published_at (v : video) = v.originally_published_at
46let video_thumbnail_path (v : video) = v.thumbnail_path
47let video_tags (v : video) = v.tags
48let video_unknown (v : video) = v.unknown
49
50(** Accessor functions for video_response *)
51let video_response_total (vr : video_response) = vr.total
52let video_response_data (vr : video_response) = vr.data
53let video_response_unknown (vr : video_response) = vr.unknown
54
55(** RFC3339 timestamp codec *)
56module Rfc3339 = struct
57 let parse s =
58 Ptime.of_rfc3339 s |> Result.to_option |> Option.map (fun (t, _, _) -> t)
59
60 let format t = Ptime.to_rfc3339 ~frac_s:6 ~tz_offset_s:0 t
61 let pp ppf t = Format.pp_print_string ppf (format t)
62
63 let jsont =
64 let kind = "RFC 3339 timestamp" in
65 let doc = "An RFC 3339 date-time string" in
66 let dec s =
67 match parse s with
68 | Some t -> t
69 | None ->
70 Jsont.Error.msgf Jsont.Meta.none "%s: invalid RFC 3339 timestamp: %S"
71 kind s
72 in
73 Jsont.map ~kind ~doc ~dec ~enc:format Jsont.string
74end
75
76(** Jsont codec for video *)
77let video_jsont : video Jsont.t =
78 let kind = "PeerTube Video" in
79 let doc = "A PeerTube video object" in
80
81 let make_video id uuid name description url embed_path published_at
82 originally_published_at thumbnail_path tags unknown : video =
83 { id; uuid; name; description; url; embed_path; published_at;
84 originally_published_at; thumbnail_path; tags; unknown }
85 in
86
87 Jsont.Object.map ~kind ~doc make_video
88 |> Jsont.Object.mem "id" Jsont.int ~enc:video_id
89 |> Jsont.Object.mem "uuid" Jsont.string ~enc:video_uuid
90 |> Jsont.Object.mem "name" Jsont.string ~enc:video_name
91 |> Jsont.Object.opt_mem "description" Jsont.string ~enc:video_description
92 |> Jsont.Object.mem "url" Jsont.string ~enc:video_url
93 |> Jsont.Object.mem "embedPath" Jsont.string ~enc:video_embed_path
94 |> Jsont.Object.mem "publishedAt" Rfc3339.jsont ~enc:video_published_at
95 |> Jsont.Object.opt_mem "originallyPublishedAt" Rfc3339.jsont ~enc:video_originally_published_at
96 |> Jsont.Object.opt_mem "thumbnailPath" Jsont.string ~enc:video_thumbnail_path
97 |> Jsont.Object.opt_mem "tags" (Jsont.list Jsont.string) ~enc:video_tags
98 |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:video_unknown
99 |> Jsont.Object.finish
100
101(** Jsont codec for video_response *)
102let video_response_jsont =
103 let kind = "PeerTube Video Response" in
104 let doc = "A PeerTube API response containing videos" in
105
106 let make_response total data unknown =
107 { total; data; unknown }
108 in
109
110 Jsont.Object.map ~kind ~doc make_response
111 |> Jsont.Object.mem "total" Jsont.int ~enc:video_response_total
112 |> Jsont.Object.mem "data" (Jsont.list video_jsont) ~enc:video_response_data
113 |> Jsont.Object.keep_unknown Jsont.json_mems ~enc:video_response_unknown
114 |> Jsont.Object.finish
115
116(** Parse a single video from JSON string *)
117let parse_video_string s =
118 match Jsont_bytesrw.decode_string' video_jsont s with
119 | Ok video -> video
120 | Error err -> failwith (Jsont.Error.to_string err)
121
122(** Parse a video response from JSON string *)
123let parse_video_response_string s =
124 match Jsont_bytesrw.decode_string' video_response_jsont s with
125 | Ok response -> response
126 | Error err -> failwith (Jsont.Error.to_string err)
127
128(** Fetch videos from a PeerTube instance channel with pagination support
129 @param count Number of videos to fetch per page
130 @param start Starting index for pagination (0-based)
131 @param client PeerTube client
132 @param channel Channel name to fetch videos from
133 @return The video response *)
134let fetch_channel_videos client ?(count=20) ?(start=0) channel =
135 let open Requests_json_api in
136 let url = Printf.sprintf "%s/api/v1/video-channels/%s/videos?count=%d&start=%d"
137 client.base_url channel count start in
138 get_json_exn client.requests_session url video_response_jsont
139
140(** Fetch all videos from a PeerTube instance channel using pagination
141 @param page_size Number of videos to fetch per page
142 @param max_pages Maximum number of pages to fetch (None for all pages)
143 @param client PeerTube client
144 @param channel Channel name to fetch videos from
145 @return All videos combined *)
146let fetch_all_channel_videos client ?(page_size=20) ?max_pages channel =
147 let rec fetch_pages start acc _total_count =
148 let response = fetch_channel_videos client ~count:page_size ~start channel in
149 let all_videos = acc @ response.data in
150
151 (* Determine if we need to fetch more pages *)
152 let fetched_count = start + List.length response.data in
153 let more_available = fetched_count < response.total in
154 let under_max_pages = match max_pages with
155 | None -> true
156 | Some max -> (start / page_size) + 1 < max
157 in
158
159 if more_available && under_max_pages then
160 fetch_pages fetched_count all_videos response.total
161 else
162 all_videos
163 in
164 fetch_pages 0 [] 0
165
166(** Fetch detailed information for a single video by UUID
167 @param client PeerTube client
168 @param uuid UUID of the video to fetch
169 @return The complete video details *)
170let fetch_video_details client uuid =
171 let open Requests_json_api in
172 let url = client.base_url / "api/v1/videos" / uuid in
173 get_json_exn client.requests_session url video_jsont
174
175(** Convert a PeerTube video to Bushel.Video.t compatible structure *)
176let to_bushel_video video =
177 let description = Option.value ~default:"" video.description in
178 let published_date = video.originally_published_at |> Option.value ~default:video.published_at in
179 (description, published_date, video.name, video.url, video.uuid, string_of_int video.id)
180
181(** Get the thumbnail URL for a video *)
182let thumbnail_url client video =
183 match video.thumbnail_path with
184 | Some path -> Some (client.base_url ^ path)
185 | None -> None
186
187(** Download a thumbnail to a file
188 @param client PeerTube client
189 @param fs The Eio filesystem capability
190 @param video The video to download the thumbnail for
191 @param output_path Path where to save the thumbnail
192 @return Ok () on success or Error with message *)
193let download_thumbnail client ~fs video output_path =
194 match thumbnail_url client video with
195 | None ->
196 Error (`Msg (Printf.sprintf "No thumbnail available for video %s" video.uuid))
197 | Some url ->
198 try
199 let open Requests_json_api in
200 match get_result client.requests_session url with
201 | Error (status_code, _body) ->
202 Error (`Msg (Printf.sprintf "HTTP error downloading thumbnail: %d" status_code))
203 | Ok body_str ->
204 try
205 let output_eio_path = Eio.Path.(fs / output_path) in
206 Eio.Path.save ~create:(`Or_truncate 0o644) output_eio_path body_str;
207 Ok ()
208 with exn ->
209 Error (`Msg (Printf.sprintf "Failed to write thumbnail: %s"
210 (Printexc.to_string exn)))
211 with exn ->
212 Error (`Msg (Printf.sprintf "Failed to download thumbnail: %s"
213 (Printexc.to_string exn)))