概要
Notionで書籍の管理をしてみたいけどすでに大量の本があって、1冊ずつ本を登録するのが面倒。
バーコード読みとるだけでバンバン登録できたらいいのに... と思ったので Google Apps Script (GAS) を使用してウェブアプリケーションをデプロイし、手持ちのデバイスのカメラから本のバーコードを読み取りNotionで作成した書籍一覧に情報を連携するシステムです。読み取ったバーコードをもとに、Google Books APIから書籍のデータを取得し、Notion APIを使用してその情報をNotionデータベースに連携します。
システム概要
Notion書籍登録イメージ
必要なもの
- だいたい持ってるはず
- Google アカウント
- Notion アカウント
- GASに埋め込む情報
- Google Books APIキー
- Notion APIシークレットトークン
- NotionデータベースID
各種APIキーの取得
Google Books APIキーの取得
本の情報を取得するためにGoogle Books APIを使用します。
- Google Cloud Consoleにアクセスします。
- プロジェクトを作成します。
- 「APIとサービス」 > 「ライブラリ」に移動します。
- 「Google Books API」を検索し、有効にします。
- 「APIとサービス」 > 「認証情報」に移動し、「認証情報を作成」ボタンをクリックしてAPIキーを作成します。
Notion APIシークレットトークンの取得
- Notion Developersにアクセスします。
- 新しいインテグレーションを作成し、シークレットトークンを取得します。
Notion 書籍一覧データベースの作成
- Notionでデータベースを作成します。
- データベースの名称を「書籍一覧」にします。
- データベースのプロパティは、以下の通りにします。
プロパティ名 | 型 | 説明 |
---|---|---|
Title | タイトル | 書籍のタイトル |
Subtitle | テキスト | 書籍のサブタイトル |
ISBN | テキスト | 書籍のISBN番号 |
Description | テキスト | 書籍の説明 |
Author | テキスト | 書籍の著者 |
PublishedDate | 日付 | 書籍の出版日 |
PageCount | 数値 | 書籍のページ数 |
Notionデータベースにコネクションの追加
- 作成したデータベースを開き、「コネクション」タブに移動します。
- Connection - Connect to から先ほど作成したインテグレーションの名称を指定します。
NotionデータベースIDの取得
- Notionでデータベースをブラウザで開きます。
- データベースのURLからIDを取得します。URLは次の形式です:
https://www.notion.so/xxxxxx?v=...
。xxxxxx
の部分がデータベースIDです。
GASプロジェクト作成
- Google Apps Scriptにアクセスします。(Google Driveから任意のディレクトリから作ってもOK)
- 「新しいプロジェクト」をクリックします。
- プロジェクト名を入力します。
GASコード追加と書き換えデプロイ手順
-
コード.gs
ファイルに以下のコードを貼り付け、最初の3つの定数を自分のAPIキーやトークンに書き換えます。// Notion APIのシークレットトークン const NOTION_API_TOKEN = '実際の値に書き換える'; // NotionデータベースのID const DATABASE_ID = '実際の値に書き換える'; // Google Booksを有効にしたAPIKey const GOOGLE_APIKEY = '実際の値に書き換える' function doGet() { return HtmlService.createHtmlOutputFromFile('index'); // index.htmlを表示 } function test_openbd() { const code = '9784480815781'; fetchBookInfo(code); } function convertISBN13ToISBN10(isbn13) { // ISBN-13の先頭3桁が"978"でない場合は無効 if (!isbn13.startsWith('978')) { return null; } // ISBN-13からISBN-10に変換するため、先頭の"978"を除去 const isbn10Base = isbn13.substring(3, 12); // チェックデジットを計算する let sum = 0; for (let i = 0; i < 9; i++) { sum += (i + 1) * parseInt(isbn10Base[i]); } const remainder = sum % 11; const checkDigit = remainder === 10 ? 'X' : remainder.toString(); // ISBN-10を返す return isbn10Base + checkDigit; } function fetchBookInfo(isbn) { const noImageUrl = 'https://via.placeholder.com/150?text=No+Image'; const googleBooksApiUrl = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}&country=JP&startIndex=0&maxResults=1&key=${GOOGLE_APIKEY}`; // リクエストを送信 const response = UrlFetchApp.fetch(googleBooksApiUrl); const bookData = JSON.parse(response.getContentText()); // レスポンスのデータを確認し、書籍情報を取得 if (bookData.totalItems > 0) { const volumeInfo = bookData.items[0].volumeInfo; // 書籍情報を取得 const bookInfo = {}; bookInfo.title = volumeInfo.title || 'タイトルなし'; bookInfo.subtitle = volumeInfo.subtitle || 'タイトルなし'; bookInfo.author = volumeInfo.authors ? volumeInfo.authors.join(', ') : '著者なし'; bookInfo.coverUrl = volumeInfo.imageLinks ? volumeInfo.imageLinks.thumbnail : noImageUrl, bookInfo.description = volumeInfo.description || '詳細なし'; bookInfo.asin = convertISBN13ToISBN10(isbn); bookInfo.publishedDate = volumeInfo.publishedDate || '1980-01-01'; bookInfo.pageCount = volumeInfo.pageCount || 0; Logger.log(bookInfo); // Notion連携 if (bookInfo.title) { const page = checkIfTitleExistsInNotion(bookInfo.title); if (page) { // 既存のレコードがある場合Notionデータベース内の該当レコードを更新 Logger.log('既存のレコードあり'); updateNotionPage(page, isbn, bookInfo); } else { // 新規の場合追加 Logger.log('新規レコード追加'); addBookInfoToNotion(isbn, bookInfo); } } return bookInfo; } else { Logger.log('書籍情報が見つかりませんでした。'); return null; } } // タイトルで検索してNotionデータベース内でデータがあるか確認する関数 function checkIfTitleExistsInNotion(title) { const url = `https://api.notion.com/v1/databases/${DATABASE_ID}/query`; // フィルタでタイトルに一致する項目を検索 const payload = { filter: { property: 'Title', // データベースの「タイトル」プロパティの名前を指定 rich_text: { contains: title // タイトルに指定した文字列が含まれるか検索 } } }; const options = { method: 'post', contentType: 'application/json', headers: { 'Authorization': `Bearer ${NOTION_API_TOKEN}`, 'Notion-Version': '2022-06-28' }, payload: JSON.stringify(payload) }; try { // Notion APIにリクエストを送信 const response = UrlFetchApp.fetch(url, options); const result = JSON.parse(response.getContentText()); // 検索結果が存在するか確認 if (result.results && result.results.length > 0) { Logger.log('Title already exists in the database.'); const pageId = result.results[0].id; return pageId; // 該当データが存在する } else { Logger.log('Title not found in the database.'); return false; // 該当データが存在しない } } catch (error) { Logger.log('Error: ' + error); return false; // エラーが発生した場合もfalseを返す } } // Notionのページを更新する関数 function updateNotionPage(pageId, isbn, bookInfo) { const url = `https://api.notion.com/v1/pages/${pageId}`; const payload = { properties: { 'ISBN': { rich_text: [ { text: { content: isbn // ISBN } } ] }, 'Description': { rich_text: [ { text: { content: bookInfo.description // ISBN } } ] }, 'PublishedDate': { date: { start: bookInfo.publishedDate, end: null } }, 'PageCount': { number: bookInfo.pageCount } }, cover: { type: "external", external: { url: `https://images-na.ssl-images-amazon.com/images/P/${bookInfo.asin}.09.LZZZZZZZ` // ここでURLを指定 } }, icon: { type: "external", external: { url: `https://images-na.ssl-images-amazon.com/images/P/${bookInfo.asin}.09.THUMBZZZ` } }, }; const options = { method: 'patch', contentType: 'application/json', headers: { 'Authorization': `Bearer ${NOTION_API_TOKEN}`, 'Notion-Version': '2022-06-28' }, payload: JSON.stringify(payload) }; UrlFetchApp.fetch(url, options); } function addBookInfoToNotion(isbn, bookInfo) { const today = new Date(); const formattedDate = today.toISOString().split('T')[0]; // "YYYY-MM-DD"形式に変換 const url = 'https://api.notion.com/v1/pages'; const payload = { parent: { database_id: DATABASE_ID }, cover: { type: "external", external: { url: `https://images-na.ssl-images-amazon.com/images/P/${bookInfo.asin}.09.LZZZZZZZ` // ここでURLを指定 } }, icon: { type: "external", external: { url: `https://images-na.ssl-images-amazon.com/images/P/${bookInfo.asin}.09.THUMBZZZ` } }, properties: { 'Title': { // Notion側のプロパティに合わせて設定 title: [ { text: { content: bookInfo.title // 書籍タイトル } } ] }, 'Subtitle': { rich_text: [ { text: { content: bookInfo.subtitle // ISBN } } ] }, 'ISBN': { rich_text: [ { text: { content: isbn // ISBN } } ] }, 'Description': { rich_text: [ { text: { content: bookInfo.description // ISBN } } ] }, 'Author': { rich_text: [ { text: { content: bookInfo.author // 出版社名 } } ] }, 'PublishedDate': { date: { start: bookInfo.publishedDate, end: null } }, 'PageCount': { number: bookInfo.pageCount } } }; const options = { method: 'POST', muteHttpExceptions: true, validateHttpsCertificates: false, followRedirects: false, contentType: 'application/json', headers: { 'Authorization': `Bearer ${NOTION_API_TOKEN}`, 'Notion-Version': '2022-06-28' }, payload: JSON.stringify(payload) }; try { const response = UrlFetchApp.fetch(url, options); Logger.log(response) } catch (e) { // 例外エラー処理 Logger.log('Error:') Logger.log(e) } }
- Google Apps Scriptエディタにアクセスします。
- ファイルの+ボタンをクリックしHTMLを選択,indexと入力して、
index.html
ファイルを作成し、下記を貼り付けます。<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Barcode Scanner</title> <style> #camera-container { display: flex; justify-content: center; align-items: center; } #camera-container.viewport canvas.drawingBuffer, video.drawingBuffer { visibility: hidden; } </style> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <script src="https://serratus.github.io/quaggaJS/examples/js/quagga.min.js"></script> </head> <body> <div class="container text-center"> <div class='row'> <div class="col"> <h1>Barcode Scanner</h1> <div id="camera-container"></div> <div id="barcode-result"></div> <button onclick="startScanner()" class='btn btn-primary btn-block' id="startScannerButton">Start Scanner</button> </div> </div> <div class='row'> <div class="col"> <h4>読み取り結果:</h4> <div id="bookInfo"> <h3 id="bookTitle">本のタイトル</h3> <p id="bookAuthor">著者</p> <img id="bookCover" src="https://via.placeholder.com/150?text=No+Image" alt="Book Cover"> </div> </div> </div> </div> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <script> let lastScannedCode = ''; // 前回のバーコードを保存する変数 let lastScannedISBNCode = ''; // 前回のバーコードを保存する変数 let debounceTimer = null; // 一定時間バーコードの入力を防ぐためのタイマー // 書籍情報を表示 function displayBookInfo(bookInfo) { document.getElementById('bookTitle').textContent = bookInfo.title || 'No title available'; document.getElementById('bookAuthor').textContent = "Author: " + (bookInfo.author || 'Unknown'); document.getElementById('bookCover').src = 'https://images-na.ssl-images-amazon.com/images/P/' + bookInfo.asin + '.09.LZZZZZZZ'; } function isValidISBN(code) { // ISBNは10桁か13桁 code = code.trim(); console.log(code); if (code.length === 13 && code.startsWith("978")) { return true; // 13桁のISBN (ISBN-13) } else if (code.length === 10) { return true; // 10桁のISBN (ISBN-10) } return false; } function startScanner() { document.getElementById('startScannerButton').disabled = true; document.getElementById('startScannerButton').textContent = 'Scanning...'; // drawingBufferを非表示にする Quagga.init({ inputStream: { name: "Live", type: "LiveStream", target: document.querySelector('#camera-container'), constraints: { decodeBarCodeRate: 3, successTimeout: 500, codeRepetition: true, tryVertical: true, frameRate: 15, width: 640, height: 480, facingMode: "environment" }, }, decoder: { readers: ["ean_reader"] } }, function (err) { if (err) { console.log(err); return; } Quagga.start(); // Hide the drawing buffer after Quagga starts const drawingBuffer = document.querySelector('#camera-container .drawingBuffer'); if (drawingBuffer) { drawingBuffer.style.display = 'none'; } }); Quagga.onDetected(function (result) { const code = result.codeResult.code.trim(); // 連続読み取り制御 if (code !== lastScannedCode) { console.log('Detected ISBN:', code); lastScannedCode = code; document.getElementById('startScannerButton').textContent = code + 'を検出...'; // ISBNであるかどうかを判定 if (code !== lastScannedISBNCode && isValidISBN(code)) { lastScannedISBNCode = code; // Quagga.stop(); // ISBNが有効ならスキャナを停止 google.script.run.withSuccessHandler(function (bookInfo) { if (bookInfo) { console.log('Book info:', bookInfo); document.getElementById('startScannerButton').textContent = bookInfo.title + 'を検出...'; // 画面に本の情報を表示 displayBookInfo(bookInfo); setTimeout(startScanner, 500); // 書籍情報が見つからない場合0.5秒後にスキャン再開 } else { console.log('Book not found in openBD'); // setTimeout(startScanner, 500); // 書籍情報が見つからない場合0.5秒後にスキャン再開 } }).fetchBookInfo(code); // Google Apps Script側の関数を呼び出し } else { console.log('Not a valid ISBN. Retrying...'); document.getElementById('startScannerButton').textContent = 'Not a valid ISBN. Retrying...'; } } }); } </script> </body> </html>
- 「デプロイ」 > 「新しいデプロイ」を選択し、ウェブアプリとしてデプロイします。
- デプロイ後、表示されたURLを使用してアプリにアクセスします。
使用方法
-
アプリを開き、「Start Scanner」ボタンをクリックします。
-
カメラが起動し、バーコードをスキャンします。
-
スキャンしたISBNに基づいて書籍情報が表示され、Notionデータベースに追加されます。
スキャン後にタイトルとカバー画像を表示します
Notionに登録された状態、アイコンとカバーに画像が登録されます
まだやりたいこと
- Kindleで買った本はどうやって登録しようか
- たまに全然違う本の情報をとってきちゃうミスリード対策