本のバーコードを読み取ってNotionで読書録を作成するアプリを作ってみた 2
はじめに
はじめまして。インディーゲームを作っております、nyorokoと申します。
ゲームづくりの他に読書が好きで、「読書録を簡単に作成・管理することはできないか?」という問題意識があり、タイトルの通りのアプリを作ってみました。
第一弾はQiitaに投稿したのですが、思ったよりも反響があったため、加筆修正してZennで第二弾として当記事を投稿することにしました。
完成したもの
Notionとは?
Notionとは、タスク管理やメモ等を一元的に行うことのできるアプリです。
Evernoteと似ていると思うのですが、無料で複数のデバイスから使用可能であるなどの違いがあります。
私はPC、iPhone、iPadなど様々なデバイスを使用しているため、メモアプリとしてはNotionを使っております。
今回は、そのNotionを使って読書録を作ってみようと考えました。
ちなみに、この記事もNotionで下書きを作成しております。
アプリの実装方法について
大まかな処理の流れ
以下のようなフローで読書録を作成します。
- 本のバーコードを読み取りISBNを取得
- ISBNをキーに本の情報を取得
- 取得した本の情報をNotionに送信
どうアプリ化するか?
まず、ウェブアプリとして実装します。
理由としては、様々なデバイスで使用できることなどが挙げられます。
次に、ウェブアプリとして実装するにあたって、GAS(Google App Script)を活用します。
GASには、
- Googleアカウントさえあれば開発環境の整備が不要
- Google謹製であり情報が多く、安定している
- 無料である
などの多くの利点があるためです。
詳細な実装方法
1. GASでプロジェクトを作成
GASについてはネット上に情報が多く存在しておりますので、詳細は割愛いたします。
例えば、この記事が参考になると思います。
2. 「index.html」を作成
デフォルトで作成される「コード.js」に、以下のコードを書き込みます。
function doGet() {
var template = 'index';
return HtmlService
.createTemplateFromFile('index')
.evaluate()
.addMetaTag('viewport', 'width=device-width, initial-scale=1') //html側にメタタグを設定しても意味がなかったためこちらで設定
}
そして、同階層に「index.html」を作成します。
これにより、ウェブアプリとして公開した際に、「index.html」が表示されます。
3. quaggaJSの使用
バーコードを読み取ってISBNを取得するために、quaggaJSを使用します。
quaggaJSについては、この記事が参考になります。
今回は、2.で作成したindex.htmlにソースコードをそのまま追記させていただきました。
これで、バーコードを読み取ってISBNを取得する部分が完成しました。
いやはや、素晴らしいライブラリですね…!
4. ISBNから本の情報を取得
この目的を達成するためには、
- Google Books API
- 楽天ブックスAPI
- 国立国会図書館サーチAPI
など様々なAPIがありますが、今回は登録不要であることなどからGoogle Books APIを使用しました。
ただ、サムネイル画像が小さかったため、本の画像のみAmazonを使用しました。
今回は、
- タイトル
- サブタイトル
- 著者
- 出版日
- ページ数
- 画像
を取得することとし、まずは対応するフォームをHTML部分に作成します。
フォームの例は以下の通りです(加筆しました)。
なお、後述のBootstrapを使わない場合、class名などは適当で大丈夫です。
<form method="post" name="form" target="dummy" action="後述の8.でデプロイしたウェブアプリのURL">
<fieldset>
<div class="container">
<div class="row">
<div class="col-lg-8">
<h3 class="mt-5">
Send To Notion<br>
<small class="text-muted">本のバーコードを読んでNotionに追加</small>
</h3>
<div><canvas id="preview"></canvas></div> <!--quaggaJSのカメラがここに表示される-->
<label class="col-form-label" for="inputDefault">ISBN:</label> <!--quaggaJSでバーコードから読み取ったISBNがここに入力される-->
<input type="text" name="isbn" id="ISBN" class="form-control"><br>
<button type="submit" onclick="GetData();" class="btn btn-primary">データ取得</button><br>
<img src="" id="imagePlace"><br>
<label class="col-form-label" for="inputDefault">画像URL</label> <!--画像URLを取得したらsrcを設定する-->
<input type="text" name="imageSrc" id="imageSrc" class="form-control"><br>
<label class="col-form-label" for="inputDefault">タイトル</label>
<input type="text" name="title" id="title" class="form-control"><br>
<label class="col-form-label" for="inputDefault">サブタイトル</label>
<input type="text" name="subtitle" id="subtitle" class="form-control"><br>
<label class="col-form-label" for="inputDefault">著者</label>
<input type="text" name="authors" id="authors" class="form-control"><br>
<label class="col-form-label" for="inputDefault">発売日</label>
<input type="text" name="publishedDate" id="publishedDate" class="form-control"><br>
<label class="col-form-label" for="inputDefault">ページ数</label>
<input type="text" name="pageCount" id="pageCount" class="form-control"><br>
<button type="submit" class="btn btn-primary">Notionに送信</button><br>
</div>
</div>
</div>
</fieldset>
</form>
「データ取得」ボタンのonclickで発火するGetData関数は以下の通りです。
function GetData(){
url = "https://www.googleapis.com/books/v1/volumes?q=isbn:"; //ISBNの手前まで
isbn = document.getElementById("ISBN").value;
let request = new XMLHttpRequest();
request.open('GET', url+isbn);
request.responseType = 'json';
request.send();
request.onload = function() {
const result = request.response;
document.getElementById("title").value = result["items"][0]["volumeInfo"]["title"];
document.getElementById("subtitle").value = result["items"][0]["volumeInfo"]["subtitle"];
document.getElementById("authors").value = result["items"][0]["volumeInfo"]["authors"];
document.getElementById("publishedDate").value = result["items"][0]["volumeInfo"]["publishedDate"];
document.getElementById("pageCount").value = result["items"][0]["volumeInfo"]["pageCount"];
document.getElementById("imagePlace").src = "https://images-na.ssl-images-amazon.com/images/P/"+toISBN10(isbn)+".09.LZZZZZZZ.jpg";
document.getElementById("imageSrc").value = "https://images-na.ssl-images-amazon.com/images/P/"+toISBN10(isbn)+".09.LZZZZZZZ.jpg";
}
}
const toISBN10 = (isbn13) => {
// 1. 先頭3文字と末尾1文字を除く
const src = isbn13.slice(3, 12);
// 2. 先頭の桁から順に10、9、8…2を掛けて合計する
const sum = src.split('').map(s => parseInt(s))
.reduce((p, c, i) => (i === 1 ? p * 10 : p) + c * (10 - i));
// 3. 合計を11で割った余りを11から引く(※引き算の結果が11の場合は0、10の時はアルファベットのXにする)
const rem = 11 - sum % 11;
const checkdigit = rem === 11 ? 0 : (rem === 10 ? 'X' : rem);
// 1.の末尾に3.の値を添えて出来上がり
return `${src}${checkdigit}`;
};
5. Notion APIの下準備
Notion APIを使用して、取得した本の情報を送信します。
下準備として、データベースの作成およびIDを取得する必要があり、この記事が参考になります。
データベースのフィールドとしては、上記で取得する項目の他に、「State」フィールドを追加しました。
後続の工程で、デフォルトでは「興味あり」というセレクト項目が送信されるようにします。
6. Notion APIを使って情報を送信
index.htmlから直接Notion APIに接続すると、CORSエラーが発生してしまいます。
そのため、間にGASを挟むことでCORSエラーを回避しました。
具体的には、「コード.gs」に以下のコードを追記し、「index.html」側のフォームからdoPostを呼び出すことでCORSエラーを回避しました。
なお、JSONの構造についてはこの記事とAPI Referenceを参考にしました。
前回からの変更点として、本の画像を本文に埋め込んでいたのですが、そうすると後で本文を書きにくくなってしまったので、プロパティに移行しました(File objectのJSONの構造に癖があり結構苦戦しました…)。
なお、NotionのGallery viewはサムネイル画像のソースとして本文だけでなくFile objectのプロパティも指定することができるので、問題なくサムネイル画像が表示されます。
Gallery viewの眺めがとても良いので私はGallery viewにしております!
function doPost(e){
SendToNotion(e.parameter.title,e.parameter.subtitle,e.parameter.authors,e.parameter.publishedDate,
Number(e.parameter.pageCount),e.parameter.isbn,e.parameter.imageSrc);
}
// Notion API
function SendToNotion(title,subtitle,authors,publishedDate,pageCount,isbn,imageSrc) {
const notion_key = 'あなたのNotionキー';
const database_id = 'あなたのデータベースid';
json_data = {
'parent': {'database_id': database_id},
'properties': {
//プロパティを記述する
'Title': {
'title': [
{
'text': {
'content': title,
}
}
]
},
'Subtitle': {
'rich_text': [
{
'text': {
'content': subtitle,
}
}
]
},
'Author': {
'rich_text': [
{
'text': {
'content': authors,
}
}
]
},
'State': {
'select': {
'name': '興味あり',
'color': 'yellow'
}
},
'PublishedDate': {
'rich_text': [
{
'text': {
'content': publishedDate,
}
}
]
},
'PageCount': {
'number': pageCount
},
'Cover':{ //本文からプロパティに移行
"type": "files",
"files": [
{
"name": "Cover",
"type": "external",
"external": {
"url": imageSrc
}
}
]
},
'ISBN': {
'rich_text': [
{
'text': {
'content': isbn,
}
}
]
}
}
//ここから本文だが、今回は使わない
// 'children': [
// ]
};
const response = UrlFetchApp.fetch( 'https://api.notion.com/v1/pages', {
"method" : "post",
"headers" : {
'Content-Type' : 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + notion_key,
'Notion-Version': '2021-05-13',
},
"payload" : JSON.stringify( json_data ),
'muteHttpExceptions': true
});
}
7. 見栄えの調整(スキップ可)
このままだと「index.html」の見栄えが良くないのですが、CSSを編集するのは手間であるため、今回はBootstrap CDNを用いてサクッとイケてるデザインにしました。
8. ウェブアプリとして公開
GASの画面の「デプロイ」ボタンを押し、「ウェブアプリとして公開」しましょう。
そしたらURLが表示されるので、それを4.のフォームのactionに設定すれば完成です!
なお、iOSの場合はウェブサイトをホーム画面に追加する機能がありますので、あたかもネイティブアプリかのような見栄えにすることができます。
最後に
思ったよりも反響があったため加筆して再投稿しましたが、有益な情報になっていれば幸甚です。
特に、前回からの変更点である「本の画像のプロパティへの移行」は、地味ですがこれにより使い勝手がかなり向上しました。
前回と同じく、「アプリを作った」と言いつつ、先人が作った素晴らしいライブラリやAPIをフルに活用させていただいておりますので、感謝の念に堪えません。
最後に、私はインディーゲームを開発しております。無料でサクッと遊べますので、ぜひリンク先だけでもご覧いただけますと幸甚です。
↑最新作です。HTML5ゲームですので、PCでもタブレットでもスマホでもすぐに遊ぶことができます。
↑私のitchのプロフィールです。コンセプトと過去の作品が載っております。
Discussion