NocoDB + n8nで自宅の蔵書管理

雑記

以前Twitter(X)でツイートしたように、自分が何の本を持っているかを一覧にできる簡易蔵書管理システムを作成した。
ローコードツールで比較的楽に作れたので作成方法を共有する。

(2024/12/2追記)動作デモを追加

tl;dr

  • iPhoneのショートカットアプリでISBNバーコードをスキャンする
  • n8nのWebhookにISBNを送り付ける
  • n8nが国立国会図書館のAPIから書籍情報+サムネイル(書影)を収集
  • NocoDBに保存し、Gallery Viewで一覧化

背景

家族に同業者がいると、同じ技術書が家に2冊あるという事態に陥ってしまう(前科2犯)。
また、自分が持っていなくても誰かが持っていないかを確認する手間が発生してしまう。
そこで、一度構築してしまえば以降の手間を極力少なく蔵書登録できるシステムの構築に着手した。

NocoDBセットアップ

NocoDBはローコードインテグレーションを得意とするデータベースライクなツールである。
セルフホストできてn8nとの連携が用意されていることから採用した。

テーブルを作成し、以下のようにフィールドを用意する。
もちろん後述のn8nもいじるのであれば、好きなように設定してよい。

  • Title (Single Line Text)
  • NDL Link (URL)
  • ISBN (Single Line Text)
  • Author (Text)
  • Publisher (Text)
  • Published Year (Text)
  • Thumbnail (Attachment)

デフォルトビューはデータベースやスプレッドシートを想起させる見た目である。

これでも問題ないが、「Create View」からGallery Viewを作成し「Edit Cards」から下記のような設定をすると、元ツイートのようなカードビューを作成することができる。

n8n

n8nはローコード開発ができるワークフローオートメーションツールである。
zapierやiftttが有名だが、セルフホスト可能なOSSとして採用した。

ワークフロー作成画面で「Import from file」を選び、下記コードを保存したテキストファイルをアップロードする。

