0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【書籍管理】GASでデプロイしたWEBアプリからバーコードスキャンでNotionに本の情報を登録してみる

Last updated at Posted at 2024-10-23

概要

Notionで書籍の管理をしてみたいけどすでに大量の本があって、1冊ずつ本を登録するのが面倒。
バーコード読みとるだけでバンバン登録できたらいいのに... と思ったので Google Apps Script (GAS) を使用してウェブアプリケーションをデプロイし、手持ちのデバイスのカメラから本のバーコードを読み取りNotionで作成した書籍一覧に情報を連携するシステムです。読み取ったバーコードをもとに、Google Books APIから書籍のデータを取得し、Notion APIを使用してその情報をNotionデータベースに連携します。

システム概要

image.png

Notion書籍登録イメージ

スクリーン ショット 2024-10-23 に 16.21.01 午後.png

必要なもの

  • だいたい持ってるはず
    • Google アカウント
    • Notion アカウント
  • GASに埋め込む情報
    • Google Books APIキー
    • Notion APIシークレットトークン
    • NotionデータベースID

各種APIキーの取得

Google Books APIキーの取得

本の情報を取得するためにGoogle Books APIを使用します。

  1. Google Cloud Consoleにアクセスします。
  2. プロジェクトを作成します。
  3. 「APIとサービス」 > 「ライブラリ」に移動します。
  4. 「Google Books API」を検索し、有効にします。
  5. 「APIとサービス」 > 「認証情報」に移動し、「認証情報を作成」ボタンをクリックしてAPIキーを作成します。

Notion APIシークレットトークンの取得

  1. Notion Developersにアクセスします。
  2. 新しいインテグレーションを作成し、シークレットトークンを取得します。

Notion 書籍一覧データベースの作成

  1. Notionでデータベースを作成します。
  2. データベースの名称を「書籍一覧」にします。
  3. データベースのプロパティは、以下の通りにします。
プロパティ名 説明
Title タイトル 書籍のタイトル
Subtitle テキスト 書籍のサブタイトル
ISBN テキスト 書籍のISBN番号
Description テキスト 書籍の説明
Author テキスト 書籍の著者
PublishedDate 日付 書籍の出版日
PageCount 数値 書籍のページ数

Notionデータベースにコネクションの追加

  1. 作成したデータベースを開き、「コネクション」タブに移動します。
  2. Connection - Connect to から先ほど作成したインテグレーションの名称を指定します。

NotionデータベースIDの取得

  1. Notionでデータベースをブラウザで開きます。
  2. データベースのURLからIDを取得します。URLは次の形式です: https://www.notion.so/xxxxxx?v=...xxxxxx の部分がデータベースIDです。

GASプロジェクト作成

  1. Google Apps Scriptにアクセスします。(Google Driveから任意のディレクトリから作ってもOK)
  2. 「新しいプロジェクト」をクリックします。
  3. プロジェクト名を入力します。

GASコード追加と書き換えデプロイ手順

  1. コード.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)
         }
     }
    
    
  2. Google Apps Scriptエディタにアクセスします。
  3. ファイルの+ボタンをクリックし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>
    
  4. 「デプロイ」 > 「新しいデプロイ」を選択し、ウェブアプリとしてデプロイします。
  5. デプロイ後、表示されたURLを使用してアプリにアクセスします。

使用方法

  1. アプリを開き、「Start Scanner」ボタンをクリックします。

  2. カメラが起動し、バーコードをスキャンします。

  3. スキャンしたISBNに基づいて書籍情報が表示され、Notionデータベースに追加されます。

    スキャン後にタイトルとカバー画像を表示します

    スクリーン ショット 2024-10-23 に 15.49.10 午後.png

    Notionに登録された状態、アイコンとカバーに画像が登録されます

    スクリーン ショット 2024-10-23 に 16.09.17 午後.png

まだやりたいこと

  • Kindleで買った本はどうやって登録しようか
  • たまに全然違う本の情報をとってきちゃうミスリード対策
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?