Immich bindings and CLI in OCaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6open Cmdliner
7
8(* Styled output helpers *)
9let header_style = Fmt.(styled `Bold string)
10let id_style = Fmt.(styled `Faint string)
11let name_style = Fmt.(styled (`Fg `Cyan) string)
12let hidden_style = Fmt.(styled (`Fg `Yellow) string)
13let count_style = Fmt.(styled (`Fg `Green) int)
14let success_style = Fmt.(styled (`Fg `Green) string)
15
16(* Search for people by name *)
17
18let search_action ~requests_config ~profile ~name ~with_hidden env =
19 Immich_auth.Error.wrap (fun () ->
20 Immich_auth.Cmd.with_client ~requests_config ?profile (fun _fs client ->
21 let api = Immich_auth.Client.client client in
22 let session = Immich.session api in
23 let base_url = Immich.base_url api in
24 (* Person.search_person returns ResponseDto (single) but should be a list *)
25 (* Use low-level API to get proper list response *)
26 let query = Printf.sprintf "?name=%s%s"
27 (Uri.pct_encode name)
28 (if with_hidden then "&withHidden=true" else "") in
29 let url = base_url ^ "/people/search" ^ query in
30 let response = Requests.get session url in
31 if Requests.Response.ok response then begin
32 let json = Requests.Response.json response in
33 let people = Openapi.Runtime.Json.decode_json_exn
34 (Jsont.list Immich.Person.ResponseDto.jsont) json in
35 if people = [] then
36 Fmt.pr "%a '%a'@."
37 Fmt.(styled `Faint string) "No people found matching"
38 name_style name
39 else begin
40 Fmt.pr "%a '%a':@."
41 header_style "People matching"
42 name_style name;
43 List.iter (fun person ->
44 let pname = Immich.Person.ResponseDto.name person in
45 let id = Immich.Person.ResponseDto.id person in
46 let is_hidden = Immich.Person.ResponseDto.is_hidden person in
47 let display_name = if pname = "" then "<unnamed>" else pname in
48 Fmt.pr " %a %a%a@."
49 id_style id
50 name_style display_name
51 hidden_style (if is_hidden then " (hidden)" else "")
52 ) people
53 end
54 end else begin
55 (* Raise API error for proper handling *)
56 raise (Openapi.Runtime.Api_error {
57 operation = "search_people";
58 method_ = "GET";
59 url;
60 status = Requests.Response.status_code response;
61 body = Requests.Response.text response;
62 parsed_body = None;
63 })
64 end
65 ) env
66 )
67
68let name_arg =
69 let doc = "Name to search for." in
70 Arg.(required & pos 0 (some string) None & info [] ~docv:"NAME" ~doc)
71
72let with_hidden_arg =
73 let doc = "Include hidden people in results." in
74 Arg.(value & flag & info ["with-hidden"] ~doc)
75
76let search_cmd env fs =
77 let doc = "Search for people by name." in
78 let info = Cmd.info "search" ~doc in
79 let search' (style_renderer, level) requests_config profile name with_hidden =
80 Immich_auth.Cmd.setup_logging_with_config style_renderer level requests_config;
81 search_action ~requests_config ~profile ~name ~with_hidden env
82 in
83 Cmd.v info Term.(const search' $ Immich_auth.Cmd.setup_logging $ Immich_auth.Cmd.requests_config_term fs $ Immich_auth.Cmd.profile_arg $ name_arg $ with_hidden_arg)
84
85(* Get person thumbnail *)
86
87let output_arg =
88 let doc = "Output file path. Use '-' for stdout." in
89 Arg.(value & opt string "-" & info ["output"; "o"] ~docv:"FILE" ~doc)
90
91let person_id_arg =
92 let doc = "Person ID." in
93 Arg.(required & pos 0 (some string) None & info [] ~docv:"PERSON_ID" ~doc)
94
95let thumbnail_action ~requests_config ~profile ~person_id ~output env =
96 Immich_auth.Error.wrap (fun () ->
97 Immich_auth.Cmd.with_client ~requests_config ?profile (fun _fs client ->
98 let api = Immich_auth.Client.client client in
99 let session = Immich.session api in
100 let base_url = Immich.base_url api in
101 let url = Printf.sprintf "%s/people/%s/thumbnail" base_url person_id in
102 let response = Requests.get session url in
103 if Requests.Response.ok response then begin
104 let data = Requests.Response.text response in
105 if output = "-" then
106 print_string data
107 else begin
108 let oc = open_out_bin output in
109 output_string oc data;
110 close_out oc;
111 Fmt.pr "%a %a@."
112 success_style "Thumbnail saved to"
113 Fmt.(styled `Bold string) output
114 end
115 end else begin
116 raise (Openapi.Runtime.Api_error {
117 operation = "get_person_thumbnail";
118 method_ = "GET";
119 url;
120 status = Requests.Response.status_code response;
121 body = Requests.Response.text response;
122 parsed_body = None;
123 })
124 end
125 ) env
126 )
127
128let thumbnail_cmd env fs =
129 let doc = "Download a person's thumbnail image." in
130 let info = Cmd.info "thumbnail" ~doc in
131 let thumbnail' (style_renderer, level) requests_config profile person_id output =
132 Immich_auth.Cmd.setup_logging_with_config style_renderer level requests_config;
133 thumbnail_action ~requests_config ~profile ~person_id ~output env
134 in
135 Cmd.v info Term.(const thumbnail' $ Immich_auth.Cmd.setup_logging $ Immich_auth.Cmd.requests_config_term fs $ Immich_auth.Cmd.profile_arg $ person_id_arg $ output_arg)
136
137(* List all people *)
138
139let list_action ~requests_config ~profile ~with_hidden env =
140 Immich_auth.Error.wrap (fun () ->
141 Immich_auth.Cmd.with_client ~requests_config ?profile (fun _fs client ->
142 let api = Immich_auth.Client.client client in
143 let with_hidden_param = if with_hidden then Some "true" else None in
144 let resp = Immich.People.get_all_people api ?with_hidden:with_hidden_param () in
145 let people = Immich.People.ResponseDto.people resp in
146 let total = Immich.People.ResponseDto.total resp in
147 let hidden_count = Immich.People.ResponseDto.hidden resp in
148 Fmt.pr "%a %a total, %a hidden@."
149 header_style "People:"
150 count_style total
151 count_style hidden_count;
152 if people = [] then
153 Fmt.pr "%a@." Fmt.(styled `Faint string) "No people found."
154 else begin
155 List.iter (fun person ->
156 let name = Immich.Person.ResponseDto.name person in
157 let id = Immich.Person.ResponseDto.id person in
158 let is_hidden = Immich.Person.ResponseDto.is_hidden person in
159 let display_name = if name = "" then "<unnamed>" else name in
160 Fmt.pr " %a %a%a@."
161 id_style id
162 name_style display_name
163 hidden_style (if is_hidden then " (hidden)" else "")
164 ) people
165 end
166 ) env
167 )
168
169let list_cmd env fs =
170 let doc = "List all people." in
171 let info = Cmd.info "list" ~doc in
172 let list' (style_renderer, level) requests_config profile with_hidden =
173 Immich_auth.Cmd.setup_logging_with_config style_renderer level requests_config;
174 list_action ~requests_config ~profile ~with_hidden env
175 in
176 Cmd.v info Term.(const list' $ Immich_auth.Cmd.setup_logging $ Immich_auth.Cmd.requests_config_term fs $ Immich_auth.Cmd.profile_arg $ with_hidden_arg)
177
178(* Faces command group *)
179
180let faces_cmd env fs =
181 let doc = "Face and people commands." in
182 let info = Cmd.info "faces" ~doc in
183 Cmd.group info
184 [ list_cmd env fs
185 ; search_cmd env fs
186 ; thumbnail_cmd env fs
187 ]