Add book note via OpenLibrary metadata Lbrary

Here is my version with a couple of added fields and some formatting improvements.

  • solved the “Author” array problem using the author_block variable and parsing the separate authors one by one outside of the template.
  • added quotes “” to all the metadata entries to be handled as strings, more reliable to handle string data.
  • removed the ID-field , because it was already part of the URL, so no duplicate entries.
  • adds a short cleaned up description of the book to the metadata
  • adds the book cover (L-Size) to the metadata.

-- ==============================
--  Open Library → SilverBullet
-- ==============================

local cfg = config.get("bookmanager") or ""

local pagePrefix = cfg.prefix or "MediaDB/Books/"
local filenameTemplate = cfg.filenameTemplate or [==[${title} (${first_publish_year})]==]
local pageTemplate = cfg.pageTemplate or [==[---
type: "book"
title: "${title}"
year: "${first_publish_year}"
author:
${author_block}
isbn: "${isbn}"
url: "https://openlibrary.org${olid}"
tags: "mediaDB/book"
dataSource: "OpenLibraryAPI"
cover: "${cover_image_url}"
description: "${description}"
---
# ${title} - ${author_name[1]}
]==]

function searchBook(queryString)
  -- Search by title, author, or ISBN
  local url = "https://openlibrary.org/search.json?q=" .. 
              string.gsub(queryString, " ", "+")
  
  local resp = net.proxyFetch(url, {
    method = "GET",
    headers = { Accept = "application/json" }
  })

  if not resp.ok then
    error("Failed to fetch from Open Library: " .. resp.status)
  end
  
  local data = resp.body

  if #data.docs == 0 then
    editor.flashNotification("No book found for: " .. queryString)
    return nil
  end

  options = {}
  -- show prompt to select docs
  for i, doc in ipairs(data.docs) do
    options[i] = {
      name=doc.title, 
      value=i, 
      description=string.format("By %s, published %s", doc.author_name, doc.first_publish_year)
  }  
  end
  
  local result = editor.filterBox("Select:", options)

  if not result then
    editor.flashNotification("Cancelled book search")
    return nil
  end
  
  return data.docs[result.value]  -- return first match
end