このコードをテキスト保存する
{
  "name": "BookShelf",
  "nodes": [
    {
      "parameters": {
        "dataPropertyName": "=data",
        "options": {}
      },
      "id": "b8345929-d3c9-451a-95d0-7e2e6a322294",
      "name": "XML",
      "type": "n8n-nodes-base.xml",
      "typeVersion": 1,
      "position": [
        600,
        660
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "a501bda3-85f3-413f-92e2-a2d6623883a2",
              "leftValue": "={{ $json.empty }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "04673a07-591f-44dd-bca9-8057ed16d4b5",
      "name": "If isEmpty",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1300,
        460
      ]
    },
    {
      "parameters": {
        "jsCode": "return [{json: {empty: items.length == 1 && Object.keys(items[0].json).length == 0}}];"
      },
      "id": "18919dcc-bac5-4790-aac2-0b03d2a4523d",
      "name": "isEmpty",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        460
      ]
    },
    {
      "parameters": {
        "authentication": "nocoDbApiToken",
        "operation": "getAll",
        "projectId": "XXXXXXXXXXXXXXX",
        "table": "XXXXXXXXXXXXXXX",
        "limit": {},
        "options": {
          "where": "=(ISBN,eq,{{ $('Webhook').item.json.body.isbn }})"
        }
      },
      "id": "b2e66cf6-ffc8-499b-bd07-f319ad8dca76",
      "name": "Check data existence",
      "type": "n8n-nodes-base.nocoDb",
      "typeVersion": 3,
      "position": [
        900,
        460
      ],
      "alwaysOutputData": true,
      "credentials": {
        "nocoDbApiToken": {
          "id": "XXXXXXXXXXXXXXX",
          "name": "NocoDB account"
        }
      }
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "bookshelf",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "20b79325-6e70-41d5-b885-cef406549374",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        460,
        460
      ],
      "webhookId": "79a62439-1f6b-4d00-935a-2dcf119fbb93"
    },
    {
      "parameters": {
        "url": "=https://ndlsearch.ndl.go.jp/api/opensearch",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "isbn",
              "value": "={{ $('Webhook').item.json.body.isbn }}"
            }
          ]
        },
        "options": {}
      },
      "id": "cc30f92d-84fa-41fb-9b02-d3fd4d7a1739",
      "name": "Get Book Detail",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1520,
        460
      ]
    },
    {
      "parameters": {
        "url": "=https://ndlsearch.ndl.go.jp/thumbnail/{{ $('Webhook').item.json.body.isbn }}.jpg",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "id": "a9e58008-1131-456a-8e82-7bd32ffe73d0",
      "name": "Get Thumbnail",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        820,
        660
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "3d7e146e-68a5-496a-8d48-b55d768991ec",
              "leftValue": "={{ $binary.data }}",
              "rightValue": "",
              "operator": {
                "type": "object",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {
          "ignoreCase": false
        }
      },
      "id": "bb67ab32-a4b0-42d7-aa57-04e3acc166ae",
      "name": "Thumbnail Exists",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1040,
        660
      ],
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "authentication": "nocoDbApiToken",
        "operation": "create",
        "projectId": "XXXXXXXXXXXXXXX",
        "table": "XXXXXXXXXXXXXXX",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldName": "ISBN",
              "fieldValue": "={{ $('Webhook').item.json.body.isbn }}"
            },
            {
              "fieldName": "Title",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item).title }}"
            },
            {
              "fieldName": "NDL link",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item).link }}"
            },
            {
              "fieldName": "Author",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item).author }}"
            },
            {
              "fieldName": "Publisher",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item)['dc:publisher'] }}"
            },
            {
              "fieldName": "Published Year",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item)['dc:date']._ }}"
            },
            {
              "fieldName": "Thumbnail",
              "binaryData": true,
              "binaryProperty": "=data"
            }
          ]
        }
      },
      "id": "15acc4e6-f135-40d4-86da-7d4fa8355684",
      "name": "Put record into DB",
      "type": "n8n-nodes-base.nocoDb",
      "typeVersion": 3,
      "position": [
        1260,
        660
      ],
      "credentials": {
        "nocoDbApiToken": {
          "id": "XXXXXXXXXXXXXXX",
          "name": "NocoDB account"
        }
      }
    },
    {
      "parameters": {
        "authentication": "nocoDbApiToken",
        "operation": "create",
        "projectId": "XXXXXXXXXXXXXXX",
        "table": "XXXXXXXXXXXXXXX",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldName": "ISBN",
              "fieldValue": "={{ $('Webhook').item.json.body.isbn }}"
            },
            {
              "fieldName": "Title",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item).title }}"
            },
            {
              "fieldName": "NDL link",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item).link }}"
            },
            {
              "fieldName": "Author",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item).author }}"
            },
            {
              "fieldName": "Publisher",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item)['dc:publisher'] }}"
            },
            {
              "fieldName": "Published Year",
              "fieldValue": "={{ ($('XML').item.json.rss.channel.item[0]||$('XML').item.json.rss.channel.item)['dc:date']._ }}"
            }
          ]
        }
      },
      "id": "d103ccd3-4989-4248-a7b0-543544ac3b27",
      "name": "Put record into DB (without img)",
      "type": "n8n-nodes-base.nocoDb",
      "typeVersion": 3,
      "position": [
        1260,
        840
      ],
      "credentials": {
        "nocoDbApiToken": {
          "id": "XXXXXXXXXXXXXXX",
          "name": "NocoDB account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "2c261d5a-2622-43fe-8ab9-a439399f4a7d",
              "leftValue": "={{ $json.body.isbn }}",
              "rightValue": "97",
              "operator": {
                "type": "string",
                "operation": "startsWith"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "84dd9dd6-e157-486e-ab53-5b475b829017",
      "name": "Verify ISBN",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        680,
        460
      ]
    }
  ],
  "connections": {
    "XML": {
      "main": [
        [
          {
            "node": "Get Thumbnail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "isEmpty": {
      "main": [
        [
          {
            "node": "If isEmpty",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If isEmpty": {
      "main": [
        [
          {
            "node": "Get Book Detail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check data existence": {
      "main": [
        [
          {
            "node": "isEmpty",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Verify ISBN",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Book Detail": {
      "main": [
        [
          {
            "node": "XML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Thumbnail": {
      "main": [
        [
          {
            "node": "Thumbnail Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Thumbnail Exists": {
      "main": [
        [
          {
            "node": "Put record into DB",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Put record into DB (without img)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify ISBN": {
      "main": [
        [
          {
            "node": "Check data existence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "bcebeaf9-b92c-4ede-84bb-9e52e4a6b13f",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "a15e2c73ec1138908351e33c43110a623d9171a8d44e8402f82173c53c1cea4b"
  },
  "id": "bD7Bj7CRkj0Gcpo2",
  "tags": []
}
アップロード時に展開されるワークフロー

NocoDBのデータソースは各所有者に依存しているため、更新が必要である。

「Create New Credential」でAPI認証情報を登録し、「Base Name or ID」「Table Name or ID」で作成済みテーブルを紐づける。
これを各ノードで繰り返す。

iOSショートカット

iOSには標準でショートカットアプリが搭載されている。
バーコードの読み取りからURLアクセスまでできるため、下記画像の通り作成すれば後はISBN読み取りするだけでどんどんNocoDBのギャラリーに登録される。

本の裏にはたいてい2種類のバーコードがあるが、ISBNは97から始まる方のコードである。
どうしてももう片方のバーコードが認識される場合は、画面から外したり指で隠すしかない。

ISBNスキャン時の確認画面

最後に、家族もiOSユーザならショートカットアプリのリンクを共有するだけで同じショートカットを使ってもらえる。

課題

国立国会図書館の書影APIは完全に充実しているわけではない。
問い合わせる本によっては画像が登録されていないということも少なくない。
なるべく書影画像を埋めたいという人はAmazonや楽天のAPIを組み合わせるという手段も検討するべき。

コメント

タイトルとURLをコピーしました