@@ -19,11 +19,11 @@ defmodule FuzzyCatalog.Catalog.Providers.HardcoverProvider do
1919
2020 case validate_isbn ( clean_isbn ) do
2121 :valid ->
22- query = build_search_query ( clean_isbn , [ "isbns" ] )
22+ query = build_isbn_query ( clean_isbn )
2323
2424 case make_graphql_request ( query ) do
2525 { :ok , response } ->
26- parse_search_response ( response , :single )
26+ parse_editions_response ( response )
2727
2828 { :error , reason } ->
2929 { :error , reason }
@@ -76,6 +76,43 @@ defmodule FuzzyCatalog.Catalog.Providers.HardcoverProvider do
7676 end
7777 end
7878
79+ defp build_isbn_query ( isbn ) do
80+ """
81+ {
82+ editions(where: {
83+ _or: [
84+ {isbn_10: {_eq: "#{ escape_graphql_string ( isbn ) } "}},
85+ {isbn_13: {_eq: "#{ escape_graphql_string ( isbn ) } "}}
86+ ]
87+ }, limit: 1) {
88+ id
89+ title
90+ subtitle
91+ edition_format
92+ isbn_10
93+ isbn_13
94+ pages
95+ release_date
96+ asin
97+ audio_seconds
98+ book {
99+ id
100+ title
101+ description
102+ cached_contributors
103+ book_series {
104+ series {
105+ name
106+ }
107+ }
108+ cached_tags
109+ cached_image
110+ }
111+ }
112+ }
113+ """
114+ end
115+
79116 defp build_search_query ( query_string , fields ) do
80117 # Format fields as comma-separated string: "field1,field2"
81118 fields_str = Enum . join ( fields , "," )
@@ -194,6 +231,210 @@ defmodule FuzzyCatalog.Catalog.Providers.HardcoverProvider do
194231 { :error , "Invalid response format" }
195232 end
196233
234+ defp parse_editions_response ( % { "data" => % { "editions" => editions } } ) when is_list ( editions ) do
235+ case editions do
236+ [ ] ->
237+ { :error , "No results found" }
238+
239+ [ edition | _ ] ->
240+ { :ok , normalize_edition_data ( edition ) }
241+ end
242+ end
243+
244+ defp parse_editions_response ( response ) do
245+ Logger . error ( "Hardcover: Unexpected editions response structure: #{ inspect ( response ) } " )
246+ { :error , "Invalid response format" }
247+ end
248+
249+ defp normalize_edition_data ( edition ) when is_map ( edition ) do
250+ book_data = edition [ "book" ] || % { }
251+
252+ # Extract edition format and map to media types
253+ edition_format = edition [ "edition_format" ]
254+ media_types = map_edition_format_to_media_types ( edition_format )
255+
256+ # Extract book-level data from cached fields
257+ contributors = extract_from_cached ( book_data [ "cached_contributors" ] )
258+ description = book_data [ "description" ]
259+ book_series = book_data [ "book_series" ] || [ ]
260+ tags = extract_from_cached ( book_data [ "cached_tags" ] )
261+ image = extract_from_cached ( book_data [ "cached_image" ] )
262+
263+ # Parse publication date
264+ publication_date = parse_date ( edition [ "release_date" ] )
265+
266+ % {
267+ title: edition [ "title" ] || book_data [ "title" ] || "Unknown Title" ,
268+ subtitle: edition [ "subtitle" ] ,
269+ author: format_contributors ( contributors ) ,
270+ isbn10: edition [ "isbn_10" ] ,
271+ isbn13: edition [ "isbn_13" ] ,
272+ publisher: nil ,
273+ publication_date: publication_date ,
274+ pages: edition [ "pages" ] ,
275+ genre: format_tags_as_genres ( tags ) ,
276+ description: description ,
277+ series: format_book_series ( book_series ) ,
278+ series_number: nil ,
279+ cover_url: extract_cover_url ( image ) ,
280+ suggested_media_types: media_types
281+ }
282+ end
283+
284+ # Expose for testing
285+ if Mix . env ( ) == :test do
286+ def __map_edition_format_to_media_types__ ( format ) ,
287+ do: map_edition_format_to_media_types ( format )
288+
289+ def __extract_from_cached__ ( data ) , do: extract_from_cached ( data )
290+ def __format_contributors__ ( contributors ) , do: format_contributors ( contributors )
291+ def __format_genres__ ( genres ) , do: format_tags_as_genres ( genres )
292+ def __format_series__ ( series ) , do: format_book_series ( series )
293+ end
294+
295+ defp map_edition_format_to_media_types ( nil ) , do: [ ]
296+ defp map_edition_format_to_media_types ( "" ) , do: [ ]
297+
298+ defp map_edition_format_to_media_types ( format ) when is_binary ( format ) do
299+ # Normalize format string (lowercase, trim)
300+ normalized = format |> String . downcase ( ) |> String . trim ( )
301+
302+ case normalized do
303+ # Direct matches
304+ "hardcover" ->
305+ [ "hardcover" ]
306+
307+ "paperback" ->
308+ [ "paperback" ]
309+
310+ "audiobook" ->
311+ [ "audiobook" ]
312+
313+ "ebook" ->
314+ [ "ebook" ]
315+
316+ "audio" ->
317+ [ "audiobook" ]
318+
319+ "digital" ->
320+ [ "ebook" ]
321+
322+ # Common variations
323+ "mass market paperback" ->
324+ [ "paperback" ]
325+
326+ "trade paperback" ->
327+ [ "paperback" ]
328+
329+ "board book" ->
330+ [ "hardcover" ]
331+
332+ "library binding" ->
333+ [ "hardcover" ]
334+
335+ # Kindle/digital formats
336+ format when format in [ "kindle" , "kindle edition" ] ->
337+ [ "ebook" ]
338+
339+ # Audio formats
340+ format when format in [ "audio cd" , "audio download" , "mp3 cd" ] ->
341+ [ "audiobook" ]
342+
343+ # Unknown
344+ _ ->
345+ Logger . debug ( "Unknown Hardcover edition_format: #{ format } " )
346+ [ ]
347+ end
348+ end
349+
350+ defp extract_from_cached ( nil ) , do: nil
351+
352+ defp extract_from_cached ( json ) when is_binary ( json ) do
353+ case Jason . decode ( json ) do
354+ { :ok , data } -> data
355+ { :error , _ } -> nil
356+ end
357+ end
358+
359+ defp extract_from_cached ( data ) when is_map ( data ) or is_list ( data ) , do: data
360+
361+ defp format_contributors ( nil ) , do: "Unknown Author"
362+ defp format_contributors ( [ ] ) , do: "Unknown Author"
363+
364+ defp format_contributors ( contributors ) when is_list ( contributors ) do
365+ contributors
366+ |> Enum . map ( fn
367+ % { "author" => % { "name" => name } } -> name
368+ % { "name" => name } -> name
369+ name when is_binary ( name ) -> name
370+ _ -> nil
371+ end )
372+ |> Enum . reject ( & is_nil / 1 )
373+ |> case do
374+ [ ] -> "Unknown Author"
375+ names -> Enum . join ( names , ", " )
376+ end
377+ end
378+
379+ defp format_book_series ( nil ) , do: nil
380+ defp format_book_series ( [ ] ) , do: nil
381+
382+ defp format_book_series ( book_series ) when is_list ( book_series ) do
383+ # book_series is a list of %{"series" => %{"name" => "..."}}
384+ book_series
385+ |> Enum . map ( fn
386+ % { "series" => % { "name" => name } } -> name
387+ _ -> nil
388+ end )
389+ |> Enum . reject ( & is_nil / 1 )
390+ |> List . first ( )
391+ end
392+
393+ defp format_tags_as_genres ( nil ) , do: nil
394+ defp format_tags_as_genres ( [ ] ) , do: nil
395+
396+ defp format_tags_as_genres ( tags ) when is_map ( tags ) do
397+ # cached_tags is a map with genre/tag categories as keys
398+ # Try to extract genre-like tags from the structure
399+ tags
400+ |> Map . values ( )
401+ |> List . flatten ( )
402+ |> Enum . take ( 3 )
403+ |> Enum . map ( fn
404+ % { "tag" => tag } -> tag
405+ % { "name" => name } -> name
406+ tag when is_binary ( tag ) -> tag
407+ _ -> nil
408+ end )
409+ |> Enum . reject ( & is_nil / 1 )
410+ |> case do
411+ [ ] -> nil
412+ names -> Enum . join ( names , ", " )
413+ end
414+ end
415+
416+ defp format_tags_as_genres ( tags ) when is_list ( tags ) do
417+ tags
418+ |> Enum . take ( 3 )
419+ |> Enum . map ( fn
420+ % { "tag" => tag } -> tag
421+ % { "name" => name } -> name
422+ tag when is_binary ( tag ) -> tag
423+ _ -> nil
424+ end )
425+ |> Enum . reject ( & is_nil / 1 )
426+ |> case do
427+ [ ] -> nil
428+ names -> Enum . join ( names , ", " )
429+ end
430+ end
431+
432+ defp parse_date ( nil ) , do: nil
433+
434+ defp parse_date ( date_str ) when is_binary ( date_str ) do
435+ FuzzyCatalog.DateUtils . parse_date ( date_str )
436+ end
437+
197438 defp normalize_hardcover_data ( book_data ) when is_map ( book_data ) do
198439 # Extract ISBNs
199440 isbns = book_data [ "isbns" ] || [ ]
@@ -218,6 +459,11 @@ defmodule FuzzyCatalog.Catalog.Providers.HardcoverProvider do
218459 # Extract cover image URL (if available)
219460 cover_url = extract_cover_url ( book_data [ "image" ] ) || book_data [ "cover_url" ]
220461
462+ # Search results don't include edition_format in the document
463+ # They aggregate all editions, so format info isn't available
464+ # Users can get format by doing an ISBN lookup instead
465+ media_types = [ ]
466+
221467 % {
222468 title: book_data [ "title" ] || "Unknown Title" ,
223469 subtitle: book_data [ "subtitle" ] ,
@@ -232,7 +478,7 @@ defmodule FuzzyCatalog.Catalog.Providers.HardcoverProvider do
232478 series: series ,
233479 series_number: nil ,
234480 cover_url: cover_url ,
235- suggested_media_types: [ ]
481+ suggested_media_types: media_types
236482 }
237483 end
238484
@@ -258,6 +504,7 @@ defmodule FuzzyCatalog.Catalog.Providers.HardcoverProvider do
258504 end
259505
260506 defp parse_release_year ( nil ) , do: nil
507+
261508 defp parse_release_year ( year ) when is_integer ( year ) do
262509 FuzzyCatalog.DateUtils . parse_date ( year )
263510 end
0 commit comments