command.define {
  name = "Open Library: Add Book to Knowledge Base",
  run = function()
    local queryString = editor.prompt("Search for book (title, author, or ISBN):")
    if not queryString then
      return
    end

    print("Searching for book " .. queryString)
    
    local book = searchBook(queryString)
    if not book then
      return
    end
    
    local titleTemplate = template.new(filenameTemplate)

    local safeBook = {}

    for k in {'title','first_publish_year','author_name', 'isbn'} do
      safeBook[k] = book[k] or "Unknown"
    end

    safeBook['olid'] = book['key']

    if book.cover_i then
      safeBook['cover_image_url'] = "https://covers.openlibrary.org/b/id/" .. book.cover_i .. "-L.jpg"
    else
      safeBook['cover_image_url'] = ""
    end

    -- build dynamic author block
    local author_lines = {}
    if type(book.author_name) == "table" then
      for _, name in ipairs(book.author_name) do
        table.insert(author_lines, '  - "' .. name .. '"')
      end
    end
    safeBook['author_block'] = table.concat(author_lines, "\n")

    -- determine ISBN robustly (search fields, then edition JSON)
    local function extract_first_isbn(b)
      if not b then return nil end

      if type(b.isbn) == "table" and #b.isbn > 0 then
        return b.isbn[1]
      elseif type(b.isbn) == "string" then
        return b.isbn
      end

      if type(b.isbn_13) == "table" and #b.isbn_13 > 0 then
        return b.isbn_13[1]
      end
      if type(b.isbn_10) == "table" and #b.isbn_10 > 0 then
        return b.isbn_10[1]
      end

      local edition = b.cover_edition_key or (type(b.edition_key) == "table" and b.edition_key[1]) or nil
      if edition then
        local ed_url = "https://openlibrary.org/books/" .. edition .. ".json"
        local ed_resp = net.proxyFetch(ed_url, {
          method = "GET",
          headers = { Accept = "application/json" }
        })
        if ed_resp.ok then
          local ed = ed_resp.body
          if type(ed.isbn_13) == "table" and #ed.isbn_13 > 0 then
            return ed.isbn_13[1]
          end
          if type(ed.isbn_10) == "table" and #ed.isbn_10 > 0 then
            return ed.isbn_10[1]
          end
        end
      end

      return nil
    end

    local found_isbn = extract_first_isbn(book)
    if found_isbn and found_isbn ~= "" then
      safeBook['isbn'] = found_isbn
    else
      safeBook['isbn'] = "Unknown"
    end

    -- fetch and clean description/plot from the work JSON
    local description_text = ""
    if book.key then
      local work_url = "https://openlibrary.org" .. book.key .. ".json"
      local work_resp = net.proxyFetch(work_url, {
        method = "GET",
        headers = { Accept = "application/json" }
      })
      if work_resp.ok then
        local w = work_resp.body
        if w.description then
          if type(w.description) == "table" then
            description_text = w.description.value or ""
          elseif type(w.description) == "string" then
            description_text = w.description
          end
        elseif w.first_sentence then
          if type(w.first_sentence) == "table" then
            description_text = w.first_sentence.value or ""
          elseif type(w.first_sentence) == "string" then
            description_text = w.first_sentence
          end
        end
      end
    end

    if description_text and description_text ~= "" then
      -- truncate at earliest marker indicating references/footnotes/extra sections
      local function cut_at_first_marker(text)
        local lower = string.lower(text)
        local markers = {
          "([source",     -- "([source" pattern
          "[source",      -- "[source" pattern
          "----------",   -- separator line
          "preceded by:", -- preceded by
          "see also:",    -- see also
          "followed by:", -- followed by
          "contains:"     -- contains
        }
        local first_pos = nil
        for _, m in ipairs(markers) do
          local s = string.find(lower, m, 1, true)
          if s and (not first_pos or s < first_pos) then
            first_pos = s
          end
        end
        if first_pos then
          return string.sub(text, 1, first_pos - 1)
        end
        return text
      end

      description_text = cut_at_first_marker(description_text)

      -- remove markdown links [text](url)
      description_text = string.gsub(description_text, "%b[]%b()", "")
      -- remove any remaining bracketed reference markers like [1], [2], etc.
      description_text = string.gsub(description_text, "%[%d+%]", "")
      -- remove reference-style links and footnotes like "[1]: http..."
      description_text = string.gsub(description_text, "%[%d+%]:%s*https?://%S+", "")
      -- remove explicit [source] or [Source] occurrences if anything left
      description_text = string.gsub(description_text, "%[source%]", "")
      description_text = string.gsub(description_text, "%[Source%]", "")
      -- remove raw URLs
      description_text = string.gsub(description_text, "https?://%S+", "")
      -- remove separators and excessive whitespace/newlines
      description_text = string.gsub(description_text, "[\r\n]+", " ")
      description_text = string.gsub(description_text, "%s+", " ")
      -- escape double quotes for frontmatter
      description_text = string.gsub(description_text, '"', '”')
      description_text = string.gsub(description_text, "^%s*(.-)%s*$", "%1")
      safeBook['description'] = description_text
    else
      safeBook['description'] = ""
    end

    local rawTitle = titleTemplate(safeBook)
    local safeTitle = string.gsub(rawTitle, "[/%\\:'\"%*%?%<%>%|]", "")
    safeTitle = string.gsub(safeTitle, "%s+", " ")

    local pageName = pagePrefix .. safeTitle

    if not space.pageExists(pageName) then
      local pageTemplate = template.new(pageTemplate)
      
      local content = pageTemplate(safeBook)
      
      -- Create the page
      space.writePage(pageName, content)
      editor.flashNotification("Book added: " .. safeTitle)
    else
      editor.flashNotification("Book already in collection: " .. safeTitle)
    end
    
    editor.navigate(pageName)

  end
}

All these modifications where necessary so I can use it in my MediaGallery which I’m currently working on, which gathers books, movies, tv-series, games, or basically any object or page with a title, subtitle, year, score, etc. and most importantly an image URL in it’s metadata and displays it in a beautiful Gridl Gallery.

Here is a first sneak peak of the Media Gallery displaying the Book Gallery with the added books from OpenLibrary

4 Likes