My agentic slop goes here. Not intended for anyone else!

more

Changed files
+723 -150
stack
+4
stack/immiche/bin/dune
···
···
+
(executable
+
(name immiche_cli)
+
(public_name immiche)
+
(libraries immiche requests eio_main keyeio eiocmd cmdliner))
+187
stack/immiche/bin/immiche_cli.ml
···
···
+
open Cmdliner
+
+
(** List people command *)
+
let list_people env _xdg profile base_url limit hidden =
+
Eio.Switch.run @@ fun sw ->
+
+
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
+
let base_url =
+
match base_url with
+
| Some url -> url
+
| None ->
+
Keyeio.Profile.get profile ~key:"base_url"
+
|> Option.value ~default:"https://photos.example.com"
+
in
+
+
try
+
let requests_session = Requests.create ~sw env in
+
let client = Immiche.create ~requests_session ~base_url ~api_key in
+
let response = Immiche.fetch_people client in
+
+
(* Filter by hidden status if requested *)
+
let people =
+
List.filter (fun (p : Immiche.person) ->
+
match hidden with
+
| Some true -> p.is_hidden
+
| Some false -> not p.is_hidden
+
| None -> true
+
) response.people
+
in
+
+
(* Limit number of results *)
+
let people =
+
match limit with
+
| Some n -> List.filteri (fun i _ -> i < n) people
+
| None -> people
+
in
+
+
Printf.printf "Found %d people (total: %d, visible: %d)\n\n"
+
(List.length people) response.total response.visible;
+
+
List.iteri (fun i (p : Immiche.person) ->
+
Printf.printf "%d. %s\n" (i + 1) p.name;
+
Printf.printf " ID: %s\n" p.id;
+
(match p.birth_date with
+
| Some bd -> Printf.printf " Birth date: %s\n" bd
+
| None -> ());
+
Printf.printf " Thumbnail: %s\n" p.thumbnail_path;
+
if p.is_hidden then Printf.printf " [HIDDEN]\n";
+
Printf.printf "\n"
+
) people;
+
0
+
with exn ->
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
+
1
+
+
(** Get a single person by ID *)
+
let get_person env _xdg profile base_url person_id =
+
Eio.Switch.run @@ fun sw ->
+
+
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
+
let base_url =
+
match base_url with
+
| Some url -> url
+
| None ->
+
Keyeio.Profile.get profile ~key:"base_url"
+
|> Option.value ~default:"https://photos.example.com"
+
in
+
+
try
+
let requests_session = Requests.create ~sw env in
+
let client = Immiche.create ~requests_session ~base_url ~api_key in
+
let person = Immiche.fetch_person client ~person_id in
+
+
Printf.printf "Person: %s\n" person.name;
+
Printf.printf "ID: %s\n" person.id;
+
(match person.birth_date with
+
| Some bd -> Printf.printf "Birth date: %s\n" bd
+
| None -> ());
+
Printf.printf "Thumbnail: %s\n" person.thumbnail_path;
+
if person.is_hidden then Printf.printf "Status: HIDDEN\n";
+
0
+
with exn ->
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
+
1
+
+
(** Search for people by name *)
+
let search_people env _xdg profile base_url name =
+
Eio.Switch.run @@ fun sw ->
+
+
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
+
let base_url =
+
match base_url with
+
| Some url -> url
+
| None ->
+
Keyeio.Profile.get profile ~key:"base_url"
+
|> Option.value ~default:"https://photos.example.com"
+
in
+
+
try
+
let requests_session = Requests.create ~sw env in
+
let client = Immiche.create ~requests_session ~base_url ~api_key in
+
let people = Immiche.search_person client ~name in
+
+
Printf.printf "Found %d people matching '%s'\n\n" (List.length people) name;
+
+
List.iteri (fun i (p : Immiche.person) ->
+
Printf.printf "%d. %s\n" (i + 1) p.name;
+
Printf.printf " ID: %s\n" p.id;
+
(match p.birth_date with
+
| Some bd -> Printf.printf " Birth date: %s\n" bd
+
| None -> ());
+
Printf.printf " Thumbnail: %s\n" p.thumbnail_path;
+
if p.is_hidden then Printf.printf " [HIDDEN]\n";
+
Printf.printf "\n"
+
) people;
+
0
+
with exn ->
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
+
1
+
+
(** Command-line arguments *)
+
+
let base_url_arg =
+
let doc = "Base URL of the Immich instance (overrides profile setting)" in
+
Arg.(value & opt (some string) None & info ["u"; "url"] ~docv:"URL" ~doc)
+
+
let limit_arg =
+
let doc = "Maximum number of results to return" in
+
Arg.(value & opt (some int) None & info ["l"; "limit"] ~docv:"N" ~doc)
+
+
let hidden_arg =
+
let doc = "Filter by hidden status (true/false)" in
+
Arg.(value & opt (some bool) None & info ["hidden"] ~docv:"BOOL" ~doc)
+
+
let person_id_arg =
+
let doc = "ID of the person" in
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"PERSON_ID" ~doc)
+
+
let name_arg =
+
let doc = "Name to search for" in
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"NAME" ~doc)
+
+
(** Commands *)
+
+
let list_cmd =
+
let doc = "List people from Immich" in
+
Eiocmd.run
+
~info:(Cmd.info "list" ~doc)
+
~app_name:"immiche"
+
~service:"immich"
+
Term.(const (fun base_url limit hidden env xdg profile ->
+
list_people env xdg profile base_url limit hidden)
+
$ base_url_arg $ limit_arg $ hidden_arg)
+
+
let get_cmd =
+
let doc = "Get a person by ID" in
+
Eiocmd.run
+
~info:(Cmd.info "get" ~doc)
+
~app_name:"immiche"
+
~service:"immich"
+
Term.(const (fun base_url person_id env xdg profile ->
+
get_person env xdg profile base_url person_id)
+
$ base_url_arg $ person_id_arg)
+
+
let search_cmd =
+
let doc = "Search for people by name" in
+
Eiocmd.run
+
~info:(Cmd.info "search" ~doc)
+
~app_name:"immiche"
+
~service:"immich"
+
Term.(const (fun base_url name env xdg profile ->
+
search_people env xdg profile base_url name)
+
$ base_url_arg $ name_arg)
+
+
(** Main command *)
+
+
let main_cmd =
+
let doc = "Immich API client" in
+
let man = [
+
`S Manpage.s_description;
+
`P "$(tname) is a command-line client for the Immich API.";
+
`P "It allows you to list, search, and view people from your Immich instance.";
+
] in
+
let info = Cmd.info "immiche" ~version:"0.1.0" ~doc ~man in
+
Cmd.group info [list_cmd; get_cmd; search_cmd]
+
+
let () = exit (Cmd.eval' main_cmd)
+1 -1
stack/immiche/dune
···
(library
(name immiche)
(public_name immiche)
-
(libraries eio eio.core requests ezjsonm fmt ptime uri))
···
(library
(name immiche)
(public_name immiche)
+
(libraries eio eio.core requests requests_json_api ezjsonm fmt ptime uri))
+22 -50
stack/immiche/immiche.ml
···
(** {1 API Functions} *)
let fetch_people { base_url; requests_session; _ } =
-
let url = sprintf "%s/api/people" base_url in
-
-
let response = Requests.get requests_session url in
-
let status = Requests.Response.status_code response in
-
-
if status <> 200 then
-
failwith (sprintf "HTTP error: %d" status)
-
else
-
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
-
let json = Ezjsonm.from_string body_str in
-
parse_people_response json
let fetch_person { base_url; requests_session; _ } ~person_id =
-
let url = sprintf "%s/api/people/%s" base_url person_id in
-
-
let response = Requests.get requests_session url in
-
let status = Requests.Response.status_code response in
-
-
if status <> 200 then
-
failwith (sprintf "HTTP error: %d" status)
-
else
-
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
-
let json = Ezjsonm.from_string body_str in
-
parse_person json
let download_thumbnail { base_url; requests_session; _ } ~fs ~person_id ~output_path =
try
-
let url = sprintf "%s/api/people/%s/thumbnail" base_url person_id in
-
-
let response = Requests.get requests_session url in
-
let status = Requests.Response.status_code response in
-
-
if status <> 200 then
-
Error (`Msg (sprintf "HTTP error: %d" status))
-
else begin
-
let img_data = Requests.Response.body response |> Eio.Flow.read_all in
-
(* Ensure output directory exists *)
-
let dir = Filename.dirname output_path in
-
if not (Sys.file_exists dir) then
-
Unix.mkdir dir 0o755;
-
(* Write the image data to file *)
-
let path = Eio.Path.(fs / output_path) in
-
Eio.Path.save ~create:(`Or_truncate 0o644) path img_data;
-
Ok ()
-
end
with
| Failure msg -> Error (`Msg msg)
| exn -> Error (`Msg (Printexc.to_string exn))
let search_person { base_url; requests_session; _ } ~name =
let encoded_name = Uri.pct_encode name in
let url = sprintf "%s/api/search/person?name=%s" base_url encoded_name in
-
-
let response = Requests.get requests_session url in
-
let status = Requests.Response.status_code response in
-
-
if status <> 200 then
-
failwith (sprintf "HTTP error: %d" status)
-
else
-
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
-
let json = Ezjsonm.from_string body_str in
-
parse_person_list json
···
(** {1 API Functions} *)
let fetch_people { base_url; requests_session; _ } =
+
let open Requests_json_api in
+
let url = base_url / "api/people" in
+
get_json_exn requests_session url parse_people_response
let fetch_person { base_url; requests_session; _ } ~person_id =
+
let open Requests_json_api in
+
let url = base_url / "api/people" / person_id in
+
get_json_exn requests_session url parse_person
let download_thumbnail { base_url; requests_session; _ } ~fs ~person_id ~output_path =
try
+
let open Requests_json_api in
+
let url = base_url / "api/people" / person_id / "thumbnail" in
+
match get_result requests_session url with
+
| Error (status, _body) ->
+
Error (`Msg (sprintf "HTTP error: %d" status))
+
| Ok img_data ->
+
(* Ensure output directory exists *)
+
let dir = Filename.dirname output_path in
+
if not (Sys.file_exists dir) then
+
Unix.mkdir dir 0o755;
+
(* Write the image data to file *)
+
let path = Eio.Path.(fs / output_path) in
+
Eio.Path.save ~create:(`Or_truncate 0o644) path img_data;
+
Ok ()
with
| Failure msg -> Error (`Msg msg)
| exn -> Error (`Msg (Printexc.to_string exn))
let search_person { base_url; requests_session; _ } ~name =
+
let open Requests_json_api in
let encoded_name = Uri.pct_encode name in
let url = sprintf "%s/api/search/person?name=%s" base_url encoded_name in
+
get_json_exn requests_session url parse_person_list
+1 -1
stack/karakeepe/dune
···
(library
(name karakeepe)
(public_name karakeepe)
-
(libraries bushel eio eio.core requests ezjsonm fmt ptime uri logs logs.fmt))
···
(library
(name karakeepe)
(public_name karakeepe)
+
(libraries bushel eio eio.core requests requests_json_api ezjsonm fmt ptime uri logs logs.fmt))
+33 -35
stack/karakeepe/karakeepe.ml
···
try
let response = Requests.get client.http_client ~headers url in
-
let status_code = Requests.Response.status_code response in
-
if status_code = 200 then begin
-
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
-
Log.debug (fun m -> m "Received %d bytes of response data" (String.length body_str));
-
-
try
-
let json = J.from_string body_str in
-
parse_bookmark_response json
-
with e ->
-
Log.err (fun m -> m "JSON parsing error: %s" (Printexc.to_string e));
-
Log.debug (fun m -> m "Response body (first 200 chars): %s"
-
(if String.length body_str > 200 then String.sub body_str 0 200 ^ "..." else body_str));
-
raise e
-
end else begin
-
Log.err (fun m -> m "HTTP error %d" status_code);
-
failwith (Fmt.str "HTTP error: %d" status_code)
-
end
with e ->
Log.err (fun m -> m "Network error: %s" (Printexc.to_string e));
raise e
···
(** Fetch detailed information for a single bookmark by ID *)
let fetch_bookmark_details client bookmark_id =
-
let url = Fmt.str "%s/api/v1/bookmarks/%s" client.base_url bookmark_id in
let headers = Requests.Headers.empty
|> Requests.Headers.set "Authorization" ("Bearer " ^ client.api_key) in
let response = Requests.get client.http_client ~headers url in
-
let status_code = Requests.Response.status_code response in
-
if status_code = 200 then begin
-
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
-
let json = J.from_string body_str in
-
parse_bookmark json
-
end else
-
failwith (Fmt.str "HTTP error: %d" status_code)
(** Get the asset URL for a given asset ID *)
let get_asset_url client asset_id =
···
(** Fetch an asset from the Karakeep server as a binary string *)
let fetch_asset client asset_id =
-
let url = Fmt.str "%s/api/assets/%s" client.base_url asset_id in
let headers = Requests.Headers.empty
|> Requests.Headers.set "Authorization" ("Bearer " ^ client.api_key) in
let response = Requests.get client.http_client ~headers url in
-
let status_code = Requests.Response.status_code response in
-
if status_code = 200 then
-
Requests.Response.body response |> Eio.Flow.read_all
-
else
-
failwith (Fmt.str "Asset fetch error: %d" status_code)
(** Create a new bookmark in Karakeep with optional tags *)
let create_bookmark client ~url ?title ?note ?tags ?(favourited=false) ?(archived=false) () =
···
|> Requests.Headers.set "Content-Type" "application/json"
in
-
let url_endpoint = Fmt.str "%s/api/v1/bookmarks" client.base_url in
let body = Requests.Body.of_string Requests.Mime.json body_str in
let response = Requests.post client.http_client ~headers ~body url_endpoint in
let status_code = Requests.Response.status_code response in
if status_code = 201 || status_code = 200 then begin
-
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
let json = J.from_string body_str in
let bookmark = parse_bookmark json in
···
let tags_body = `O [("tags", `A tag_objects)] in
let tags_body_str = J.to_string tags_body in
-
let tags_url = Fmt.str "%s/api/v1/bookmarks/%s/tags" client.base_url bookmark.id in
let tags_body = Requests.Body.of_string Requests.Mime.json tags_body_str in
let tags_response = Requests.post client.http_client ~headers ~body:tags_body tags_url in
···
bookmark
| _ -> bookmark
end else begin
-
let error_body = Requests.Response.body response |> Eio.Flow.read_all in
failwith (Fmt.str "Failed to create bookmark. HTTP error: %d. Details: %s" status_code error_body)
end
···
try
let response = Requests.get client.http_client ~headers url in
+
match Requests_json_api.check_ok response with
+
| Ok body_str ->
+
Log.debug (fun m -> m "Received %d bytes of response data" (String.length body_str));
+
(try
+
let json = J.from_string body_str in
+
parse_bookmark_response json
+
with e ->
+
Log.err (fun m -> m "JSON parsing error: %s" (Printexc.to_string e));
+
Log.debug (fun m -> m "Response body (first 200 chars): %s"
+
(if String.length body_str > 200 then String.sub body_str 0 200 ^ "..." else body_str));
+
raise e)
+
| Error (status_code, _) ->
+
Log.err (fun m -> m "HTTP error %d" status_code);
+
failwith (Fmt.str "HTTP error: %d" status_code)
with e ->
Log.err (fun m -> m "Network error: %s" (Printexc.to_string e));
raise e
···
(** Fetch detailed information for a single bookmark by ID *)
let fetch_bookmark_details client bookmark_id =
+
let open Requests_json_api in
+
let url = client.base_url / "api/v1/bookmarks" / bookmark_id in
let headers = Requests.Headers.empty
|> Requests.Headers.set "Authorization" ("Bearer " ^ client.api_key) in
let response = Requests.get client.http_client ~headers url in
+
match check_ok response with
+
| Ok body_str ->
+
let json = J.from_string body_str in
+
parse_bookmark json
+
| Error (status_code, _) ->
+
failwith (Fmt.str "HTTP error: %d" status_code)
(** Get the asset URL for a given asset ID *)
let get_asset_url client asset_id =
···
(** Fetch an asset from the Karakeep server as a binary string *)
let fetch_asset client asset_id =
+
let open Requests_json_api in
+
let url = client.base_url / "api/assets" / asset_id in
let headers = Requests.Headers.empty
|> Requests.Headers.set "Authorization" ("Bearer " ^ client.api_key) in
let response = Requests.get client.http_client ~headers url in
+
match check_ok response with
+
| Ok body_str -> body_str
+
| Error (status_code, _) ->
+
failwith (Fmt.str "Asset fetch error: %d" status_code)
(** Create a new bookmark in Karakeep with optional tags *)
let create_bookmark client ~url ?title ?note ?tags ?(favourited=false) ?(archived=false) () =
···
|> Requests.Headers.set "Content-Type" "application/json"
in
+
let open Requests_json_api in
+
let url_endpoint = client.base_url / "api/v1/bookmarks" in
let body = Requests.Body.of_string Requests.Mime.json body_str in
let response = Requests.post client.http_client ~headers ~body url_endpoint in
let status_code = Requests.Response.status_code response in
if status_code = 201 || status_code = 200 then begin
+
let body_str = read_body response in
let json = J.from_string body_str in
let bookmark = parse_bookmark json in
···
let tags_body = `O [("tags", `A tag_objects)] in
let tags_body_str = J.to_string tags_body in
+
let tags_url = client.base_url / "api/v1/bookmarks" / bookmark.id / "tags" in
let tags_body = Requests.Body.of_string Requests.Mime.json tags_body_str in
let tags_response = Requests.post client.http_client ~headers ~body:tags_body tags_url in
···
bookmark
| _ -> bookmark
end else begin
+
let error_body = read_body response in
failwith (Fmt.str "Failed to create bookmark. HTTP error: %d. Details: %s" status_code error_body)
end
+4
stack/peertubee/bin/dune
···
···
+
(executable
+
(name peertubee_cli)
+
(public_name peertubee)
+
(libraries peertubee requests eio_main keyeio eiocmd cmdliner))
+138
stack/peertubee/bin/peertubee_cli.ml
···
···
+
open Cmdliner
+
+
(** List channel videos command *)
+
let list_videos env _xdg profile base_url channel limit =
+
Eio.Switch.run @@ fun sw ->
+
+
let base_url =
+
match base_url with
+
| Some url -> url
+
| None ->
+
Keyeio.Profile.get profile ~key:"base_url"
+
|> Option.value ~default:"https://peertube.example.com"
+
in
+
+
try
+
let requests_session = Requests.create ~sw env in
+
let client = Peertubee.create ~requests_session ~base_url in
+
let videos =
+
match limit with
+
| Some n -> Peertubee.fetch_all_channel_videos client ~page_size:n channel
+
| None -> Peertubee.fetch_all_channel_videos client channel
+
in
+
+
Printf.printf "Found %d videos from channel '%s'\n\n" (List.length videos) channel;
+
+
List.iteri (fun i (v : Peertubee.video) ->
+
Printf.printf "%d. %s\n" (i + 1) v.name;
+
Printf.printf " UUID: %s\n" v.uuid;
+
Printf.printf " URL: %s\n" v.url;
+
(match v.description with
+
| Some desc ->
+
let desc_short =
+
if String.length desc > 100 then String.sub desc 0 100 ^ "..."
+
else desc
+
in
+
Printf.printf " Description: %s\n" desc_short
+
| None -> ());
+
Printf.printf " Published: %s\n" (Ptime.to_rfc3339 v.published_at);
+
if v.tags <> [] then
+
Printf.printf " Tags: %s\n" (String.concat ", " v.tags);
+
Printf.printf "\n"
+
) videos;
+
0
+
with exn ->
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
+
1
+
+
(** Get a single video by UUID *)
+
let get_video env _xdg profile base_url uuid =
+
Eio.Switch.run @@ fun sw ->
+
+
let base_url =
+
match base_url with
+
| Some url -> url
+
| None ->
+
Keyeio.Profile.get profile ~key:"base_url"
+
|> Option.value ~default:"https://peertube.example.com"
+
in
+
+
try
+
let requests_session = Requests.create ~sw env in
+
let client = Peertubee.create ~requests_session ~base_url in
+
let video = Peertubee.fetch_video_details client uuid in
+
+
Printf.printf "Video: %s\n" video.name;
+
Printf.printf "UUID: %s\n" video.uuid;
+
Printf.printf "URL: %s\n" video.url;
+
Printf.printf "Embed path: %s\n" video.embed_path;
+
(match video.description with
+
| Some desc -> Printf.printf "Description: %s\n" desc
+
| None -> ());
+
Printf.printf "Published: %s\n" (Ptime.to_rfc3339 video.published_at);
+
(match video.originally_published_at with
+
| Some t -> Printf.printf "Originally published: %s\n" (Ptime.to_rfc3339 t)
+
| None -> ());
+
if video.tags <> [] then
+
Printf.printf "Tags: %s\n" (String.concat ", " video.tags);
+
(match Peertubee.thumbnail_url client video with
+
| Some url -> Printf.printf "Thumbnail: %s\n" url
+
| None -> ());
+
0
+
with exn ->
+
Printf.eprintf "Error: %s\n" (Printexc.to_string exn);
+
1
+
+
(** Command-line arguments *)
+
+
let base_url_arg =
+
let doc = "Base URL of the PeerTube instance (overrides profile setting)" in
+
Arg.(value & opt (some string) None & info ["u"; "url"] ~docv:"URL" ~doc)
+
+
let channel_arg =
+
let doc = "Channel name to fetch videos from" in
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"CHANNEL" ~doc)
+
+
let limit_arg =
+
let doc = "Maximum number of videos to return" in
+
Arg.(value & opt (some int) None & info ["l"; "limit"] ~docv:"N" ~doc)
+
+
let uuid_arg =
+
let doc = "UUID of the video" in
+
Arg.(required & pos 0 (some string) None & info [] ~docv:"UUID" ~doc)
+
+
(** Commands *)
+
+
let list_cmd =
+
let doc = "List videos from a PeerTube channel" in
+
Eiocmd.run
+
~info:(Cmd.info "list" ~doc)
+
~app_name:"peertubee"
+
~service:"peertube"
+
Term.(const (fun base_url channel limit env xdg profile ->
+
list_videos env xdg profile base_url channel limit)
+
$ base_url_arg $ channel_arg $ limit_arg)
+
+
let get_cmd =
+
let doc = "Get a video by UUID" in
+
Eiocmd.run
+
~info:(Cmd.info "get" ~doc)
+
~app_name:"peertubee"
+
~service:"peertube"
+
Term.(const (fun base_url uuid env xdg profile ->
+
get_video env xdg profile base_url uuid)
+
$ base_url_arg $ uuid_arg)
+
+
(** Main command *)
+
+
let main_cmd =
+
let doc = "PeerTube API client" in
+
let man = [
+
`S Manpage.s_description;
+
`P "$(tname) is a command-line client for the PeerTube API.";
+
`P "It allows you to list and view videos from PeerTube instances.";
+
] in
+
let info = Cmd.info "peertubee" ~version:"0.1.0" ~doc ~man in
+
Cmd.group info [list_cmd; get_cmd]
+
+
let () = exit (Cmd.eval' main_cmd)
+1 -1
stack/peertubee/dune
···
(library
(name peertubee)
(public_name peertubee)
-
(libraries ezjsonm eio eio.core requests ptime fmt))
···
(library
(name peertubee)
(public_name peertubee)
+
(libraries ezjsonm eio eio.core requests requests_json_api ptime fmt))
+17 -31
stack/peertubee/peertubee.ml
···
@param channel Channel name to fetch videos from
@return The video response *)
let fetch_channel_videos client ?(count=20) ?(start=0) channel =
let url = Printf.sprintf "%s/api/v1/video-channels/%s/videos?count=%d&start=%d"
client.base_url channel count start in
-
let response = Requests.get client.requests_session url in
-
let status_code = Requests.Response.status_code response in
-
if status_code = 200 then
-
let s = Requests.Response.body response |> Eio.Flow.read_all in
-
let json = J.from_string s in
-
parse_video_response json
-
else
-
failwith (Fmt.str "HTTP error: %d" status_code)
(** Fetch all videos from a PeerTube instance channel using pagination
@param page_size Number of videos to fetch per page
···
@param uuid UUID of the video to fetch
@return The complete video details *)
let fetch_video_details client uuid =
-
let url = Printf.sprintf "%s/api/v1/videos/%s" client.base_url uuid in
-
let response = Requests.get client.requests_session url in
-
let status_code = Requests.Response.status_code response in
-
if status_code = 200 then
-
let s = Requests.Response.body response |> Eio.Flow.read_all in
-
let json = J.from_string s in
-
(* Parse the single video details *)
-
parse_video json
-
else
-
failwith (Fmt.str "HTTP error: %d" status_code)
(** Convert a PeerTube video to Bushel.Video.t compatible structure *)
let to_bushel_video video =
···
Error (`Msg (Printf.sprintf "No thumbnail available for video %s" video.uuid))
| Some url ->
try
-
let response = Requests.get client.requests_session url in
-
let status_code = Requests.Response.status_code response in
-
if status_code = 200 then
-
let body_str = Requests.Response.body response |> Eio.Flow.read_all in
-
try
-
let output_eio_path = Eio.Path.(fs / output_path) in
-
Eio.Path.save ~create:(`Or_truncate 0o644) output_eio_path body_str;
-
Ok ()
-
with exn ->
-
Error (`Msg (Printf.sprintf "Failed to write thumbnail: %s"
-
(Printexc.to_string exn)))
-
else
-
Error (`Msg (Printf.sprintf "HTTP error downloading thumbnail: %d" status_code))
with exn ->
Error (`Msg (Printf.sprintf "Failed to download thumbnail: %s"
(Printexc.to_string exn)))
···
@param channel Channel name to fetch videos from
@return The video response *)
let fetch_channel_videos client ?(count=20) ?(start=0) channel =
+
let open Requests_json_api in
let url = Printf.sprintf "%s/api/v1/video-channels/%s/videos?count=%d&start=%d"
client.base_url channel count start in
+
get_json_exn client.requests_session url parse_video_response
(** Fetch all videos from a PeerTube instance channel using pagination
@param page_size Number of videos to fetch per page
···
@param uuid UUID of the video to fetch
@return The complete video details *)
let fetch_video_details client uuid =
+
let open Requests_json_api in
+
let url = client.base_url / "api/v1/videos" / uuid in
+
get_json_exn client.requests_session url parse_video
(** Convert a PeerTube video to Bushel.Video.t compatible structure *)
let to_bushel_video video =
···
Error (`Msg (Printf.sprintf "No thumbnail available for video %s" video.uuid))
| Some url ->
try
+
let open Requests_json_api in
+
match get_result client.requests_session url with
+
| Error (status_code, _body) ->
+
Error (`Msg (Printf.sprintf "HTTP error downloading thumbnail: %d" status_code))
+
| Ok body_str ->
+
try
+
let output_eio_path = Eio.Path.(fs / output_path) in
+
Eio.Path.save ~create:(`Or_truncate 0o644) output_eio_path body_str;
+
Ok ()
+
with exn ->
+
Error (`Msg (Printf.sprintf "Failed to write thumbnail: %s"
+
(Printexc.to_string exn)))
with exn ->
Error (`Msg (Printf.sprintf "Failed to download thumbnail: %s"
(Printexc.to_string exn)))
+1
stack/requests/lib/dune
···
uri
jsonm
yojson
base64
cacheio
cookeio
···
uri
jsonm
yojson
+
ezjsonm
base64
cacheio
cookeio
+20
stack/requests_json_api/dune-project
···
···
+
(lang dune 3.0)
+
(name requests_json_api)
+
+
(generate_opam_files true)
+
+
(source (github yourusername/requests_json_api))
+
(license MIT)
+
(authors "Your Name")
+
(maintainers "Your Name")
+
+
(package
+
(name requests_json_api)
+
(synopsis "JSON API helpers for the Requests HTTP client library")
+
(description "Convenient combinators for working with JSON APIs using the Requests library")
+
(depends
+
(ocaml (>= 5.0))
+
dune
+
requests
+
eio
+
ezjsonm))
+4
stack/requests_json_api/lib/dune
···
···
+
(library
+
(public_name requests_json_api)
+
(name requests_json_api)
+
(libraries requests eio ezjsonm))
+121
stack/requests_json_api/lib/requests_json_api.ml
···
···
+
(** JSON API Helpers for Requests *)
+
+
(** {1 Response Helpers} *)
+
+
let read_body response = Requests.Response.body response |> Eio.Flow.read_all
+
+
let status response = Requests.Response.status_code response
+
+
let is_ok response = status response = 200
+
+
let is_2xx response =
+
let s = status response in
+
s >= 200 && s < 300
+
+
let check_2xx response =
+
let status = status response in
+
let body = read_body response in
+
if status >= 200 && status < 300 then Ok body
+
else Error (status, body)
+
+
let check_ok response =
+
let status = status response in
+
let body = read_body response in
+
if status = 200 then Ok body
+
else Error (status, body)
+
+
(** {1 Request Helpers with Status Checking} *)
+
+
let get_ok session url =
+
let response = Requests.get session url in
+
if not (is_ok response) then
+
failwith (Printf.sprintf "HTTP %d" (status response));
+
read_body response
+
+
let get_2xx session url =
+
let response = Requests.get session url in
+
if not (is_2xx response) then
+
failwith (Printf.sprintf "HTTP %d" (status response));
+
read_body response
+
+
let get_result session url =
+
try
+
let response = Requests.get session url in
+
check_2xx response
+
with exn ->
+
Error (0, Printexc.to_string exn)
+
+
(** {1 JSON Helpers} *)
+
+
let parse_json parser body_str =
+
Ezjsonm.from_string body_str |> parser
+
+
let parse_json_result parser body_str =
+
try Ok (parse_json parser body_str)
+
with exn -> Error (Printexc.to_string exn)
+
+
let get_json_exn session url parser =
+
get_2xx session url |> parse_json parser
+
+
let get_json session url parser =
+
match get_result session url with
+
| Ok body ->
+
(match parse_json_result parser body with
+
| Ok result -> Ok result
+
| Error msg -> Error (`Json_error msg))
+
| Error (status, body) -> Error (`Http (status, body))
+
+
let post_json session url json_value =
+
let body_str = Ezjsonm.value_to_string json_value in
+
let body = Requests.Body.of_string Requests.Mime.json body_str in
+
Requests.post session url ~body
+
+
let post_json_exn session url json_value =
+
let response = post_json session url json_value in
+
if not (is_2xx response) then
+
failwith (Printf.sprintf "HTTP %d" (status response));
+
read_body response
+
+
let post_json_result session url json_value =
+
try
+
let response = post_json session url json_value in
+
check_2xx response
+
with exn ->
+
Error (0, Printexc.to_string exn)
+
+
(** {1 URL Helpers} *)
+
+
let ( / ) base path =
+
(* Handle trailing slash in base and leading slash in path *)
+
let base = if String.ends_with ~suffix:"/" base then String.sub base 0 (String.length base - 1) else base in
+
let path = if String.starts_with ~prefix:"/" path then String.sub path 1 (String.length path - 1) else path in
+
base ^ "/" ^ path
+
+
let make_url = ( / )
+
+
(** {1 Let Syntax for Composition} *)
+
+
module Syntax = struct
+
let ( let* ) x f = Result.bind x f
+
let ( let+ ) x f = Result.map f x
+
let ( and* ) a b = match a, b with
+
| Ok a, Ok b -> Ok (a, b)
+
| Error e, _ | _, Error e -> Error e
+
let ( and+ ) = ( and* )
+
end
+
+
(** {1 Convenience Combinators} *)
+
+
let or_fail = function
+
| Ok x -> x
+
| Error (`Http (code, msg)) ->
+
failwith (Printf.sprintf "HTTP %d: %s" code msg)
+
| Error (`Json_error msg) ->
+
failwith (Printf.sprintf "JSON error: %s" msg)
+
+
let or_fail_with prefix = function
+
| Ok x -> x
+
| Error (`Http (code, msg)) ->
+
failwith (Printf.sprintf "%s: HTTP %d: %s" prefix code msg)
+
| Error (`Json_error msg) ->
+
failwith (Printf.sprintf "%s: JSON error: %s" prefix msg)
+115
stack/requests_json_api/lib/requests_json_api.mli
···
···
+
(** JSON API Helpers for Requests
+
+
This module provides convenient combinators for working with JSON APIs,
+
reducing boilerplate code for common patterns like:
+
- Making requests and checking status codes
+
- Reading response bodies as strings
+
- Parsing JSON responses
+
- Building URLs from base + path
+
- Creating JSON request bodies
+
+
{2 Example Usage}
+
+
{[
+
open Requests_json_api
+
+
let fetch_users session =
+
get_json_exn session (base_url / "users") parse_users
+
]}
+
*)
+
+
(** {1 Response Helpers} *)
+
+
val read_body : Requests.Response.t -> string
+
(** Read response body as a string. Equivalent to:
+
[Requests.Response.body response |> Eio.Flow.read_all] *)
+
+
val status : Requests.Response.t -> int
+
(** Get status code. Alias for [Requests.Response.status_code] *)
+
+
val is_ok : Requests.Response.t -> bool
+
(** Check if status is exactly 200 *)
+
+
val is_2xx : Requests.Response.t -> bool
+
(** Check if status is in 200-299 range *)
+
+
val check_2xx : Requests.Response.t -> (string, int * string) result
+
(** Check if response is 2xx and return body, or error with (status, body) *)
+
+
val check_ok : Requests.Response.t -> (string, int * string) result
+
(** Check if response is 200 and return body, or error with (status, body) *)
+
+
(** {1 Request Helpers with Status Checking} *)
+
+
val get_ok : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> string
+
(** [get_ok session url] makes a GET request and returns the body as string.
+
Raises [Failure] if status is not 200. *)
+
+
val get_2xx : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> string
+
(** Like [get_ok] but accepts any 2xx status code.
+
Raises [Failure] on non-2xx status. *)
+
+
val get_result : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> (string, int * string) result
+
(** [get_result session url] makes a GET request and returns Result.
+
[Ok body] on 2xx, [Error (status, body)] otherwise. *)
+
+
(** {1 JSON Helpers} *)
+
+
val parse_json : (Ezjsonm.value -> 'a) -> string -> 'a
+
(** [parse_json parser body_str] parses JSON string and applies parser.
+
Raises exception on parse error. *)
+
+
val parse_json_result : (Ezjsonm.value -> 'a) -> string -> ('a, string) result
+
(** Like [parse_json] but returns Result on parse error. *)
+
+
val get_json_exn : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> (Ezjsonm.value -> 'a) -> 'a
+
(** [get_json_exn session url parser] does GET request, checks status is 2xx,
+
reads body, parses JSON, and applies parser. Raises on any error. *)
+
+
val get_json : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> (Ezjsonm.value -> 'a) ->
+
('a, [> `Http of int * string | `Json_error of string]) result
+
(** Like [get_json_exn] but returns Result instead of raising. *)
+
+
val post_json : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> Ezjsonm.value -> Requests.Response.t
+
(** [post_json session url json_value] creates JSON body and POSTs it. *)
+
+
val post_json_exn : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> Ezjsonm.value -> string
+
(** Like [post_json] but checks status is 2xx and returns body. Raises on error. *)
+
+
val post_json_result : (_ Eio.Time.clock, _ Eio.Net.t) Requests.t -> string -> Ezjsonm.value ->
+
(string, int * string) result
+
(** Like [post_json_exn] but returns Result. *)
+
+
(** {1 URL Helpers} *)
+
+
val ( / ) : string -> string -> string
+
(** [base_url / path] joins base URL and path, handling trailing/leading slashes.
+
Example: ["https://api.com" / "users" / "123"] *)
+
+
val make_url : string -> string -> string
+
(** [make_url base_url path] is the function form of [/]. *)
+
+
(** {1 Let Syntax for Composition} *)
+
+
module Syntax : sig
+
val ( let* ) : ('a, 'e) result -> ('a -> ('b, 'e) result) -> ('b, 'e) result
+
(** Result bind operator for chaining operations *)
+
+
val ( let+ ) : ('a, 'e) result -> ('a -> 'b) -> ('b, 'e) result
+
(** Result map operator *)
+
+
val ( and* ) : ('a, 'e) result -> ('b, 'e) result -> ('a * 'b, 'e) result
+
(** Result product for parallel operations *)
+
+
val ( and+ ) : ('a, 'e) result -> ('b, 'e) result -> ('a * 'b, 'e) result
+
(** Result product for parallel operations (alias) *)
+
end
+
+
(** {1 Convenience Combinators} *)
+
+
val or_fail : ('a, [< `Http of int * string | `Json_error of string]) result -> 'a
+
(** [or_fail result] unwraps a result or raises [Failure] with error message.
+
Useful for converting Result-based APIs to exception-based. *)
+
+
val or_fail_with : string -> ('a, [< `Http of int * string | `Json_error of string]) result -> 'a
+
(** Like [or_fail] but with custom error prefix. *)
+33
stack/requests_json_api/requests_json_api.opam
···
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "JSON API helpers for the Requests HTTP client library"
+
description:
+
"Convenient combinators for working with JSON APIs using the Requests library"
+
maintainer: ["Your Name"]
+
authors: ["Your Name"]
+
license: "MIT"
+
homepage: "https://github.com/yourusername/requests_json_api"
+
bug-reports: "https://github.com/yourusername/requests_json_api/issues"
+
depends: [
+
"ocaml" {>= "5.0"}
+
"dune" {>= "3.0"}
+
"requests"
+
"eio"
+
"ezjsonm"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
dev-repo: "git+https://github.com/yourusername/requests_json_api.git"
+1 -1
stack/typesense-cliente/dune
···
(library
(public_name typesense-cliente)
(name typesense_cliente)
-
(libraries eio requests ezjsonm fmt uri ptime))
···
(library
(public_name typesense-cliente)
(name typesense_cliente)
+
(libraries eio requests requests_json_api ezjsonm fmt uri ptime))
+20 -30
stack/typesense-cliente/typesense_cliente.ml
···
(** Make HTTP request to Typesense API *)
let make_request client ?(meth=`GET) ?(body="") path =
let uri = Uri.of_string (client.config.endpoint ^ path) in
let body = if body = "" then None else Some (Requests.Body.of_string Requests.Mime.json body) in
···
(Uri.to_string uri)
in
-
let status = Requests.Response.status_code response in
-
let body_flow = Requests.Response.body response in
-
let body_str = Eio.Flow.read_all body_flow in
-
-
if status >= 200 && status < 300 then
-
Ok body_str
-
else
-
Error (Http_error (status, body_str))
with exn ->
Error (Connection_error (Printexc.to_string exn))
···
match make_request client path with
| Ok response_str ->
-
(try
-
let json = Ezjsonm.from_string response_str in
-
let search_response = parse_search_response collection_name json in
-
Ok search_response
-
with exn ->
-
Error (Json_error (Printexc.to_string exn)))
| Error err -> Error err
(** Helper function to drop n elements from list *)
···
match make_request client ~meth:`POST ~body "/multi_search" with
| Ok response_str ->
-
(try
-
let json = Ezjsonm.from_string response_str in
-
let multisearch_resp = parse_multisearch_response json in
-
Ok multisearch_resp
-
with exn ->
-
Error (Json_error (Printexc.to_string exn)))
| Error err -> Error err
(** Combine multisearch results into single result set *)
···
(** List all collections *)
let list_collections client =
match make_request client "/collections" with
| Ok response_str ->
-
(try
-
let json = Ezjsonm.from_string response_str in
-
let collections = Ezjsonm.get_list (fun c ->
-
let name = Ezjsonm.get_dict c |> List.assoc "name" |> Ezjsonm.get_string in
-
let num_docs = Ezjsonm.get_dict c |> List.assoc "num_documents" |> Ezjsonm.get_int in
-
(name, num_docs)
-
) json in
-
Ok collections
-
with exn ->
-
Error (Json_error (Printexc.to_string exn)))
| Error err -> Error err
(** Pretty printer utilities *)
···
(** Make HTTP request to Typesense API *)
let make_request client ?(meth=`GET) ?(body="") path =
+
let open Requests_json_api in
let uri = Uri.of_string (client.config.endpoint ^ path) in
let body = if body = "" then None else Some (Requests.Body.of_string Requests.Mime.json body) in
···
(Uri.to_string uri)
in
+
match check_2xx response with
+
| Ok body_str -> Ok body_str
+
| Error (status, body_str) -> Error (Http_error (status, body_str))
with exn ->
Error (Connection_error (Printexc.to_string exn))
···
match make_request client path with
| Ok response_str ->
+
(match Requests_json_api.parse_json_result (parse_search_response collection_name) response_str with
+
| Ok search_response -> Ok search_response
+
| Error msg -> Error (Json_error msg))
| Error err -> Error err
(** Helper function to drop n elements from list *)
···
match make_request client ~meth:`POST ~body "/multi_search" with
| Ok response_str ->
+
(match Requests_json_api.parse_json_result parse_multisearch_response response_str with
+
| Ok multisearch_resp -> Ok multisearch_resp
+
| Error msg -> Error (Json_error msg))
| Error err -> Error err
(** Combine multisearch results into single result set *)
···
(** List all collections *)
let list_collections client =
+
let parse_collections json =
+
Ezjsonm.get_list (fun c ->
+
let name = Ezjsonm.get_dict c |> List.assoc "name" |> Ezjsonm.get_string in
+
let num_docs = Ezjsonm.get_dict c |> List.assoc "num_documents" |> Ezjsonm.get_int in
+
(name, num_docs)
+
) json
+
in
match make_request client "/collections" with
| Ok response_str ->
+
(match Requests_json_api.parse_json_result parse_collections response_str with
+
| Ok collections -> Ok collections
+
| Error msg -> Error (Json_error msg))
| Error err -> Error err
(** Pretty printer utilities *)