···
5
+
let src = Logs.Src.create "karakeepe" ~doc:"Karakeepe API client"
6
+
module Log = (val Logs.src_log src : Logs.LOG)
(** Type representing a Karakeep bookmark *)
···
try J.find json ["id"] |> J.get_string
66
-
prerr_endline (Fmt.str "Error parsing bookmark ID: %s" (Printexc.to_string e));
67
-
prerr_endline (Fmt.str "JSON: %s" (J.value_to_string json));
69
+
Log.err (fun m -> m "Error parsing bookmark ID: %s@.JSON: %s"
70
+
(Printexc.to_string e) (J.value_to_string json));
failwith "Unable to parse bookmark ID"
···
| Some (`String id) -> "karakeep-asset://" ^ id
| _ -> failwith "No URL or asset ID found in bookmark"))
91
-
prerr_endline (Fmt.str "Bookmark JSON structure: %s" (J.value_to_string json));
94
+
Log.err (fun m -> m "No URL found in bookmark@.JSON structure: %s"
95
+
(J.value_to_string json));
failwith "No URL found in bookmark"
···
{ id; title; url; note; created_at; updated_at; favourited; archived; tags;
tagging_status; summary; content; assets }
155
-
(** Parse a Karakeep bookmark response *)
159
+
(** Parse a Karakeep bookmark response - handles multiple API response formats *)
let parse_bookmark_response json =
157
-
prerr_endline (Fmt.str "Full response JSON: %s" (J.value_to_string json));
161
+
Log.debug (fun m -> m "Parsing API response: %s" (J.value_to_string json));
163
+
(* Try format 1: {total: int, data: [...], nextCursor?: string} *)
164
+
let try_format1 () =
165
+
Log.debug (fun m -> m "Trying format 1: {total, data, nextCursor}");
let total = J.find json ["total"] |> J.get_int in
let bookmarks_json = J.find json ["data"] in
162
-
prerr_endline "Found bookmarks in data array";
let data = J.get_list parse_bookmark bookmarks_json in
try Some (J.find json ["nextCursor"] |> J.get_string)
173
+
Log.debug (fun m -> m "Successfully parsed format 1: %d bookmarks" (List.length data));
{ total; data; next_cursor }
170
-
prerr_endline (Fmt.str "First format parse error: %s" (Printexc.to_string e1));
172
-
let bookmarks_json = J.find json ["bookmarks"] in
173
-
prerr_endline "Found bookmarks in bookmarks array";
175
-
try J.get_list parse_bookmark bookmarks_json
177
-
prerr_endline (Fmt.str "Error parsing bookmarks array: %s" (Printexc.to_string e));
181
-
try Some (J.find json ["nextCursor"] |> J.get_string)
184
-
{ total = List.length data; data; next_cursor }
186
-
prerr_endline (Fmt.str "Second format parse error: %s" (Printexc.to_string e2));
188
-
let error = J.find json ["error"] |> J.get_string in
190
-
try J.find json ["message"] |> J.get_string
191
-
with _ -> "Unknown error"
193
-
prerr_endline (Fmt.str "API Error: %s - %s" error message);
194
-
{ total = 0; data = []; next_cursor = None }
197
-
prerr_endline "Trying alternate array format";
198
-
prerr_endline (Fmt.str "JSON structure keys: %s"
177
+
(* Try format 2: {bookmarks: [...], nextCursor?: string} - no total field *)
178
+
let try_format2 () =
179
+
Log.debug (fun m -> m "Trying format 2: {bookmarks, nextCursor}");
180
+
let bookmarks_json = J.find json ["bookmarks"] in
181
+
let data = J.get_list parse_bookmark bookmarks_json in
183
+
try Some (J.find json ["nextCursor"] |> J.get_string)
186
+
(* Calculate total from data length when total field is missing *)
187
+
let total = List.length data in
188
+
Log.debug (fun m -> m "Successfully parsed format 2: %d bookmarks" total);
189
+
{ total; data; next_cursor }
192
+
(* Try format 3: API error response {error: string, message?: string} *)
193
+
let try_error_format () =
194
+
Log.debug (fun m -> m "Checking for API error response");
195
+
let error = J.find json ["error"] |> J.get_string in
197
+
try J.find json ["message"] |> J.get_string
198
+
with _ -> "Unknown error"
200
+
Log.err (fun m -> m "API returned error: %s - %s" error message);
201
+
{ total = 0; data = []; next_cursor = None }
204
+
(* Try format 4: Plain array at root level *)
205
+
let try_array_format () =
206
+
Log.debug (fun m -> m "Trying format 4: array at root");
209
+
let data = J.get_list parse_bookmark json in
210
+
Log.debug (fun m -> m "Successfully parsed array format: %d bookmarks" (List.length data));
211
+
{ total = List.length data; data; next_cursor = None }
212
+
| _ -> raise Not_found
215
+
(* Try each format in order *)
220
+
try try_error_format ()
222
+
try try_array_format ()
224
+
Log.err (fun m -> m "Failed to parse response in any known format");
225
+
Log.debug (fun m -> m "JSON keys: %s"
200
-
| `O fields -> String.concat ", " (List.map (fun (k, _) -> k) fields)
227
+
| `O fields -> String.concat ", " (List.map fst fields)
| _ -> "not an object"));
203
-
if J.find_opt json ["nextCursor"] <> None then begin
204
-
prerr_endline "Found nextCursor, checking alternate structures";
205
-
let bookmarks_json =
206
-
try Some (J.find json ["data"])
209
-
match bookmarks_json with
210
-
| Some json_array ->
211
-
prerr_endline "Found bookmarks in data field";
213
-
let data = J.get_list parse_bookmark json_array in
215
-
try Some (J.find json ["nextCursor"] |> J.get_string)
218
-
{ total = List.length data; data; next_cursor }
220
-
prerr_endline (Fmt.str "Error parsing bookmarks from data: %s" (Printexc.to_string e));
221
-
{ total = 0; data = []; next_cursor = None }
224
-
prerr_endline "No bookmarks found in alternate structure";
225
-
{ total = 0; data = []; next_cursor = None }
231
-
try J.get_list parse_bookmark json
233
-
prerr_endline (Fmt.str "Error parsing root array: %s" (Printexc.to_string e));
236
-
{ total = List.length data; data; next_cursor = None }
238
-
prerr_endline "Not an array at root level";
239
-
{ total = 0; data = []; next_cursor = None }
242
-
prerr_endline (Fmt.str "Third format parse error: %s" (Printexc.to_string e3));
{ total = 0; data = []; next_cursor = None }
(** Fetch bookmarks from a Karakeep instance with pagination support *)
let fetch_bookmarks ~sw ~env ~api_key ?(limit=50) ?(offset=0) ?cursor ?(include_content=false) ?filter_tags base_url =
···
List.map (fun tag -> Uri.pct_encode ~component:`Query_key tag) tags
let tags_param = String.concat "," encoded_tags in
262
-
prerr_endline (Fmt.str "Adding tags filter: %s" tags_param);
251
+
Log.debug (fun m -> m "Adding tags filter: %s" tags_param);
url ^ "&tags=" ^ tags_param
···
let headers = Requests.Headers.empty
|> Requests.Headers.set "Authorization" ("Bearer " ^ api_key) in
270
-
prerr_endline (Fmt.str "Fetching bookmarks from: %s" url);
259
+
Log.debug (fun m -> m "Fetching bookmarks from: %s" url);
let response = Requests.One.get ~sw ~clock:env#clock ~net:env#net ~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
277
-
prerr_endline (Fmt.str "Received %d bytes of response data" (String.length body_str));
266
+
Log.debug (fun m -> m "Received %d bytes of response data" (String.length body_str));
let json = J.from_string body_str in
parse_bookmark_response json
283
-
prerr_endline (Fmt.str "JSON parsing error: %s" (Printexc.to_string e));
284
-
prerr_endline (Fmt.str "Response body (first 200 chars): %s"
272
+
Log.err (fun m -> m "JSON parsing error: %s" (Printexc.to_string e));
273
+
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));
288
-
prerr_endline (Fmt.str "HTTP error %d" status_code);
277
+
Log.err (fun m -> m "HTTP error %d" status_code);
failwith (Fmt.str "HTTP error: %d" status_code)
292
-
prerr_endline (Fmt.str "Network error: %s" (Printexc.to_string e));
281
+
Log.err (fun m -> m "Network error: %s" (Printexc.to_string e));
(** Fetch all bookmarks from a Karakeep instance using pagination *)
296
-
let fetch_all_bookmarks ~sw ~env ~api_key ?(page_size=50) ?max_pages ?filter_tags ?(include_content=false) base_url =
297
-
let rec fetch_pages page_num cursor acc _total_count =
300
-
| Some cursor_str -> fetch_bookmarks ~sw ~env ~api_key ~limit:page_size ~cursor:cursor_str ~include_content ?filter_tags base_url
301
-
| None -> fetch_bookmarks ~sw ~env ~api_key ~limit:page_size ~offset:(page_num * page_size) ~include_content ?filter_tags base_url
285
+
let fetch_all_bookmarks ~sw ~env ~api_key ?(page_size=50) ?max_pages ?max_bookmarks ?filter_tags ?(include_content=false) base_url =
286
+
let rec fetch_pages page_num cursor acc =
287
+
(* Check if we've reached the max_bookmarks limit *)
288
+
let reached_limit = match max_bookmarks with
289
+
| Some max when List.length acc >= max ->
290
+
Log.debug (fun m -> m "Reached max_bookmarks limit (%d)" max);
304
-
let all_bookmarks = acc @ response.data in
295
+
if reached_limit then
298
+
Log.debug (fun m -> m "Fetching page %d" page_num);
301
+
| Some cursor_str -> fetch_bookmarks ~sw ~env ~api_key ~limit:page_size ~cursor:cursor_str ~include_content ?filter_tags base_url
302
+
| None -> fetch_bookmarks ~sw ~env ~api_key ~limit:page_size ~offset:(page_num * page_size) ~include_content ?filter_tags base_url
306
-
let more_available =
307
-
match response.next_cursor with
310
-
let fetched_count = (page_num * page_size) + List.length response.data in
311
-
fetched_count < response.total
305
+
let all_bookmarks = acc @ response.data in
306
+
Log.debug (fun m -> m "Fetched %d bookmarks this page, %d total so far"
307
+
(List.length response.data) (List.length all_bookmarks));
309
+
(* Truncate to max_bookmarks if needed *)
310
+
let all_bookmarks = match max_bookmarks with
311
+
| Some max when List.length all_bookmarks > max ->
312
+
Log.debug (fun m -> m "Truncating to max_bookmarks (%d)" max);
313
+
List.filteri (fun i _ -> i < max) all_bookmarks
314
+
| _ -> all_bookmarks
317
+
(* Determine if more pages are available:
318
+
- If next_cursor is present, there are definitely more pages
319
+
- If no next_cursor and we got fewer items than page_size, we're done
320
+
- If no next_cursor and total is reliable (> current count), there may be more *)
321
+
let more_available =
322
+
match response.next_cursor with
324
+
Log.debug (fun m -> m "More pages available (next_cursor present)");
327
+
let current_count = List.length all_bookmarks in
328
+
let got_full_page = List.length response.data = page_size in
329
+
let total_indicates_more = response.total > current_count in
330
+
(* If we got a full page and total indicates more, continue *)
331
+
let has_more = got_full_page && total_indicates_more in
333
+
Log.debug (fun m -> m "More pages likely available (%d fetched < %d total)"
334
+
current_count response.total)
336
+
Log.debug (fun m -> m "No more pages (got %d items, total=%d)"
337
+
(List.length response.data) response.total);
314
-
let under_max_pages = match max_pages with
316
-
| Some max -> page_num + 1 < max
341
+
let under_max_pages = match max_pages with
343
+
| Some max -> page_num + 1 < max
346
+
let under_max_bookmarks = match max_bookmarks with
348
+
| Some max -> List.length all_bookmarks < max
319
-
if more_available && under_max_pages then
320
-
fetch_pages (page_num + 1) response.next_cursor all_bookmarks response.total
351
+
if more_available && under_max_pages && under_max_bookmarks then
352
+
fetch_pages (page_num + 1) response.next_cursor all_bookmarks
354
+
Log.debug (fun m -> m "Pagination complete: fetched %d total bookmarks" (List.length all_bookmarks));
324
-
fetch_pages 0 None [] 0
359
+
fetch_pages 0 None []
(** Fetch detailed information for a single bookmark by ID *)
let fetch_bookmark_details ~sw ~env ~api_key base_url bookmark_id =